Compare commits
53 Commits
f92cfafda4
...
3.3.0+147
| 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
|
@@ -180,6 +180,7 @@
|
||||
"noFortuneData": "No fortune data available for this month.",
|
||||
"creatorHub": "Creator Hub",
|
||||
"creatorHubDescription": "Manage posts, analytics, and more.",
|
||||
"publicationSites": "Publication Sites",
|
||||
"developerPortal": "Developer Portal",
|
||||
"developerPortalDescription": "Build with Solar Network™.",
|
||||
"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.",
|
||||
"amountOfSplits": "Amount of Splits",
|
||||
"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/CoreOnly (= 12.4.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (5.0.4):
|
||||
- firebase_crashlytics (5.0.5):
|
||||
- Firebase/Crashlytics (= 12.4.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
@@ -140,15 +140,13 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_platform_alert (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- flutter_timezone (0.0.1):
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- KeychainAccess
|
||||
- flutter_webrtc (1.2.0):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 137.7151.04)
|
||||
@@ -216,7 +214,8 @@ PODS:
|
||||
- Flutter
|
||||
- irondash_engine_context (0.0.1):
|
||||
- Flutter
|
||||
- Kingfisher (8.6.1)
|
||||
- KeychainAccess (4.2.2)
|
||||
- Kingfisher (8.6.2)
|
||||
- KingfisherWebP (1.7.2):
|
||||
- Kingfisher (~> 8.0)
|
||||
- libwebp (>= 1.1.0)
|
||||
@@ -250,14 +249,13 @@ PODS:
|
||||
- nanopb/encode (3.30910.0)
|
||||
- native_exif (0.0.1):
|
||||
- Flutter
|
||||
- objective_c (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- pasteboard (0.0.1):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- pointer_interceptor_ios (0.0.1):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
@@ -269,7 +267,6 @@ PODS:
|
||||
- Flutter
|
||||
- record_ios (1.1.0):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- SDWebImage (5.21.3):
|
||||
- SDWebImage/Core (= 5.21.3)
|
||||
- SDWebImage/Core (5.21.3)
|
||||
@@ -315,8 +312,6 @@ PODS:
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- WebRTC-SDK (137.7151.04)
|
||||
@@ -338,7 +333,6 @@ DEPENDENCIES:
|
||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/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_timezone (from `.symlinks/plugins/flutter_timezone/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_video (from `.symlinks/plugins/media_kit_video/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`)
|
||||
- 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`)
|
||||
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/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`)
|
||||
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/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`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -390,6 +383,7 @@ SPEC REPOS:
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- KeychainAccess
|
||||
- Kingfisher
|
||||
- KingfisherWebP
|
||||
- libwebp
|
||||
@@ -397,7 +391,6 @@ SPEC REPOS:
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- SAMKeychain
|
||||
- SDWebImage
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
@@ -434,8 +427,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_platform_alert:
|
||||
:path: ".symlinks/plugins/flutter_platform_alert/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_timezone:
|
||||
@@ -460,12 +451,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
native_exif:
|
||||
:path: ".symlinks/plugins/native_exif/ios"
|
||||
objective_c:
|
||||
:path: ".symlinks/plugins/objective_c/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
pasteboard:
|
||||
:path: ".symlinks/plugins/pasteboard/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
pointer_interceptor_ios:
|
||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||
protocol_handler_ios:
|
||||
@@ -490,8 +481,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
@@ -507,7 +496,7 @@ SPEC CHECKSUMS:
|
||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
||||
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||
firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464
|
||||
firebase_crashlytics: c039028126cb45e32f4c217aa392408b0963d081
|
||||
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||
@@ -524,10 +513,9 @@ SPEC CHECKSUMS:
|
||||
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||
@@ -536,7 +524,8 @@ SPEC CHECKSUMS:
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||
Kingfisher: 7ac7a7288653787a54206b11a3c74f49ab650f1f
|
||||
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||
Kingfisher: 23d18f54677d973b713e54ce6a8f5eef6e7056ba
|
||||
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
||||
@@ -545,17 +534,16 @@ SPEC CHECKSUMS:
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
@@ -567,7 +555,6 @@ SPEC CHECKSUMS:
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||
|
||||
|
||||
@@ -2,17 +2,19 @@ import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:island/database/message.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';
|
||||
|
||||
part 'drift_db.g.dart';
|
||||
|
||||
// Define the database
|
||||
@DriftDatabase(tables: [ChatMessages, PostDrafts])
|
||||
@DriftDatabase(tables: [ChatRooms, ChatMembers, ChatMessages, PostDrafts])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 7;
|
||||
int get schemaVersion => 8;
|
||||
|
||||
@override
|
||||
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 query, {
|
||||
bool? withAttachments,
|
||||
Future<SnAccount?> Function(String accountId)? fetchAccount,
|
||||
}) async {
|
||||
var selectStatement = select(chatMessages)
|
||||
..where((m) => m.roomId.equals(roomId));
|
||||
@@ -178,7 +186,11 @@ class AppDatabase extends _$AppDatabase {
|
||||
await (selectStatement
|
||||
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
||||
.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
|
||||
@@ -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);
|
||||
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(
|
||||
id: dbMessage.id,
|
||||
roomId: dbMessage.roomId,
|
||||
senderId: dbMessage.senderId,
|
||||
sender: sender,
|
||||
data: data,
|
||||
createdAt: dbMessage.createdAt,
|
||||
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
|
||||
Future<List<SnPost>> getAllPostDrafts() async {
|
||||
final drafts = await select(postDrafts).get();
|
||||
@@ -276,4 +443,10 @@ class AppDatabase extends _$AppDatabase {
|
||||
return await (select(postDrafts)
|
||||
..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);
|
||||
}
|
||||
|
||||
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 {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get roomId => text()();
|
||||
TextColumn get senderId => text()();
|
||||
TextColumn get roomId => text().references(ChatRooms, #id)();
|
||||
TextColumn get senderId => text().references(ChatMembers, #id)();
|
||||
TextColumn get content => text().nullable()();
|
||||
TextColumn get nonce => text().nullable()();
|
||||
TextColumn get data => text()();
|
||||
@@ -72,6 +114,7 @@ class LocalChatMessage {
|
||||
final String id;
|
||||
final String roomId;
|
||||
final String senderId;
|
||||
final SnChatMember? sender;
|
||||
final Map<String, dynamic> data;
|
||||
final DateTime createdAt;
|
||||
MessageStatus status;
|
||||
@@ -94,6 +137,7 @@ class LocalChatMessage {
|
||||
required this.id,
|
||||
required this.roomId,
|
||||
required this.senderId,
|
||||
required this.sender,
|
||||
required this.data,
|
||||
required this.createdAt,
|
||||
required this.nonce,
|
||||
@@ -114,7 +158,12 @@ class LocalChatMessage {
|
||||
});
|
||||
|
||||
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(
|
||||
@@ -122,11 +171,26 @@ class LocalChatMessage {
|
||||
MessageStatus status, {
|
||||
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(
|
||||
id: message.id,
|
||||
roomId: message.chatRoomId,
|
||||
senderId: message.senderId,
|
||||
data: message.toJson(),
|
||||
sender: message.sender,
|
||||
data: msgData,
|
||||
createdAt: message.createdAt,
|
||||
status: status,
|
||||
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? get roomId => _roomId;
|
||||
|
||||
Future<void> joinRoom(String roomId) async {
|
||||
if (_roomId == roomId && _room != null) {
|
||||
SnChatRoom? _chatRoom;
|
||||
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');
|
||||
return;
|
||||
} else if (_room != null) {
|
||||
@@ -223,6 +229,7 @@ class CallNotifier extends _$CallNotifier {
|
||||
}
|
||||
}
|
||||
_roomId = roomId;
|
||||
_chatRoom = room;
|
||||
if (_room != null) {
|
||||
await _room!.disconnect();
|
||||
await _room!.dispose();
|
||||
@@ -355,6 +362,7 @@ class CallNotifier extends _$CallNotifier {
|
||||
sourceId: source.id,
|
||||
maxFrameRate: 30.0,
|
||||
captureScreenAudio: true,
|
||||
useiOSBroadcastExtension: true,
|
||||
),
|
||||
);
|
||||
await _localParticipant!.publishVideoTrack(track);
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'call.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$callNotifierHash() => r'a8ca3f625c0db3ad9992033ae70864ce15efc281';
|
||||
String _$callNotifierHash() => r'2caee30f42315e539cb4df17c0d464ceed41ffa0';
|
||||
|
||||
/// See also [CallNotifier].
|
||||
@ProviderFor(CallNotifier)
|
||||
|
||||
@@ -6,6 +6,7 @@ import "package:flutter/material.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/drift_db.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/account.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.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:island/screens/chat/chat.dart";
|
||||
import "package:island/pods/chat/chat_rooms.dart";
|
||||
import "package:island/screens/account/profile.dart";
|
||||
|
||||
part 'messages_notifier.g.dart';
|
||||
|
||||
@@ -45,6 +47,8 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
bool _isUpdatingState = false;
|
||||
DateTime? _lastPauseTime;
|
||||
|
||||
late final Future<SnAccount?> Function(String) _fetchAccount;
|
||||
|
||||
@override
|
||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||
_roomId = roomId;
|
||||
@@ -53,6 +57,15 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
final room = await ref.watch(chatroomProvider(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) {
|
||||
throw Exception('Room not found');
|
||||
}
|
||||
@@ -133,6 +146,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
_roomId,
|
||||
searchQuery,
|
||||
withAttachments: withAttachments,
|
||||
fetchAccount: _fetchAccount,
|
||||
);
|
||||
} else {
|
||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||
@@ -140,8 +154,16 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
offset: offset,
|
||||
limit: take,
|
||||
);
|
||||
dbMessages =
|
||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
||||
dbMessages = await Future.wait(
|
||||
chatMessagesFromDb
|
||||
.map(
|
||||
(msg) => _database.companionToMessage(
|
||||
msg,
|
||||
fetchAccount: _fetchAccount,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||
@@ -202,8 +224,14 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
offset: offset,
|
||||
limit: take,
|
||||
);
|
||||
final dbMessages =
|
||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
||||
final dbMessages = await Future.wait(
|
||||
chatMessagesFromDb
|
||||
.map(
|
||||
(msg) =>
|
||||
_database.companionToMessage(msg, fetchAccount: _fetchAccount),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
// Always ensure unique messages to prevent duplicate keys
|
||||
final uniqueMessages = <LocalChatMessage>[];
|
||||
@@ -272,6 +300,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
for (final message in messages) {
|
||||
await _database.saveMessage(_database.messageToCompanion(message));
|
||||
if (message.sender != null) {
|
||||
await _database.saveMember(message.sender!); // Save/update member data
|
||||
}
|
||||
if (message.nonce != null) {
|
||||
_pendingMessages.removeWhere(
|
||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
||||
@@ -300,7 +331,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
final lastMessage =
|
||||
dbMessages.isEmpty
|
||||
? null
|
||||
: _database.companionToMessage(dbMessages.first);
|
||||
: await _database.companionToMessage(
|
||||
dbMessages.first,
|
||||
fetchAccount: _fetchAccount,
|
||||
);
|
||||
|
||||
if (lastMessage == null) {
|
||||
talker.log('No local messages, fetching from network');
|
||||
@@ -468,6 +502,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
_pendingMessages[localMessage.id] = localMessage;
|
||||
_fileUploadProgress[localMessage.id] = {};
|
||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||
await _database.saveMember(mockMessage.sender);
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||
@@ -888,7 +923,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
await (_database.select(_database.chatMessages)
|
||||
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
||||
if (localMessage != null) {
|
||||
return _database.companionToMessage(localMessage);
|
||||
return _database.companionToMessage(
|
||||
localMessage,
|
||||
fetchAccount: _fetchAccount,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await _apiClient.get(
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$messagesNotifierHash() => r'fc9c99024a0801efa4894f250aea8bdc6127a0b6';
|
||||
String _$messagesNotifierHash() => r'27e5d686d9204ba39adbd1838cf4a6eaea0ac85f';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -12,6 +12,9 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
with CursorPagingNotifierMixin<FileListItem> {
|
||||
String _currentPath = '/';
|
||||
String? _poolId;
|
||||
String? _query;
|
||||
String? _order;
|
||||
bool _orderDesc = false;
|
||||
|
||||
void setPath(String path) {
|
||||
_currentPath = path;
|
||||
@@ -23,6 +26,21 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setQuery(String? query) {
|
||||
_query = query;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrder(String? order) {
|
||||
_order = order;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrderDesc(bool orderDesc) {
|
||||
_orderDesc = orderDesc;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||
|
||||
@@ -38,6 +56,16 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
queryParameters['pool'] = _poolId!;
|
||||
}
|
||||
|
||||
if (_query != null) {
|
||||
queryParameters['query'] = _query!;
|
||||
}
|
||||
|
||||
if (_order != null) {
|
||||
queryParameters['order'] = _order!;
|
||||
}
|
||||
|
||||
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/index/browse',
|
||||
queryParameters: queryParameters,
|
||||
@@ -72,6 +100,9 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||
with CursorPagingNotifierMixin<FileListItem> {
|
||||
String? _poolId;
|
||||
bool _recycled = false;
|
||||
String? _query;
|
||||
String? _order;
|
||||
bool _orderDesc = false;
|
||||
|
||||
void setPool(String? poolId) {
|
||||
_poolId = poolId;
|
||||
@@ -83,6 +114,21 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setQuery(String? query) {
|
||||
_query = query;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrder(String? order) {
|
||||
_order = order;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrderDesc(bool orderDesc) {
|
||||
_orderDesc = orderDesc;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||
|
||||
@@ -108,6 +154,16 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||
queryParameters['recycled'] = _recycled.toString();
|
||||
}
|
||||
|
||||
if (_query != null) {
|
||||
queryParameters['query'] = _query!;
|
||||
}
|
||||
|
||||
if (_order != null) {
|
||||
queryParameters['order'] = _order!;
|
||||
}
|
||||
|
||||
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/index/unindexed',
|
||||
queryParameters: queryParameters,
|
||||
|
||||
@@ -45,7 +45,7 @@ final billingQuotaProvider =
|
||||
// ignore: unused_element
|
||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||
String _$cloudFileListNotifierHash() =>
|
||||
r'5f2f80357cb31ac6473df5ac2101f9a462004f81';
|
||||
r'533dfa86f920b60cf7491fb4aeb95ece19e428af';
|
||||
|
||||
/// See also [CloudFileListNotifier].
|
||||
@ProviderFor(CloudFileListNotifier)
|
||||
@@ -66,7 +66,7 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||
typedef _$CloudFileListNotifier =
|
||||
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||
String _$unindexedFileListNotifierHash() =>
|
||||
r'48fc92432a50a562190da5fe8ed0920d171b07b6';
|
||||
r'afa487d7b956b71b21ca1b073a01364a34ede1d5';
|
||||
|
||||
/// See also [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);
|
||||
@@ -293,6 +293,10 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
||||
.toList();
|
||||
}
|
||||
|
||||
void clearAllTasks() {
|
||||
state = [];
|
||||
}
|
||||
|
||||
DriveTask? getTask(String taskId) {
|
||||
return state.where((task) => task.taskId == taskId).firstOrNull;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:firebase_analytics/firebase_analytics.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:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
@@ -36,41 +37,65 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
} catch (error, stackTrace) {
|
||||
if (!kIsWeb) {
|
||||
if (error is DioException) {
|
||||
FlutterPlatformAlert.showCustomAlert(
|
||||
windowTitle: 'failedToLoadUserInfo'.tr(),
|
||||
text: [
|
||||
(error.response?.statusCode == 401
|
||||
? 'failedToLoadUserInfoUnauthorized'
|
||||
: 'failedToLoadUserInfoNetwork')
|
||||
.tr()
|
||||
.trim(),
|
||||
'',
|
||||
'${error.response?.statusCode ?? 'Network Error'}',
|
||||
if (error.response?.headers != null) error.response?.headers,
|
||||
if (error.response?.data != null)
|
||||
jsonEncode(error.response?.data),
|
||||
].join('\n'),
|
||||
iconStyle: IconStyle.error,
|
||||
neutralButtonTitle: 'retry'.tr(),
|
||||
negativeButtonTitle: 'okay'.tr(),
|
||||
showOverlayDialog<bool>(
|
||||
builder:
|
||||
(context, close) => AlertDialog(
|
||||
title: Text('failedToLoadUserInfo'.tr()),
|
||||
content: Text(
|
||||
[
|
||||
(error.response?.statusCode == 401
|
||||
? 'failedToLoadUserInfoUnauthorized'
|
||||
: 'failedToLoadUserInfoNetwork')
|
||||
.tr()
|
||||
.trim(),
|
||||
'',
|
||||
'${error.response?.statusCode ?? 'Network Error'}',
|
||||
if (error.response?.headers != null)
|
||||
error.response?.headers,
|
||||
if (error.response?.data != null)
|
||||
jsonEncode(error.response?.data),
|
||||
].join('\n'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(false),
|
||||
child: Text('okay'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => close(true),
|
||||
child: Text('retry'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((value) {
|
||||
if (value == CustomButton.neutralButton) {
|
||||
if (value == true) {
|
||||
fetchUser();
|
||||
}
|
||||
});
|
||||
}
|
||||
FlutterPlatformAlert.showCustomAlert(
|
||||
windowTitle: 'failedToLoadUserInfo'.tr(),
|
||||
text:
|
||||
[
|
||||
'failedToLoadUserInfoNetwork'.tr(),
|
||||
error.toString(),
|
||||
].join('\n\n').trim(),
|
||||
iconStyle: IconStyle.error,
|
||||
neutralButtonTitle: 'retry'.tr(),
|
||||
negativeButtonTitle: 'okay'.tr(),
|
||||
showOverlayDialog<bool>(
|
||||
builder:
|
||||
(context, close) => AlertDialog(
|
||||
title: Text('failedToLoadUserInfo'.tr()),
|
||||
content: Text(
|
||||
[
|
||||
'failedToLoadUserInfoNetwork'.tr(),
|
||||
error.toString(),
|
||||
].join('\n\n').trim(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(false),
|
||||
child: Text('okay'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => close(true),
|
||||
child: Text('retry'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((value) {
|
||||
if (value == CustomButton.neutralButton) {
|
||||
if (value == true) {
|
||||
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/room.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/thought/think.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_detail.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/posts/compose.dart';
|
||||
import 'package:island/screens/posts/compose_article.dart';
|
||||
@@ -117,14 +118,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
return ArticleEditScreen(id: id);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'chatCall',
|
||||
path: '/chat/:id/call',
|
||||
builder: (context, state) {
|
||||
final id = state.pathParameters['id']!;
|
||||
return CallScreen(roomId: id);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'logs',
|
||||
path: '/logs',
|
||||
@@ -170,6 +163,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
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
|
||||
ShellRoute(
|
||||
navigatorKey: _tabsShellKey,
|
||||
@@ -427,23 +436,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
name: 'files',
|
||||
path: '/files',
|
||||
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
|
||||
@@ -485,6 +477,29 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
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(
|
||||
name: 'creatorStickers',
|
||||
|
||||
@@ -384,9 +384,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
icon: const Icon(Symbols.content_copy, size: 16),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: value));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('copiedToClipboard'.tr())),
|
||||
);
|
||||
showSnackBar('copiedToClipboard'.tr());
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
|
||||
@@ -9,6 +9,7 @@ class CaptchaScreen extends ConsumerWidget {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: false,
|
||||
builder: (context) => const CaptchaScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class CaptchaScreen extends ConsumerStatefulWidget {
|
||||
static Future<String?> show(BuildContext context) {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const CaptchaScreen(),
|
||||
);
|
||||
|
||||
@@ -3,30 +3,31 @@ import 'package:flutter/material.dart' hide ConnectionState;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:island/widgets/app_scaffold.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_participant_tile.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class CallScreen extends HookConsumerWidget {
|
||||
final String roomId;
|
||||
const CallScreen({super.key, required this.roomId});
|
||||
final SnChatRoom room;
|
||||
const CallScreen({super.key, required this.room});
|
||||
|
||||
@override
|
||||
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 callNotifier = ref.watch(callNotifierProvider.notifier);
|
||||
|
||||
useEffect(() {
|
||||
talker.info('[Call] Joining the call...');
|
||||
callNotifier.joinRoom(roomId).catchError((_) {
|
||||
callNotifier.joinRoom(room).catchError((_) {
|
||||
showConfirmAlert(
|
||||
'Seems there already has a call connected, do you want override it?',
|
||||
'Call already connected',
|
||||
@@ -35,7 +36,7 @@ class CallScreen extends HookConsumerWidget {
|
||||
talker.info('[Call] Joining the call... with overrides');
|
||||
callNotifier.disconnect();
|
||||
callNotifier.dispose();
|
||||
callNotifier.joinRoom(roomId);
|
||||
callNotifier.joinRoom(room);
|
||||
});
|
||||
});
|
||||
return null;
|
||||
@@ -110,7 +111,7 @@ class CallScreen extends HookConsumerWidget {
|
||||
onPressed: () {
|
||||
callNotifier.disconnect();
|
||||
callNotifier.dispose();
|
||||
callNotifier.joinRoom(roomId);
|
||||
callNotifier.joinRoom(room);
|
||||
},
|
||||
child: Text('retry').tr(),
|
||||
),
|
||||
@@ -120,72 +121,7 @@ class CallScreen extends HookConsumerWidget {
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(child: CallContent()),
|
||||
CallControlsBar(),
|
||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -6,9 +8,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/chat_summary.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/services/event_bus.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
@@ -47,6 +53,17 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
.watch(chatSummaryProvider)
|
||||
.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() {
|
||||
if (subtitle != null) return subtitle!;
|
||||
|
||||
@@ -55,7 +72,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
if (data == null) {
|
||||
return isDirect && room.description == null
|
||||
? Text(
|
||||
room.members!.map((e) => '@${e.account.name}').join(', '),
|
||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1);
|
||||
@@ -111,7 +128,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
(_, _) =>
|
||||
isDirect && room.description == null
|
||||
? Text(
|
||||
room.members!.map((e) => '@${e.account.name}').join(', '),
|
||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||
maxLines: 1,
|
||||
)
|
||||
: 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(
|
||||
leading: Badge(
|
||||
isLabelVisible: summary.when(
|
||||
@@ -132,7 +160,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
(isDirect && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
room.members!
|
||||
validMembers
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
@@ -140,11 +168,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
||||
: ProfilePictureWidget(fileId: room.picture?.id),
|
||||
),
|
||||
title: Text(
|
||||
(isDirect && room.name == null)
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name ?? '',
|
||||
),
|
||||
title: Text(titleText),
|
||||
subtitle: buildSubtitle(),
|
||||
trailing: trailing, // Add this line
|
||||
onTap: () async {
|
||||
@@ -162,12 +186,92 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
|
||||
@riverpod
|
||||
Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
||||
final db = ref.watch(databaseProvider);
|
||||
|
||||
try {
|
||||
final localRoomsData = await db.select(db.chatRooms).get();
|
||||
if (localRoomsData.isNotEmpty) {
|
||||
final localRooms = await Future.wait(
|
||||
localRoomsData.map((row) async {
|
||||
final membersRows =
|
||||
await (db.select(db.chatMembers)
|
||||
..where((m) => m.chatRoomId.equals(row.id))).get();
|
||||
final members =
|
||||
membersRows.map((mRow) {
|
||||
final account = SnAccount.fromJson(mRow.account);
|
||||
SnAccountStatus? status;
|
||||
if (mRow.status != null) {
|
||||
status = SnAccountStatus.fromJson(jsonDecode(mRow.status!));
|
||||
}
|
||||
return SnChatMember(
|
||||
id: mRow.id,
|
||||
chatRoomId: mRow.chatRoomId,
|
||||
accountId: mRow.accountId,
|
||||
account: account,
|
||||
nick: mRow.nick,
|
||||
role: mRow.role,
|
||||
notify: mRow.notify,
|
||||
joinedAt: mRow.joinedAt,
|
||||
breakUntil: mRow.breakUntil,
|
||||
timeoutUntil: mRow.timeoutUntil,
|
||||
isBot: mRow.isBot,
|
||||
status: status,
|
||||
lastTyped: mRow.lastTyped,
|
||||
createdAt: mRow.createdAt,
|
||||
updatedAt: mRow.updatedAt,
|
||||
deletedAt: mRow.deletedAt,
|
||||
chatRoom: null,
|
||||
);
|
||||
}).toList();
|
||||
return SnChatRoom(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
type: row.type,
|
||||
isPublic: row.isPublic!,
|
||||
isCommunity: row.isCommunity!,
|
||||
picture:
|
||||
row.picture != null ? SnCloudFile.fromJson(row.picture!) : null,
|
||||
background:
|
||||
row.background != null
|
||||
? SnCloudFile.fromJson(row.background!)
|
||||
: null,
|
||||
realmId: row.realmId,
|
||||
realm: null,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
deletedAt: row.deletedAt,
|
||||
members: members,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// Background sync
|
||||
Future(() async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final resp = await client.get('/sphere/chat');
|
||||
final remoteRooms =
|
||||
resp.data
|
||||
.map((e) => SnChatRoom.fromJson(e))
|
||||
.cast<SnChatRoom>()
|
||||
.toList();
|
||||
await db.saveChatRooms(remoteRooms);
|
||||
ref.invalidateSelf();
|
||||
} catch (_) {}
|
||||
}).ignore();
|
||||
|
||||
return localRooms;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Fallback to API
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/sphere/chat');
|
||||
return resp.data
|
||||
.map((e) => SnChatRoom.fromJson(e))
|
||||
.cast<SnChatRoom>()
|
||||
.toList();
|
||||
final rooms =
|
||||
resp.data.map((e) => SnChatRoom.fromJson(e)).cast<SnChatRoom>().toList();
|
||||
await db.saveChatRooms(rooms);
|
||||
return rooms;
|
||||
}
|
||||
|
||||
class ChatListBodyWidget extends HookConsumerWidget {
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'chat.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatroomsJoinedHash() => r'3bb6389af07e81007680484d04bf5fe6f6c10571';
|
||||
String _$chatroomsJoinedHash() => r'9523efecd1869e7dd26adfc8ec87be48db19ee1c';
|
||||
|
||||
/// See also [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/public_room_preview.dart";
|
||||
import "package:island/screens/thought/think_sheet.dart";
|
||||
import "package:island/screens/chat/widgets/message_item_wrapper.dart";
|
||||
|
||||
class ChatRoomScreen extends HookConsumerWidget {
|
||||
final String id;
|
||||
@@ -148,9 +149,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final inputKey = useMemoized(() => GlobalKey());
|
||||
final inputHeight = useState<double>(80.0);
|
||||
|
||||
// Track previous height for smooth animations
|
||||
final previousInputHeight = usePrevious<double>(inputHeight.value);
|
||||
|
||||
// Periodic height measurement for dynamic sizing
|
||||
useEffect(() {
|
||||
final timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
|
||||
@@ -181,6 +179,38 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final isSelectionMode = useState<bool>(false);
|
||||
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 isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
||||
|
||||
@@ -624,428 +654,70 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
||||
previousInputHeight != null && previousInputHeight != inputHeight.value
|
||||
? TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(
|
||||
begin: previousInputHeight,
|
||||
end: inputHeight.value,
|
||||
),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
builder:
|
||||
(context, height, child) => SuperListView.builder(
|
||||
listController: listController,
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom + 8 + height,
|
||||
),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
findChildIndexCallback: (key) {
|
||||
if (key is! ValueKey<String>) return null;
|
||||
final messageId = key.value.substring(
|
||||
messageKeyPrefix.length,
|
||||
);
|
||||
final index = messageList.indexWhere(
|
||||
(m) => (m.nonce ?? m.id) == messageId,
|
||||
);
|
||||
// Return null for invalid indices to let SuperListView handle it properly
|
||||
return index >= 0 ? index : null;
|
||||
},
|
||||
extentEstimation: (_, _) => 40,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
final nextMessage =
|
||||
index < messageList.length - 1
|
||||
? messageList[index + 1]
|
||||
: null;
|
||||
final isLastInGroup =
|
||||
nextMessage == null ||
|
||||
nextMessage.senderId != message.senderId ||
|
||||
nextMessage.createdAt
|
||||
.difference(message.createdAt)
|
||||
.inMinutes
|
||||
.abs() >
|
||||
3;
|
||||
Widget chatMessageListWidget(
|
||||
List<LocalChatMessage> messageList,
|
||||
) => AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8 + inputHeight.value,
|
||||
),
|
||||
child: SuperListView.builder(
|
||||
listController: listController,
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
findChildIndexCallback: (key) {
|
||||
if (key is! ValueKey<String>) return null;
|
||||
final messageId = key.value.substring(messageKeyPrefix.length);
|
||||
final index = messageList.indexWhere(
|
||||
(m) => (m.nonce ?? m.id) == messageId,
|
||||
);
|
||||
return 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 key = Key('$messageKeyPrefix${message.nonce ?? message.id}');
|
||||
|
||||
final messageWidget = chatIdentity.when(
|
||||
skipError: true,
|
||||
data:
|
||||
(identity) => GestureDetector(
|
||||
onLongPress: () {
|
||||
if (!isSelectionMode.value) {
|
||||
toggleSelectionMode();
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (isSelectionMode.value) {
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
color:
|
||||
selectedMessages.value.contains(message.id)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(0.3)
|
||||
: null,
|
||||
child: Stack(
|
||||
children: [
|
||||
MessageItem(
|
||||
key:
|
||||
settings.disableAnimation
|
||||
? key
|
||||
: null,
|
||||
message: message,
|
||||
isCurrentUser:
|
||||
identity?.id == message.senderId,
|
||||
onAction:
|
||||
isSelectionMode.value
|
||||
? null
|
||||
: (action) {
|
||||
switch (action) {
|
||||
case MessageItemAction.delete:
|
||||
messagesNotifier
|
||||
.deleteMessage(
|
||||
message.id,
|
||||
);
|
||||
case MessageItemAction.edit:
|
||||
messageEditingTo.value =
|
||||
message
|
||||
.toRemoteMessage();
|
||||
messageController.text =
|
||||
messageEditingTo
|
||||
.value
|
||||
?.content ??
|
||||
'';
|
||||
attachments.value =
|
||||
messageEditingTo
|
||||
.value!
|
||||
.attachments
|
||||
.map(
|
||||
(e) =>
|
||||
UniversalFile.fromAttachment(
|
||||
e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
case MessageItemAction
|
||||
.forward:
|
||||
messageForwardingTo.value =
|
||||
message
|
||||
.toRemoteMessage();
|
||||
case MessageItemAction.reply:
|
||||
messageReplyingTo.value =
|
||||
message
|
||||
.toRemoteMessage();
|
||||
case MessageItemAction.resend:
|
||||
messagesNotifier
|
||||
.retryMessage(
|
||||
message.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
onJump: (messageId) {
|
||||
scrollToMessage(
|
||||
messageId: messageId,
|
||||
messageList: messageList,
|
||||
messagesNotifier: messagesNotifier,
|
||||
listController: listController,
|
||||
scrollController: scrollController,
|
||||
ref: ref,
|
||||
);
|
||||
},
|
||||
progress:
|
||||
attachmentProgress.value[message.id],
|
||||
showAvatar: isLastInGroup,
|
||||
isSelectionMode: isSelectionMode.value,
|
||||
isSelected: selectedMessages.value
|
||||
.contains(message.id),
|
||||
onToggleSelection: toggleMessageSelection,
|
||||
onEnterSelectionMode: () {
|
||||
if (!isSelectionMode.value) {
|
||||
toggleSelectionMode();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (selectedMessages.value.contains(
|
||||
message.id,
|
||||
))
|
||||
...([
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 12,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
loading:
|
||||
() => MessageItem(
|
||||
message: message,
|
||||
isCurrentUser: false,
|
||||
onAction: null,
|
||||
progress: null,
|
||||
showAvatar: false,
|
||||
onJump: (_) {},
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
return settings.disableAnimation
|
||||
? messageWidget
|
||||
: TweenAnimationBuilder<double>(
|
||||
key: key,
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
duration: Duration(
|
||||
milliseconds: 400 + (index % 5) * 50,
|
||||
), // Staggered delay
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, animationValue, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
0,
|
||||
20 * (1 - animationValue),
|
||||
), // Slide up from bottom
|
||||
child: Opacity(
|
||||
opacity: animationValue,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: messageWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: SuperListView.builder(
|
||||
listController: listController,
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
8 +
|
||||
inputHeight.value,
|
||||
),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
findChildIndexCallback: (key) {
|
||||
if (key is! ValueKey<String>) return null;
|
||||
final messageId = key.value.substring(messageKeyPrefix.length);
|
||||
final index = messageList.indexWhere(
|
||||
(m) => (m.nonce ?? m.id) == messageId,
|
||||
);
|
||||
// Return null for invalid indices to let SuperListView handle it properly
|
||||
return index >= 0 ? index : null;
|
||||
},
|
||||
extentEstimation: (_, _) => 40,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
final nextMessage =
|
||||
index < messageList.length - 1
|
||||
? messageList[index + 1]
|
||||
: null;
|
||||
final isLastInGroup =
|
||||
nextMessage == null ||
|
||||
nextMessage.senderId != message.senderId ||
|
||||
nextMessage.createdAt
|
||||
.difference(message.createdAt)
|
||||
.inMinutes
|
||||
.abs() >
|
||||
3;
|
||||
|
||||
// Use a stable animation key that doesn't change during message lifecycle
|
||||
final key = Key(
|
||||
'$messageKeyPrefix${message.nonce ?? message.id}',
|
||||
);
|
||||
|
||||
final messageWidget = chatIdentity.when(
|
||||
skipError: true,
|
||||
data:
|
||||
(identity) => GestureDetector(
|
||||
onLongPress: () {
|
||||
if (!isSelectionMode.value) {
|
||||
toggleSelectionMode();
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (isSelectionMode.value) {
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
color:
|
||||
selectedMessages.value.contains(message.id)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(0.3)
|
||||
: null,
|
||||
child: Stack(
|
||||
children: [
|
||||
MessageItem(
|
||||
key: settings.disableAnimation ? key : null,
|
||||
message: message,
|
||||
isCurrentUser: identity?.id == message.senderId,
|
||||
onAction:
|
||||
isSelectionMode.value
|
||||
? null
|
||||
: (action) {
|
||||
switch (action) {
|
||||
case MessageItemAction.delete:
|
||||
messagesNotifier.deleteMessage(
|
||||
message.id,
|
||||
);
|
||||
case MessageItemAction.edit:
|
||||
messageEditingTo.value =
|
||||
message.toRemoteMessage();
|
||||
messageController.text =
|
||||
messageEditingTo
|
||||
.value
|
||||
?.content ??
|
||||
'';
|
||||
attachments.value =
|
||||
messageEditingTo
|
||||
.value!
|
||||
.attachments
|
||||
.map(
|
||||
(e) =>
|
||||
UniversalFile.fromAttachment(
|
||||
e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
case MessageItemAction.forward:
|
||||
messageForwardingTo.value =
|
||||
message.toRemoteMessage();
|
||||
case MessageItemAction.reply:
|
||||
messageReplyingTo.value =
|
||||
message.toRemoteMessage();
|
||||
case MessageItemAction.resend:
|
||||
messagesNotifier.retryMessage(
|
||||
message.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
onJump: (messageId) {
|
||||
scrollToMessage(
|
||||
messageId: messageId,
|
||||
messageList: messageList,
|
||||
messagesNotifier: messagesNotifier,
|
||||
listController: listController,
|
||||
scrollController: scrollController,
|
||||
ref: ref,
|
||||
);
|
||||
},
|
||||
progress: attachmentProgress.value[message.id],
|
||||
showAvatar: isLastInGroup,
|
||||
isSelectionMode: isSelectionMode.value,
|
||||
isSelected: selectedMessages.value.contains(
|
||||
message.id,
|
||||
),
|
||||
onToggleSelection: toggleMessageSelection,
|
||||
onEnterSelectionMode: () {
|
||||
if (!isSelectionMode.value) {
|
||||
toggleSelectionMode();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (selectedMessages.value.contains(message.id))
|
||||
...([
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 12,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
loading:
|
||||
() => MessageItem(
|
||||
message: message,
|
||||
isCurrentUser: false,
|
||||
onAction: null,
|
||||
progress: null,
|
||||
showAvatar: false,
|
||||
onJump: (_) {},
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
return settings.disableAnimation
|
||||
? messageWidget
|
||||
: TweenAnimationBuilder<double>(
|
||||
key: key,
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
duration: Duration(
|
||||
milliseconds: 400 + (index % 5) * 50,
|
||||
), // Staggered delay
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, animationValue, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
0,
|
||||
20 * (1 - animationValue),
|
||||
), // Slide up from bottom
|
||||
child: Opacity(opacity: animationValue, child: child),
|
||||
);
|
||||
},
|
||||
child: messageWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
return MessageItemWrapper(
|
||||
key: key,
|
||||
message: message,
|
||||
index: index,
|
||||
isLastInGroup: isLastInGroup,
|
||||
isSelectionMode: isSelectionMode.value,
|
||||
selectedMessages: selectedMessages.value,
|
||||
chatIdentity: chatIdentity,
|
||||
toggleSelectionMode: toggleSelectionMode,
|
||||
toggleMessageSelection: toggleMessageSelection,
|
||||
onMessageAction: onMessageAction,
|
||||
onJump:
|
||||
(messageId) => scrollToMessage(
|
||||
messageId: messageId,
|
||||
messageList: messageList,
|
||||
messagesNotifier: messagesNotifier,
|
||||
listController: listController,
|
||||
scrollController: scrollController,
|
||||
ref: ref,
|
||||
),
|
||||
attachmentProgress: attachmentProgress.value,
|
||||
disableAnimation: settings.disableAnimation,
|
||||
roomOpenTime: roomOpenTime,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
@@ -1066,7 +738,11 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
AudioCallButton(roomId: id),
|
||||
chatRoom.when(
|
||||
data: (data) => AudioCallButton(room: data!),
|
||||
error: (err, _) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () async {
|
||||
@@ -1167,7 +843,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
left: 0,
|
||||
right: 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)
|
||||
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(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
@@ -585,7 +600,7 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
).padding(horizontal: 12),
|
||||
buildNavigationWidget(true),
|
||||
],
|
||||
)
|
||||
).padding(vertical: 24)
|
||||
: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
@@ -831,7 +846,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
|
||||
try {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
await apiClient.post(
|
||||
'/sphere/publishers/$publisherUname/invites',
|
||||
'/sphere/publishers/invites/$publisherUname',
|
||||
data: {'related_user_id': result.id, 'role': 0},
|
||||
);
|
||||
// Refresh both providers
|
||||
@@ -1119,7 +1134,7 @@ class _PublisherInviteSheet extends HookConsumerWidget {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post(
|
||||
'/publishers/invites/${invite.publisher!.name}/accept',
|
||||
'/sphere/publishers/invites/${invite.publisher!.name}/accept',
|
||||
);
|
||||
ref.invalidate(publisherInvitesProvider);
|
||||
ref.invalidate(publishersManagedProvider);
|
||||
@@ -1132,7 +1147,7 @@ class _PublisherInviteSheet extends HookConsumerWidget {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post(
|
||||
'/publishers/invites/${invite.publisher!.name}/decline',
|
||||
'/sphere/publishers/invites/${invite.publisher!.name}/decline',
|
||||
);
|
||||
ref.invalidate(publisherInvitesProvider);
|
||||
} catch (err) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/poll.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/poll/poll_editor.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/poll/poll_feedback.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@@ -234,19 +235,9 @@ class _CreatorPollItem extends HookConsumerWidget {
|
||||
'/sphere/polls/${pollWithStats.id}',
|
||||
);
|
||||
ref.invalidate(pollListNotifierProvider(pubName));
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Poll deleted successfully'),
|
||||
),
|
||||
);
|
||||
}
|
||||
showSnackBar('Poll deleted successfully');
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to delete poll')),
|
||||
);
|
||||
}
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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(
|
||||
child: FriendsOverviewWidget(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
hideWhenEmpty: true,
|
||||
),
|
||||
),
|
||||
if (notificationCount.value != null &&
|
||||
|
||||
@@ -6,19 +6,24 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/file_references.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/upload_tasks.dart';
|
||||
import 'package:island/models/drive_task.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
import 'package:island/widgets/content/file_viewer_contents.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class FileDetailScreen extends HookConsumerWidget {
|
||||
final SnCloudFile item;
|
||||
@@ -167,6 +172,24 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
break;
|
||||
}
|
||||
|
||||
// Add references button
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: Icon(Icons.link),
|
||||
onPressed:
|
||||
() => showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'File References',
|
||||
child: ReferencesList(fileId: item.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Always add info button
|
||||
actions.add(
|
||||
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
||||
@@ -210,9 +233,8 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> _downloadFile(WidgetRef ref) async {
|
||||
final taskId = ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.addLocalDownloadTask(item);
|
||||
final taskNotifier = ref.read(uploadTasksProvider.notifier);
|
||||
final taskId = taskNotifier.addLocalDownloadTask(item);
|
||||
try {
|
||||
showSnackBar('Downloading file...');
|
||||
|
||||
@@ -230,12 +252,8 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
queryParameters: {'original': true},
|
||||
onReceiveProgress: (count, total) {
|
||||
if (total > 0) {
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateDownloadProgress(taskId, count, total);
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateTransmissionProgress(taskId, count / total);
|
||||
taskNotifier.updateDownloadProgress(taskId, count, total);
|
||||
taskNotifier.updateTransmissionProgress(taskId, count / total);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -244,18 +262,14 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateTaskStatus(taskId, DriveTaskStatus.completed);
|
||||
taskNotifier.updateTaskStatus(taskId, DriveTaskStatus.completed);
|
||||
showSnackBar('File saved to downloads');
|
||||
} catch (e) {
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateTaskStatus(
|
||||
taskId,
|
||||
DriveTaskStatus.failed,
|
||||
errorMessage: e.toString(),
|
||||
);
|
||||
taskNotifier.updateTaskStatus(
|
||||
taskId,
|
||||
DriveTaskStatus.failed,
|
||||
errorMessage: e.toString(),
|
||||
);
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
@@ -275,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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'The last selected number will be your special number.',
|
||||
'lotteryLastNumberSpecial'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -738,11 +738,11 @@ class _LotteryPurchaseSheetState extends State<LotteryPurchaseSheet> {
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter a multiplier';
|
||||
return 'lotteryMultiplierRequired'.tr();
|
||||
}
|
||||
final parsed = int.tryParse(value);
|
||||
if (parsed == null || parsed < 1 || parsed > 10) {
|
||||
return 'Multiplier must be between 1 and 10';
|
||||
return 'lotteryMultiplierRange'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -451,7 +451,7 @@ class PollEditorScreen extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
Navigator.of(context).pop();
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
|
||||
@@ -82,12 +82,32 @@ class _ParsedVersion implements Comparable<_ParsedVersion> {
|
||||
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
|
||||
int compareTo(_ParsedVersion other) {
|
||||
if (major != other.major) return major.compareTo(other.major);
|
||||
if (minor != other.minor) return minor.compareTo(other.minor);
|
||||
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
|
||||
@@ -244,13 +264,14 @@ class UpdateService {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => _WindowsUpdateDialog(
|
||||
updateUrl: url,
|
||||
onComplete: () {
|
||||
// Close the update sheet
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
builder:
|
||||
(context) => _WindowsUpdateDialog(
|
||||
updateUrl: url,
|
||||
onComplete: () {
|
||||
// Close the update sheet
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -321,7 +342,9 @@ class _WindowsUpdateDialog extends StatefulWidget {
|
||||
|
||||
class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
||||
final ValueNotifier<double?> progressNotifier = ValueNotifier<double?>(null);
|
||||
final ValueNotifier<String> messageNotifier = ValueNotifier<String>('Downloading installer...');
|
||||
final ValueNotifier<String> messageNotifier = ValueNotifier<String>(
|
||||
'Downloading installer...',
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -392,16 +415,17 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
||||
Navigator.of(context).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Update Failed'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'),
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Update Failed'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -458,7 +482,9 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
talker.info('[Update] Windows installer downloaded successfully to: $filePath');
|
||||
talker.info(
|
||||
'[Update] Windows installer downloaded successfully to: $filePath',
|
||||
);
|
||||
return filePath;
|
||||
} else {
|
||||
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;
|
||||
} catch (e) {
|
||||
talker.error('[Update] Error extracting Windows installer: $e');
|
||||
@@ -514,10 +542,11 @@ class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
||||
talker.info('[Update] Running Windows installer from: $extractDir');
|
||||
|
||||
final dir = Directory(extractDir);
|
||||
final exeFiles = dir
|
||||
.listSync()
|
||||
.where((f) => f is File && f.path.endsWith('.exe'))
|
||||
.toList();
|
||||
final exeFiles =
|
||||
dir
|
||||
.listSync()
|
||||
.where((f) => f is File && f.path.endsWith('.exe'))
|
||||
.toList();
|
||||
|
||||
if (exeFiles.isEmpty) {
|
||||
talker.info('[Update] No .exe file found in extracted directory');
|
||||
|
||||
@@ -39,6 +39,7 @@ class AccountName extends StatelessWidget {
|
||||
final String? textOverride;
|
||||
final bool ignorePermissions;
|
||||
final bool hideVerificationMark;
|
||||
final bool hideOverlay;
|
||||
const AccountName({
|
||||
super.key,
|
||||
required this.account,
|
||||
@@ -46,6 +47,7 @@ class AccountName extends StatelessWidget {
|
||||
this.textOverride,
|
||||
this.ignorePermissions = false,
|
||||
this.hideVerificationMark = false,
|
||||
this.hideOverlay = false,
|
||||
});
|
||||
|
||||
Alignment _parseGradientDirection(String direction) {
|
||||
@@ -189,20 +191,33 @@ class AccountName extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (account.perkSubscription != null)
|
||||
StellarMembershipMark(membership: account.perkSubscription!),
|
||||
StellarMembershipMark(
|
||||
membership: account.perkSubscription!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (account.profile.verification != null &&
|
||||
!hideVerificationMark)
|
||||
VerificationMark(mark: account.profile.verification!),
|
||||
if (account.automatedId != null)
|
||||
Tooltip(
|
||||
message: 'accountAutomated'.tr(),
|
||||
child: Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
),
|
||||
VerificationMark(
|
||||
mark: account.profile.verification!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (account.automatedId != null)
|
||||
hideOverlay
|
||||
? Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
)
|
||||
: Tooltip(
|
||||
message: 'accountAutomated'.tr(),
|
||||
child: Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -233,19 +248,32 @@ class AccountName extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (account.perkSubscription != null)
|
||||
StellarMembershipMark(membership: account.perkSubscription!),
|
||||
if (account.profile.verification != null)
|
||||
VerificationMark(mark: account.profile.verification!),
|
||||
if (account.automatedId != null)
|
||||
Tooltip(
|
||||
message: 'accountAutomated'.tr(),
|
||||
child: Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
),
|
||||
StellarMembershipMark(
|
||||
membership: account.perkSubscription!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (account.profile.verification != null)
|
||||
VerificationMark(
|
||||
mark: account.profile.verification!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (account.automatedId != null)
|
||||
hideOverlay
|
||||
? Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
)
|
||||
: Tooltip(
|
||||
message: 'accountAutomated'.tr(),
|
||||
child: Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -253,39 +281,53 @@ class AccountName extends StatelessWidget {
|
||||
|
||||
class VerificationMark extends StatelessWidget {
|
||||
final SnVerificationMark mark;
|
||||
const VerificationMark({super.key, required this.mark});
|
||||
final bool hideOverlay;
|
||||
const VerificationMark({
|
||||
super.key,
|
||||
required this.mark,
|
||||
this.hideOverlay = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
richMessage: TextSpan(
|
||||
text: mark.title ?? 'No title',
|
||||
children: [
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: mark.description ?? 'descriptionNone'.tr(),
|
||||
style: TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: Icon(
|
||||
mark.type == 4
|
||||
? Symbols.play_circle
|
||||
: mark.type == 0
|
||||
? Symbols.build_circle
|
||||
: Symbols.verified,
|
||||
size: 16,
|
||||
color: kVerificationMarkColors[mark.type],
|
||||
fill: 1,
|
||||
),
|
||||
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(
|
||||
text: mark.title ?? 'No title',
|
||||
children: [
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: mark.description ?? 'descriptionNone'.tr(),
|
||||
style: TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: icon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StellarMembershipMark extends StatelessWidget {
|
||||
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) {
|
||||
switch (identifier) {
|
||||
@@ -321,20 +363,24 @@ class StellarMembershipMark extends StatelessWidget {
|
||||
final tierColor = _getMembershipTierColor(membership.identifier);
|
||||
final tierIcon = Symbols.kid_star;
|
||||
|
||||
return Tooltip(
|
||||
richMessage: TextSpan(
|
||||
text: 'stellarMembership'.tr(),
|
||||
children: [
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: 'currentMembershipMember'.tr(args: [tierName]),
|
||||
style: TextStyle(fontWeight: FontWeight.normal),
|
||||
final icon = Icon(tierIcon, size: 16, color: tierColor, fill: 1);
|
||||
|
||||
return hideOverlay
|
||||
? icon
|
||||
: Tooltip(
|
||||
richMessage: TextSpan(
|
||||
text: 'stellarMembership'.tr(),
|
||||
children: [
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: 'currentMembershipMember'.tr(args: [tierName]),
|
||||
style: TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
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_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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.
|
||||
/// 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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedItem = useState<HeatmapItem?>(null);
|
||||
final selectedItem = useState<SelectedHeatmapItem?>(null);
|
||||
|
||||
final now = DateTime.now();
|
||||
|
||||
@@ -101,48 +114,18 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
final heatmapData = HeatmapData(
|
||||
rows: [
|
||||
'Mon',
|
||||
'Tue',
|
||||
'Wed',
|
||||
'Thu',
|
||||
'Fri',
|
||||
'Sat',
|
||||
'Sun',
|
||||
], // Days of week vertically
|
||||
columns:
|
||||
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',
|
||||
),
|
||||
],
|
||||
);
|
||||
// Find maximum value for color scaling
|
||||
final maxValue =
|
||||
dataMap.values.isNotEmpty
|
||||
? dataMap.values.reduce((a, b) => a > b ? a : b)
|
||||
: 1.0;
|
||||
|
||||
// Helper function to get color based on activity level
|
||||
Color getActivityColor(double value) {
|
||||
if (value == 0) return Colors.grey.withOpacity(0.1);
|
||||
final intensity = value / maxValue;
|
||||
return Colors.green.withOpacity(0.2 + (intensity * 0.8));
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
@@ -151,39 +134,103 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'activityHeatmap',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
).tr(),
|
||||
const Gap(8),
|
||||
// Month labels row
|
||||
// Month labels row - aligned with month start positions
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 30), // Space for day labels
|
||||
...monthLabels.asMap().entries.map((entry) {
|
||||
final month = entry.value;
|
||||
...List.generate(weeks.length, (weekIndex) {
|
||||
// Check if this week is the start of a month
|
||||
final monthIndex = monthPositions.indexOf(weekIndex);
|
||||
final monthText =
|
||||
monthIndex != -1 ? monthLabels[monthIndex] : null;
|
||||
|
||||
return Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
month,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
return monthText != null
|
||||
? Expanded(
|
||||
child: Text(
|
||||
monthText,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
Heatmap(
|
||||
heatmapData: heatmapData,
|
||||
rowsVisible: 7,
|
||||
showXAxisLabels: false,
|
||||
onItemSelectedListener: (item) {
|
||||
selectedItem.value = item;
|
||||
},
|
||||
// Custom heatmap grid
|
||||
Column(
|
||||
children: List.generate(7, (dayIndex) {
|
||||
final dayLabels = [
|
||||
'Mon',
|
||||
'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),
|
||||
// Legend
|
||||
@@ -203,9 +250,7 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
TextSpan(
|
||||
text: _formatDate(
|
||||
selectedItem.value!.xAxisLabel ?? '',
|
||||
),
|
||||
text: _formatDate(selectedItem.value!.dateString),
|
||||
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:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:styled_widget/styled_widget.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}) {
|
||||
final context = globalOverlay.currentState!.context;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
@@ -29,43 +30,60 @@ void showSnackBar(String message, {SnackBarAction? action}) {
|
||||
),
|
||||
),
|
||||
),
|
||||
curve: Curves.easeInOut,
|
||||
snackBarPosition: SnackBarPosition.bottom,
|
||||
);
|
||||
}
|
||||
|
||||
void clearSnackBar(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
}
|
||||
|
||||
OverlayEntry? _loadingOverlay;
|
||||
GlobalKey<_FadeOverlayState> _loadingOverlayKey = GlobalKey();
|
||||
|
||||
class _FadeOverlay extends StatefulWidget {
|
||||
const _FadeOverlay({super.key, required this.child});
|
||||
final Widget child;
|
||||
const _FadeOverlay({
|
||||
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
|
||||
State<_FadeOverlay> createState() => _FadeOverlayState();
|
||||
}
|
||||
|
||||
class _FadeOverlayState extends State<_FadeOverlay> {
|
||||
bool _visible = false;
|
||||
class _FadeOverlayState extends State<_FadeOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() => _visible = true);
|
||||
});
|
||||
_controller = AnimationController(vsync: this, duration: widget.duration);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> animateOut() async {
|
||||
await _controller.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
opacity: _visible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: widget.child,
|
||||
);
|
||||
final animation = CurvedAnimation(parent: _controller, curve: widget.curve);
|
||||
if (widget.builder != null) {
|
||||
return widget.builder!(context, animation);
|
||||
}
|
||||
return FadeTransition(opacity: animation, child: widget.child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,10 +127,156 @@ void hideLoadingModal(BuildContext context) async {
|
||||
final state = entry.mounted ? _loadingOverlayKey.currentState : null;
|
||||
|
||||
if (state != null) {
|
||||
// ignore: invalid_use_of_protected_member
|
||||
state.setState(() => state._visible = false);
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
await state.animateOut();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (uri.queryParameters.isNotEmpty) {
|
||||
path =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
@@ -28,12 +28,12 @@ Future<SnRealtimeCall?> ongoingCall(Ref ref, String roomId) async {
|
||||
}
|
||||
|
||||
class AudioCallButton extends HookConsumerWidget {
|
||||
final String roomId;
|
||||
const AudioCallButton({super.key, required this.roomId});
|
||||
final SnChatRoom room;
|
||||
const AudioCallButton({super.key, required this.room});
|
||||
|
||||
@override
|
||||
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 callNotifier = ref.read(callNotifierProvider.notifier);
|
||||
final isLoading = useState(false);
|
||||
@@ -42,10 +42,9 @@ class AudioCallButton extends HookConsumerWidget {
|
||||
Future<void> handleJoin() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await apiClient.post('/sphere/chat/realtime/$roomId');
|
||||
if (context.mounted) {
|
||||
context.pushNamed('chatCall', pathParameters: {'id': roomId});
|
||||
}
|
||||
await apiClient.post('/sphere/chat/realtime/${room.id}');
|
||||
// Just join the room, the overlay will handle the UI
|
||||
await callNotifier.joinRoom(room);
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
} finally {
|
||||
@@ -56,7 +55,7 @@ class AudioCallButton extends HookConsumerWidget {
|
||||
Future<void> handleEnd() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await apiClient.delete('/sphere/chat/realtime/$roomId');
|
||||
await apiClient.delete('/sphere/chat/realtime/${room.id}');
|
||||
callNotifier.dispose(); // Clean up call resources
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
@@ -94,9 +93,14 @@ class AudioCallButton extends HookConsumerWidget {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.call),
|
||||
tooltip: 'Join Ongoing Call',
|
||||
onPressed: () {
|
||||
if (context.mounted) {
|
||||
context.pushNamed('chatCall', pathParameters: {'id': roomId});
|
||||
onPressed: () async {
|
||||
isLoading.value = true;
|
||||
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
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.call),
|
||||
tooltip: 'Start/Join Call',
|
||||
tooltip: 'Start Call',
|
||||
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:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/models/chat.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/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/content/sheet.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';
|
||||
|
||||
class CallControlsBar extends HookConsumerWidget {
|
||||
const CallControlsBar({super.key});
|
||||
final bool isCompact;
|
||||
const CallControlsBar({super.key, this.isCompact = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -21,11 +29,14 @@ class CallControlsBar extends HookConsumerWidget {
|
||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isCompact ? 12 : 20,
|
||||
vertical: isCompact ? 8 : 16,
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 16,
|
||||
spacing: 16,
|
||||
runSpacing: isCompact ? 12 : 16,
|
||||
spacing: isCompact ? 12 : 16,
|
||||
children: [
|
||||
_buildCircularButtonWithDropdown(
|
||||
context: context,
|
||||
@@ -73,12 +84,15 @@ class CallControlsBar extends HookConsumerWidget {
|
||||
(innerContext) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Gap(24),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.logout, fill: 1),
|
||||
title: Text('callLeave').tr(),
|
||||
onTap: () {
|
||||
callNotifier.disconnect();
|
||||
Navigator.of(context).pop();
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
Navigator.of(innerContext).pop();
|
||||
},
|
||||
),
|
||||
@@ -96,7 +110,9 @@ class CallControlsBar extends HookConsumerWidget {
|
||||
);
|
||||
callNotifier.dispose();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
Navigator.of(innerContext).pop();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -124,12 +140,14 @@ class CallControlsBar extends HookConsumerWidget {
|
||||
required Color backgroundColor,
|
||||
Color? iconColor,
|
||||
}) {
|
||||
final size = isCompact ? 40.0 : 56.0;
|
||||
final iconSize = isCompact ? 20.0 : 24.0;
|
||||
return Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
|
||||
child: IconButton(
|
||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
|
||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: iconSize),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
@@ -145,41 +163,51 @@ class CallControlsBar extends HookConsumerWidget {
|
||||
Color? iconColor,
|
||||
String? deviceType, // 'videoinput' or 'audioinput'
|
||||
}) {
|
||||
final size = isCompact ? 40.0 : 56.0;
|
||||
final iconSize = isCompact ? 20.0 : 24.0;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: 24),
|
||||
icon: Icon(icon, color: iconColor ?? Colors.white, size: iconSize),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
),
|
||||
if (hasDropdown && deviceType != null)
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showDeviceSelectionDialog(context, ref, deviceType),
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 0.5,
|
||||
bottom: 0,
|
||||
right: isCompact ? 0 : -4,
|
||||
child: Material(
|
||||
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(
|
||||
width: isCompact ? 16 : 24,
|
||||
height: isCompact ? 16 : 24,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.white,
|
||||
size: isCompact ? 12 : 20,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.white,
|
||||
size: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -279,34 +307,150 @@ class CallControlsBar extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
class CallOverlayBar extends HookConsumerWidget {
|
||||
const CallOverlayBar({super.key});
|
||||
final SnChatRoom room;
|
||||
const CallOverlayBar({super.key, required this.room});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final callState = ref.watch(callNotifierProvider);
|
||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||
// Only show if connected and not on the call screen
|
||||
if (!callState.isConnected) return const SizedBox.shrink();
|
||||
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
|
||||
|
||||
// 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 =
|
||||
callNotifier.participants
|
||||
.where(
|
||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||
)
|
||||
.isEmpty
|
||||
? callNotifier.participants.first
|
||||
? callNotifier.participants.firstOrNull
|
||||
: callNotifier.participants
|
||||
.where(
|
||||
(element) => element.remoteParticipant.lastSpokeAt != null,
|
||||
)
|
||||
.fold(
|
||||
callNotifier.participants.first,
|
||||
callNotifier.participants.firstOrNull,
|
||||
(value, element) =>
|
||||
element.remoteParticipant.lastSpokeAt != null &&
|
||||
(value.remoteParticipant.lastSpokeAt == null ||
|
||||
(value?.remoteParticipant.lastSpokeAt == null ||
|
||||
element.remoteParticipant.lastSpokeAt!
|
||||
.compareTo(
|
||||
value
|
||||
value!
|
||||
.remoteParticipant
|
||||
.lastSpokeAt!,
|
||||
) >
|
||||
@@ -315,11 +459,76 @@ class CallOverlayBar extends HookConsumerWidget {
|
||||
: value,
|
||||
);
|
||||
|
||||
final actionButtonStyle = ButtonStyle(
|
||||
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
|
||||
);
|
||||
if (lastSpeaker == null) {
|
||||
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(
|
||||
key: const ValueKey('active_collapsed'),
|
||||
onTap: () => isExpanded.value = true,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Row(
|
||||
@@ -328,30 +537,32 @@ class CallOverlayBar extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (callNotifier.localParticipant == null) {
|
||||
return CircularProgressIndicator().center();
|
||||
}
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child:
|
||||
SpeakingRippleAvatar(
|
||||
live: lastSpeaker,
|
||||
size: 36,
|
||||
).center(),
|
||||
);
|
||||
},
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child:
|
||||
SpeakingRippleAvatar(
|
||||
live: lastSpeaker,
|
||||
size: 36,
|
||||
).center(),
|
||||
),
|
||||
const Gap(8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('@${lastSpeaker.participant.identity}').bold(),
|
||||
Text(
|
||||
formatDuration(callState.duration),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
_getChatRoomName(callNotifier.chatRoom, userInfo),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
formatDuration(callState.duration),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -361,41 +572,20 @@ class CallOverlayBar extends HookConsumerWidget {
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
callNotifier.toggleMicrophone();
|
||||
},
|
||||
style: actionButtonStyle,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
|
||||
),
|
||||
onPressed: () {
|
||||
callNotifier.toggleCamera();
|
||||
},
|
||||
style: actionButtonStyle,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
callState.isScreenSharing
|
||||
? Icons.stop_screen_share
|
||||
: Icons.screen_share,
|
||||
),
|
||||
onPressed: () {
|
||||
callNotifier.toggleScreenShare(context);
|
||||
},
|
||||
style: actionButtonStyle,
|
||||
icon: const Icon(Icons.expand_more),
|
||||
onPressed: () => isExpanded.value = true,
|
||||
tooltip: 'Expand',
|
||||
),
|
||||
],
|
||||
).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: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 {
|
||||
final CallParticipantLive live;
|
||||
final double size;
|
||||
@@ -18,79 +71,58 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final account = ref.watch(accountProvider(live.participant.identity));
|
||||
|
||||
final avatarRadius = size / 2;
|
||||
final clampedLevel = live.remoteParticipant.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: live.remoteParticipant.isSpeaking ? rippleRadius : avatarRadius,
|
||||
),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
builder: (context, animatedRadius, child) {
|
||||
return Stack(
|
||||
return SpeakingRipple(
|
||||
size: size,
|
||||
audioLevel: live.remoteParticipant.audioLevel,
|
||||
isSpeaking: live.remoteParticipant.isSpeaking,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: size,
|
||||
height: size,
|
||||
alignment: Alignment.center,
|
||||
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),
|
||||
decoration: const BoxDecoration(shape: BoxShape.circle),
|
||||
child: account.when(
|
||||
data:
|
||||
(value) => CallParticipantGestureDetector(
|
||||
participant: live,
|
||||
child: ProfilePictureWidget(
|
||||
file: value.profile.picture,
|
||||
radius: size / 2,
|
||||
),
|
||||
),
|
||||
error:
|
||||
(_, _) => CircleAvatar(
|
||||
radius: size / 2,
|
||||
child: const Icon(Symbols.person_remove),
|
||||
),
|
||||
loading:
|
||||
() => CircleAvatar(
|
||||
radius: size / 2,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (live.remoteParticipant.isMuted)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
Container(
|
||||
width: size,
|
||||
height: size,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle),
|
||||
child: account.when(
|
||||
data:
|
||||
(value) => CallParticipantGestureDetector(
|
||||
participant: live,
|
||||
child: ProfilePictureWidget(
|
||||
file: value.profile.picture,
|
||||
radius: size / 2,
|
||||
),
|
||||
),
|
||||
error:
|
||||
(_, _) => CircleAvatar(
|
||||
radius: size / 2,
|
||||
child: const Icon(Symbols.person_remove),
|
||||
),
|
||||
loading:
|
||||
() => CircleAvatar(
|
||||
radius: size / 2,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
child: const Icon(
|
||||
Symbols.mic_off,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
if (live.remoteParticipant.isMuted)
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: const Icon(
|
||||
Symbols.mic_off,
|
||||
size: 14,
|
||||
fill: 1,
|
||||
).padding(left: 1.5, top: 1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -103,6 +135,8 @@ class CallParticipantTile extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final userInfo = ref.watch(accountProvider(live.participant.name));
|
||||
|
||||
final hasVideo =
|
||||
live.hasVideo &&
|
||||
live.remoteParticipant.trackPublications.values
|
||||
@@ -110,42 +144,92 @@ class CallParticipantTile extends HookConsumerWidget {
|
||||
.isNotEmpty;
|
||||
|
||||
if (hasVideo) {
|
||||
return Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: VideoTrackRenderer(
|
||||
live.remoteParticipant.trackPublications.values
|
||||
.where((track) => track.kind == TrackType.VIDEO)
|
||||
.first
|
||||
.track
|
||||
as VideoTrack,
|
||||
renderMode: VideoRenderMode.platformView,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Text(
|
||||
'@${live.participant.name}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
BoxShadow(
|
||||
color: Colors.black54,
|
||||
offset: Offset(1, 1),
|
||||
spreadRadius: 8,
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: LayoutBuilder(
|
||||
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,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
VideoTrackRenderer(
|
||||
live.remoteParticipant.trackPublications.values
|
||||
.where((track) => track.kind == TrackType.VIDEO)
|
||||
.first
|
||||
.track
|
||||
as VideoTrack,
|
||||
renderMode: VideoRenderMode.platformView,
|
||||
),
|
||||
Positioned(
|
||||
left: 8,
|
||||
bottom: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (live.remoteParticipant.isMuted)
|
||||
const Icon(
|
||||
Symbols.mic_off,
|
||||
size: 14,
|
||||
color: Colors.redAccent,
|
||||
).padding(right: 4),
|
||||
Text(
|
||||
userInfo.value?.nick ?? live.participant.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SpeakingRippleAvatar(size: 84, live: live);
|
||||
|
||||
@@ -45,6 +45,8 @@ void _insertPlaceholder(TextEditingController controller, String placeholder) {
|
||||
|
||||
const kInputDrawerExpandedHeight = 180.0;
|
||||
|
||||
const kExpandedSectionTabHeight = 32.0;
|
||||
|
||||
class _ExpandedSection extends StatelessWidget {
|
||||
final TextEditingController messageController;
|
||||
final SnPoll? selectedPoll;
|
||||
@@ -75,9 +77,23 @@ class _ExpandedSection extends StatelessWidget {
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
splashBorderRadius: const BorderRadius.all(Radius.circular(40)),
|
||||
tabs: [Tab(text: 'Features'), Tab(text: 'Stickers')],
|
||||
PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kExpandedSectionTabHeight),
|
||||
child: TabBar(
|
||||
splashBorderRadius: const BorderRadius.all(
|
||||
Radius.circular(40),
|
||||
),
|
||||
tabs: [
|
||||
Tab(
|
||||
text: 'features'.tr(),
|
||||
height: kExpandedSectionTabHeight,
|
||||
),
|
||||
Tab(
|
||||
text: 'stickers'.tr(),
|
||||
height: kExpandedSectionTabHeight,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: kInputDrawerExpandedHeight,
|
||||
@@ -248,6 +264,7 @@ class ChatInput extends HookConsumerWidget {
|
||||
|
||||
void send() {
|
||||
inputFocusNode.requestFocus();
|
||||
if (isExpanded.value) isExpanded.value = false;
|
||||
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) {
|
||||
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(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: _CloudFileListEntry(
|
||||
@@ -242,7 +244,15 @@ class CloudFileList extends HookConsumerWidget {
|
||||
minWidth: minWidth ?? 0,
|
||||
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 meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
||||
|
||||
final ratio = meta['ratio'] as num?;
|
||||
|
||||
final fit = BoxFit.cover;
|
||||
|
||||
Widget bg = const SizedBox.shrink();
|
||||
@@ -448,9 +456,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
||||
fit: fit,
|
||||
useInternalGate: false,
|
||||
))
|
||||
: IntrinsicWidth(
|
||||
child: IntrinsicHeight(child: const SizedBox.shrink()),
|
||||
);
|
||||
: const SizedBox.shrink();
|
||||
|
||||
Widget overlays;
|
||||
if (lockedByDS) {
|
||||
@@ -481,7 +487,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
||||
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:gal/gal.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
@@ -171,6 +172,24 @@ class CloudFileLightbox extends HookConsumerWidget {
|
||||
),
|
||||
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(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/widgets/alert.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:island/widgets/data_saving_gate.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
|
||||
import 'file_viewer_contents.dart';
|
||||
import 'image.dart';
|
||||
@@ -66,34 +59,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
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(
|
||||
height: 400,
|
||||
decoration: BoxDecoration(
|
||||
@@ -166,30 +131,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.download,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: downloadFile,
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.info,
|
||||
Symbols.more_horiz,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
context.pushNamed(
|
||||
'fileDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
extra: item,
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -201,34 +156,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
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(
|
||||
height: 400,
|
||||
decoration: BoxDecoration(
|
||||
@@ -304,30 +231,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.download,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: downloadFile,
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.info,
|
||||
Symbols.more_horiz,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
context.pushNamed(
|
||||
'fileDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
extra: item,
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -356,41 +273,13 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
'audio' => AudioFileContent(item: item, uri: uri),
|
||||
_ => Builder(
|
||||
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(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -422,19 +311,12 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: downloadFile,
|
||||
icon: const Icon(Symbols.download),
|
||||
label: Text('download').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
context.pushNamed(
|
||||
'fileDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
extra: item,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.info),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:math' as math;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -29,8 +30,64 @@ class PdfFileContent extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pdfViewer = useMemoized(() => SfPdfViewer.network(uri), [uri]);
|
||||
return pdfViewer;
|
||||
final fileFuture = useMemoized(
|
||||
() => 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'));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,6 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
label: 'copyToClipboard'.tr(),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: href));
|
||||
clearSnackBar(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -51,7 +51,10 @@ class SheetScaffold extends StatelessWidget {
|
||||
const Spacer(),
|
||||
...actions,
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
icon: Icon(
|
||||
Symbols.close,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed:
|
||||
() =>
|
||||
onClose != null
|
||||
|
||||
@@ -29,7 +29,7 @@ Future<void> _showSetTokenDialog(BuildContext context, WidgetRef ref) async {
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Enter access token',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -17,6 +19,7 @@ import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/utils/file_icon_utils.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/utils/text.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
@@ -64,6 +67,52 @@ class FileListView extends HookConsumerWidget {
|
||||
|
||||
if (usage == null) return const SizedBox.shrink();
|
||||
|
||||
final unindexedNotifier = ref.read(
|
||||
unindexedFileListNotifierProvider.notifier,
|
||||
);
|
||||
final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||
final recycled = useState<bool>(false);
|
||||
final poolsAsync = ref.watch(poolsProvider);
|
||||
final isSelectionMode = useState<bool>(false);
|
||||
final selectedFileIds = useState<Set<String>>({});
|
||||
final currentVisibleItems = useState<List<FileListItem>>([]);
|
||||
final query = useState<String?>(null);
|
||||
final order = useState<String?>('date');
|
||||
final orderDesc = useState<bool>(true);
|
||||
final queryDebounceTimer = useRef<Timer?>(null);
|
||||
|
||||
useEffect(() {
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
isSelectionMode.value = false;
|
||||
selectedFileIds.value.clear();
|
||||
}
|
||||
return null;
|
||||
}, [mode.value]);
|
||||
|
||||
useEffect(() {
|
||||
// Sync pool when mode or selectedPool changes
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
unindexedNotifier.setPool(selectedPool.value?.id);
|
||||
} else {
|
||||
cloudNotifier.setPool(selectedPool.value?.id);
|
||||
}
|
||||
return null;
|
||||
}, [selectedPool.value, mode.value]);
|
||||
|
||||
useEffect(() {
|
||||
// Sync query, order, and orderDesc filters
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
unindexedNotifier.setQuery(query.value);
|
||||
unindexedNotifier.setOrder(order.value);
|
||||
unindexedNotifier.setOrderDesc(orderDesc.value);
|
||||
} else {
|
||||
cloudNotifier.setQuery(query.value);
|
||||
cloudNotifier.setOrder(order.value);
|
||||
cloudNotifier.setOrderDesc(orderDesc.value);
|
||||
}
|
||||
return null;
|
||||
}, [query.value, order.value, orderDesc.value, mode.value]);
|
||||
|
||||
final isRefreshing = ref.watch(
|
||||
mode.value == FileListMode.normal
|
||||
? cloudFileListNotifierProvider.select((value) => value.isLoading)
|
||||
@@ -90,6 +139,9 @@ class FileListView extends HookConsumerWidget {
|
||||
ref,
|
||||
context,
|
||||
viewMode,
|
||||
isSelectionMode,
|
||||
selectedFileIds,
|
||||
currentVisibleItems,
|
||||
),
|
||||
),
|
||||
_ => PagingHelperSliverView(
|
||||
@@ -110,97 +162,13 @@ class FileListView extends HookConsumerWidget {
|
||||
context,
|
||||
currentPath,
|
||||
viewMode,
|
||||
isSelectionMode,
|
||||
selectedFileIds,
|
||||
currentVisibleItems,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
final unindexedNotifier = ref.read(
|
||||
unindexedFileListNotifierProvider.notifier,
|
||||
);
|
||||
final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||
final recycled = useState<bool>(false);
|
||||
final poolsAsync = ref.watch(poolsProvider);
|
||||
|
||||
useEffect(() {
|
||||
// Sync pool when mode or selectedPool changes
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
unindexedNotifier.setPool(selectedPool.value?.id);
|
||||
} else {
|
||||
cloudNotifier.setPool(selectedPool.value?.id);
|
||||
}
|
||||
return null;
|
||||
}, [selectedPool.value, mode.value]);
|
||||
|
||||
final poolDropdownItems = poolsAsync.when(
|
||||
data:
|
||||
(pools) => [
|
||||
const DropdownMenuItem<SnFilePool>(
|
||||
value: null,
|
||||
child: Text('All Pools', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
...pools.map(
|
||||
(p) => DropdownMenuItem<SnFilePool>(
|
||||
value: p,
|
||||
child: Text(p.name, style: const TextStyle(fontSize: 14)),
|
||||
),
|
||||
),
|
||||
],
|
||||
loading: () => const <DropdownMenuItem<SnFilePool>>[],
|
||||
error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[],
|
||||
);
|
||||
|
||||
final poolDropdown = DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnFilePool>(
|
||||
value: selectedPool.value,
|
||||
items: poolDropdownItems,
|
||||
onChanged:
|
||||
isRefreshing
|
||||
? null
|
||||
: (value) {
|
||||
selectedPool.value = value;
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
unindexedNotifier.setPool(value?.id);
|
||||
} else {
|
||||
cloudNotifier.setPool(value?.id);
|
||||
}
|
||||
},
|
||||
customButton: Container(
|
||||
height: 28,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(ref.context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 6,
|
||||
children: [
|
||||
const Icon(Symbols.pool, size: 16),
|
||||
Flexible(
|
||||
child: Text(
|
||||
selectedPool.value?.name ?? 'All files',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).fontSize(12),
|
||||
),
|
||||
],
|
||||
).height(24),
|
||||
),
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.zero,
|
||||
height: 28,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: const DropdownStyleData(maxHeight: 200),
|
||||
),
|
||||
);
|
||||
|
||||
late Widget pathContent;
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
pathContent = const Text(
|
||||
@@ -310,7 +278,20 @@ class FileListView extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Gap(12),
|
||||
poolDropdown.padding(horizontal: 16),
|
||||
_buildGlobalFilters(
|
||||
ref,
|
||||
poolsAsync,
|
||||
selectedPool,
|
||||
mode,
|
||||
currentPath,
|
||||
isRefreshing,
|
||||
unindexedNotifier,
|
||||
cloudNotifier,
|
||||
query,
|
||||
order,
|
||||
orderDesc,
|
||||
queryDebounceTimer,
|
||||
),
|
||||
const Gap(6),
|
||||
Card(
|
||||
child: Padding(
|
||||
@@ -380,6 +361,23 @@ class FileListView extends HookConsumerWidget {
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isSelectionMode.value
|
||||
? Symbols.close
|
||||
: Symbols.select_check_box,
|
||||
),
|
||||
onPressed:
|
||||
() => isSelectionMode.value = !isSelectionMode.value,
|
||||
tooltip:
|
||||
isSelectionMode.value
|
||||
? 'Exit Selection Mode'
|
||||
: 'Enter Selection Mode',
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
if (mode.value == FileListMode.normal)
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.create_new_folder),
|
||||
@@ -442,6 +440,117 @@ class FileListView extends HookConsumerWidget {
|
||||
viewMode.value == FileListViewMode.waterfall ? 12 : null,
|
||||
),
|
||||
),
|
||||
if (isSelectionMode.value)
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
elevation: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
isSelectionMode.value = false;
|
||||
selectedFileIds.value.clear();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
const Gap(12),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
final allIds =
|
||||
currentVisibleItems.value
|
||||
.expand(
|
||||
(item) => item.maybeMap(
|
||||
file: (f) => [f.fileIndex.id],
|
||||
unindexedFile: (u) => [u.file.id],
|
||||
orElse: () => <String>[],
|
||||
),
|
||||
)
|
||||
.toSet();
|
||||
|
||||
if (allIds
|
||||
.difference(selectedFileIds.value)
|
||||
.isEmpty) {
|
||||
// All items are selected, deselect all
|
||||
selectedFileIds.value.clear();
|
||||
} else {
|
||||
// Select all visible items
|
||||
selectedFileIds.value = allIds;
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
currentVisibleItems.value.isEmpty
|
||||
? 'Select All'
|
||||
: currentVisibleItems.value
|
||||
.expand(
|
||||
(item) => item.maybeMap(
|
||||
file: (f) => [f.fileIndex.id],
|
||||
unindexedFile: (u) => [u.file.id],
|
||||
orElse: () => <String>[],
|
||||
),
|
||||
)
|
||||
.toSet()
|
||||
.difference(selectedFileIds.value)
|
||||
.isEmpty
|
||||
? 'Deselect All'
|
||||
: 'Select All',
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text('${selectedFileIds.value.length} selected'),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Symbols.delete),
|
||||
label: const Text('Delete'),
|
||||
onPressed:
|
||||
selectedFileIds.value.isNotEmpty
|
||||
? () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'Are you sure you want to delete the selected files?',
|
||||
'Delete Selected Files',
|
||||
);
|
||||
if (!confirmed) return;
|
||||
if (context.mounted) {
|
||||
showLoadingModal(context);
|
||||
}
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final resp = await client.post(
|
||||
'/drive/files/batches/delete',
|
||||
data: {
|
||||
'file_ids':
|
||||
selectedFileIds.value.toList(),
|
||||
},
|
||||
);
|
||||
final count = resp.data['count'] as int;
|
||||
selectedFileIds.value.clear();
|
||||
isSelectionMode.value = false;
|
||||
ref.invalidate(
|
||||
mode.value == FileListMode.normal
|
||||
? cloudFileListNotifierProvider
|
||||
: unindexedFileListNotifierProvider,
|
||||
);
|
||||
showSnackBar('Deleted $count files.');
|
||||
} catch (e) {
|
||||
showSnackBar(
|
||||
'Failed to delete selected files.',
|
||||
);
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -456,7 +565,11 @@ class FileListView extends HookConsumerWidget {
|
||||
BuildContext context,
|
||||
ValueNotifier<String> currentPath,
|
||||
ValueNotifier<FileListViewMode> currentViewMode,
|
||||
ValueNotifier<bool> isSelectionMode,
|
||||
ValueNotifier<Set<String>> selectedFileIds,
|
||||
ValueNotifier<List<FileListItem>> currentVisibleItems,
|
||||
) {
|
||||
currentVisibleItems.value = items;
|
||||
return switch (currentViewMode.value) {
|
||||
// Waterfall mode
|
||||
FileListViewMode.waterfall => SliverMasonryGrid(
|
||||
@@ -476,7 +589,23 @@ class FileListView extends HookConsumerWidget {
|
||||
|
||||
final item = items[index];
|
||||
return item.map(
|
||||
file: (fileItem) => _buildWaterfallFileTile(fileItem, ref, context),
|
||||
file:
|
||||
(fileItem) => _buildWaterfallFileTile(
|
||||
fileItem,
|
||||
ref,
|
||||
context,
|
||||
isSelectionMode.value,
|
||||
selectedFileIds.value.contains(fileItem.fileIndex.id),
|
||||
() {
|
||||
if (selectedFileIds.value.contains(fileItem.fileIndex.id)) {
|
||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||
..remove(fileItem.fileIndex.id);
|
||||
} else {
|
||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||
..add(fileItem.fileIndex.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
folder:
|
||||
(folderItem) =>
|
||||
_buildWaterfallFolderTile(folderItem, currentPath, context),
|
||||
@@ -497,58 +626,23 @@ class FileListView extends HookConsumerWidget {
|
||||
|
||||
final item = items[index];
|
||||
return item.map(
|
||||
file: (fileItem) {
|
||||
final file = fileItem.fileIndex.file;
|
||||
return ListTile(
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: getFileIcon(file, size: 24),
|
||||
),
|
||||
),
|
||||
title:
|
||||
file.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(
|
||||
file.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(formatFileSize(file.size)),
|
||||
onTap: () {
|
||||
context.push('/files/${fileItem.fileIndex.id}', extra: file);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (context.mounted) {
|
||||
showLoadingModal(context);
|
||||
}
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete(
|
||||
'/drive/index/remove/${fileItem.fileIndex.id}',
|
||||
);
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteFile'.tr());
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
file:
|
||||
(fileItem) => _buildIndexedListTile(
|
||||
fileItem,
|
||||
ref,
|
||||
context,
|
||||
isSelectionMode.value,
|
||||
selectedFileIds.value.contains(fileItem.fileIndex.id),
|
||||
() {
|
||||
if (selectedFileIds.value.contains(fileItem.fileIndex.id)) {
|
||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||
..remove(fileItem.fileIndex.id);
|
||||
} else {
|
||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||
..add(fileItem.fileIndex.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
folder:
|
||||
(folderItem) => ListTile(
|
||||
leading: ClipRRect(
|
||||
@@ -564,7 +658,7 @@ class FileListView extends HookConsumerWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: const Text('Folder'),
|
||||
subtitle: const Text('folder').tr(),
|
||||
onTap: () {
|
||||
final newPath =
|
||||
currentPath.value == '/'
|
||||
@@ -675,6 +769,9 @@ class FileListView extends HookConsumerWidget {
|
||||
FileItem fileItem,
|
||||
WidgetRef ref,
|
||||
BuildContext context,
|
||||
bool isSelectionMode,
|
||||
bool isSelected,
|
||||
VoidCallback? toggleSelection,
|
||||
) {
|
||||
return _buildWaterfallFileTileBase(
|
||||
fileItem.fileIndex.file,
|
||||
@@ -710,6 +807,9 @@ class FileListView extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
],
|
||||
isSelectionMode,
|
||||
isSelected,
|
||||
toggleSelection,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -719,6 +819,9 @@ class FileListView extends HookConsumerWidget {
|
||||
WidgetRef ref,
|
||||
BuildContext context,
|
||||
List<Widget>? actions,
|
||||
bool isSelectionMode,
|
||||
bool isSelected,
|
||||
VoidCallback? toggleSelection,
|
||||
) {
|
||||
final meta = file.fileMeta is Map ? (file.fileMeta as Map) : const {};
|
||||
final ratio =
|
||||
@@ -786,7 +889,11 @@ class FileListView extends HookConsumerWidget {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
context.push(getRoutePath(), extra: file);
|
||||
if (isSelectionMode && toggleSelection != null) {
|
||||
toggleSelection();
|
||||
} else {
|
||||
context.push(getRoutePath(), extra: file);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -812,7 +919,13 @@ class FileListView extends HookConsumerWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
getFileIcon(file, size: 24, tinyPreview: false),
|
||||
if (isSelectionMode)
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) => toggleSelection?.call(),
|
||||
)
|
||||
else
|
||||
getFileIcon(file, size: 24, tinyPreview: false),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -897,7 +1010,11 @@ class FileListView extends HookConsumerWidget {
|
||||
WidgetRef ref,
|
||||
BuildContext context,
|
||||
ValueNotifier<FileListViewMode> currentViewMode,
|
||||
ValueNotifier<bool> isSelectionMode,
|
||||
ValueNotifier<Set<String>> selectedFileIds,
|
||||
ValueNotifier<List<FileListItem>> currentVisibleItems,
|
||||
) {
|
||||
currentVisibleItems.value = items;
|
||||
return switch (currentViewMode.value) {
|
||||
// Waterfall mode
|
||||
FileListViewMode.waterfall => SliverMasonryGrid(
|
||||
@@ -930,6 +1047,19 @@ class FileListView extends HookConsumerWidget {
|
||||
unindexedFileItem,
|
||||
ref,
|
||||
context,
|
||||
isSelectionMode.value,
|
||||
selectedFileIds.value.contains(unindexedFileItem.file.id),
|
||||
() {
|
||||
if (selectedFileIds.value.contains(
|
||||
unindexedFileItem.file.id,
|
||||
)) {
|
||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||
..remove(unindexedFileItem.file.id);
|
||||
} else {
|
||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||
..add(unindexedFileItem.file.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}, childCount: widgetCount),
|
||||
@@ -953,10 +1083,23 @@ class FileListView extends HookConsumerWidget {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
unindexedFile:
|
||||
(unindexedFileItem) => _buildListUnindexedFileTile(
|
||||
(unindexedFileItem) => _buildUnindexedListTile(
|
||||
unindexedFileItem,
|
||||
ref,
|
||||
context,
|
||||
isSelectionMode.value,
|
||||
selectedFileIds.value.contains(unindexedFileItem.file.id),
|
||||
() {
|
||||
if (selectedFileIds.value.contains(
|
||||
unindexedFileItem.file.id,
|
||||
)) {
|
||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||
..remove(unindexedFileItem.file.id);
|
||||
} else {
|
||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||
..add(unindexedFileItem.file.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -964,10 +1107,149 @@ class FileListView extends HookConsumerWidget {
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildIndexedListTile(
|
||||
FileItem fileItem,
|
||||
WidgetRef ref,
|
||||
BuildContext context,
|
||||
bool isSelectionMode,
|
||||
bool isSelected,
|
||||
VoidCallback toggleSelection,
|
||||
) {
|
||||
final file = fileItem.fileIndex.file;
|
||||
return ListTile(
|
||||
leading: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isSelectionMode)
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) => toggleSelection(),
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: getFileIcon(file, size: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title:
|
||||
file.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(formatFileSize(file.size)),
|
||||
onTap: () {
|
||||
if (isSelectionMode) {
|
||||
toggleSelection();
|
||||
} else {
|
||||
context.push('/files/${fileItem.fileIndex.id}', extra: file);
|
||||
}
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (context.mounted) {
|
||||
showLoadingModal(context);
|
||||
}
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/drive/index/remove/${fileItem.fileIndex.id}');
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteFile'.tr());
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnindexedListTile(
|
||||
UnindexedFileItem unindexedFileItem,
|
||||
WidgetRef ref,
|
||||
BuildContext context,
|
||||
bool isSelectionMode,
|
||||
bool isSelected,
|
||||
VoidCallback toggleSelection,
|
||||
) {
|
||||
final file = unindexedFileItem.file;
|
||||
return ListTile(
|
||||
leading: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isSelectionMode)
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) => toggleSelection(),
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: getFileIcon(file, size: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title:
|
||||
file.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(formatFileSize(file.size)),
|
||||
onTap: () {
|
||||
if (isSelectionMode) {
|
||||
toggleSelection();
|
||||
} else {
|
||||
context.push('/files/${file.id}', extra: file);
|
||||
}
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (context.mounted) {
|
||||
showLoadingModal(context);
|
||||
}
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/drive/files/${file.id}');
|
||||
ref.invalidate(unindexedFileListNotifierProvider);
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteFile'.tr());
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaterfallUnindexedFileTile(
|
||||
UnindexedFileItem unindexedFileItem,
|
||||
WidgetRef ref,
|
||||
BuildContext context,
|
||||
bool isSelectionMode,
|
||||
bool isSelected,
|
||||
VoidCallback? toggleSelection,
|
||||
) {
|
||||
return _buildWaterfallFileTileBase(
|
||||
unindexedFileItem.file,
|
||||
@@ -1001,57 +1283,9 @@ class FileListView extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListUnindexedFileTile(
|
||||
UnindexedFileItem unindexedFileItem,
|
||||
WidgetRef ref,
|
||||
BuildContext context,
|
||||
) {
|
||||
final file = unindexedFileItem.file;
|
||||
return ListTile(
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: getFileIcon(file, size: 24),
|
||||
),
|
||||
),
|
||||
title:
|
||||
file.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(formatFileSize(file.size)),
|
||||
onTap: () {
|
||||
context.push('/files/${file.id}', extra: file);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (context.mounted) {
|
||||
showLoadingModal(context);
|
||||
}
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/drive/files/${file.id}');
|
||||
ref.invalidate(unindexedFileListNotifierProvider);
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteFile'.tr());
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
isSelectionMode,
|
||||
isSelected,
|
||||
toggleSelection,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1147,4 +1381,207 @@ class FileListView extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGlobalFilters(
|
||||
WidgetRef ref,
|
||||
AsyncValue<List<SnFilePool>> poolsAsync,
|
||||
ValueNotifier<SnFilePool?> selectedPool,
|
||||
ValueNotifier<FileListMode> mode,
|
||||
ValueNotifier<String> currentPath,
|
||||
bool isRefreshing,
|
||||
dynamic unindexedNotifier,
|
||||
dynamic cloudNotifier,
|
||||
ValueNotifier<String?> query,
|
||||
ValueNotifier<String?> order,
|
||||
ValueNotifier<bool> orderDesc,
|
||||
ObjectRef<Timer?> queryDebounceTimer,
|
||||
) {
|
||||
final poolDropdownItems = poolsAsync.when(
|
||||
data:
|
||||
(pools) => [
|
||||
const DropdownMenuItem<SnFilePool>(
|
||||
value: null,
|
||||
child: Text('All Pools', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
...pools.map(
|
||||
(p) => DropdownMenuItem<SnFilePool>(
|
||||
value: p,
|
||||
child: Text(p.name, style: const TextStyle(fontSize: 14)),
|
||||
),
|
||||
),
|
||||
],
|
||||
loading: () => const <DropdownMenuItem<SnFilePool>>[],
|
||||
error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[],
|
||||
);
|
||||
|
||||
final poolDropdown = DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnFilePool>(
|
||||
value: selectedPool.value,
|
||||
items: poolDropdownItems,
|
||||
onChanged:
|
||||
isRefreshing
|
||||
? null
|
||||
: (value) {
|
||||
selectedPool.value = value;
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
unindexedNotifier.setPool(value?.id);
|
||||
} else {
|
||||
cloudNotifier.setPool(value?.id);
|
||||
}
|
||||
},
|
||||
customButton: Container(
|
||||
height: 28,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(ref.context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 6,
|
||||
children: [
|
||||
const Icon(Symbols.pool, size: 16),
|
||||
Flexible(
|
||||
child: Text(
|
||||
selectedPool.value?.name ?? 'All files',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).fontSize(12),
|
||||
),
|
||||
],
|
||||
).height(24),
|
||||
),
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.zero,
|
||||
height: 28,
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: const DropdownStyleData(maxHeight: 200),
|
||||
),
|
||||
);
|
||||
|
||||
final queryField = SizedBox(
|
||||
width: 200,
|
||||
height: 28,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fileName'.tr(),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 6,
|
||||
),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
style: const TextStyle(fontSize: 13, height: 1),
|
||||
onChanged: (value) {
|
||||
queryDebounceTimer.value?.cancel();
|
||||
queryDebounceTimer.value = Timer(
|
||||
const Duration(milliseconds: 300),
|
||||
() {
|
||||
query.value = value.isEmpty ? null : value;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final orderDropdown = DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
value: order.value,
|
||||
items:
|
||||
['date', 'size', 'name']
|
||||
.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child:
|
||||
Text(
|
||||
e == 'date' ? e : 'file${e.capitalizeEachWord()}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
).tr(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) => order.value = value,
|
||||
customButton: Container(
|
||||
height: 28,
|
||||
width: 80,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(ref.context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child:
|
||||
Text(
|
||||
(order.value ?? 'date') == 'date'
|
||||
? (order.value ?? 'date')
|
||||
: 'file${order.value?.capitalizeEachWord()}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
height: 28,
|
||||
width: 80,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
dropdownStyleData: const DropdownStyleData(maxHeight: 200),
|
||||
),
|
||||
);
|
||||
|
||||
final orderDescToggle = IconButton(
|
||||
icon: Icon(
|
||||
orderDesc.value ? Symbols.arrow_upward : Symbols.arrow_downward,
|
||||
),
|
||||
onPressed: () {
|
||||
final newValue = !orderDesc.value;
|
||||
orderDesc.value = newValue;
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
unindexedNotifier.setOrderDesc(newValue);
|
||||
} else {
|
||||
cloudNotifier.setOrderDesc(newValue);
|
||||
}
|
||||
},
|
||||
tooltip: orderDesc.value ? 'descendingOrder'.tr() : 'ascendingOrder'.tr(),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
);
|
||||
|
||||
final refreshButton = IconButton(
|
||||
icon: const Icon(Symbols.refresh),
|
||||
onPressed: () {
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
ref.invalidate(unindexedFileListNotifierProvider);
|
||||
} else {
|
||||
cloudNotifier.setPath(currentPath.value);
|
||||
}
|
||||
},
|
||||
tooltip: 'Refresh',
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
poolDropdown,
|
||||
queryField,
|
||||
orderDropdown,
|
||||
orderDescToggle,
|
||||
refreshButton,
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 8),
|
||||
),
|
||||
).padding(horizontal: 12);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,7 +457,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
maxLines: 6,
|
||||
decoration: const InputDecoration(
|
||||
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:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@@ -56,9 +57,7 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
||||
void saveEmbedView() {
|
||||
final uri = uriController.text.trim();
|
||||
if (uri.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('embedUriRequired'.tr())));
|
||||
showSnackBar('embedUriRequired'.tr());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -281,8 +281,8 @@ class ComposeFundSheet extends HookConsumerWidget {
|
||||
// Return the fund that was just created (but not yet paid)
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
Navigator.of(context).pop(fund);
|
||||
}
|
||||
Navigator.of(context).pop(fund);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,10 +327,10 @@ class ComposeFundSheet extends HookConsumerWidget {
|
||||
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(updatedFund);
|
||||
}
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(updatedFund);
|
||||
} else {
|
||||
isPushing.value = false;
|
||||
}
|
||||
|
||||
@@ -751,12 +751,7 @@ class ComposeLogic {
|
||||
|
||||
return post;
|
||||
} catch (err) {
|
||||
// Show error message if context is mounted
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error: $err')));
|
||||
}
|
||||
showErrorAlert(err);
|
||||
rethrow;
|
||||
} finally {
|
||||
state.submitting.value = false;
|
||||
|
||||
@@ -45,6 +45,7 @@ class PostItemScreenshot extends ConsumerWidget {
|
||||
children: [
|
||||
Gap(renderingPadding.vertical),
|
||||
PostHeader(
|
||||
hideOverlay: true,
|
||||
item: item,
|
||||
isFullPost: isFullPost,
|
||||
isInteractive: false,
|
||||
@@ -73,6 +74,7 @@ class PostItemScreenshot extends ConsumerWidget {
|
||||
isFullPost: isFullPost,
|
||||
isTextSelectable: false,
|
||||
isInteractive: false,
|
||||
hideOverlay: true,
|
||||
),
|
||||
if (isShowReference)
|
||||
ReferencedPostWidget(
|
||||
|
||||
@@ -631,12 +631,33 @@ class CustomReactionForm extends HookConsumerWidget {
|
||||
),
|
||||
suffixIcon: InkWell(
|
||||
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(
|
||||
context,
|
||||
Offset(
|
||||
(MediaQuery.sizeOf(context).width - 500) / 2,
|
||||
MediaQuery.sizeOf(context).height - 500,
|
||||
),
|
||||
Offset(horizontalOffset, verticalOffset),
|
||||
alignment: Alignment.topLeft,
|
||||
onPick: (placeholder) {
|
||||
// Remove the surrounding : from the placeholder
|
||||
|
||||
@@ -554,6 +554,7 @@ class PostHeader extends StatelessWidget {
|
||||
final EdgeInsets renderingPadding;
|
||||
final bool isRelativeTime;
|
||||
final bool isCompact;
|
||||
final bool hideOverlay;
|
||||
|
||||
const PostHeader({
|
||||
super.key,
|
||||
@@ -564,6 +565,7 @@ class PostHeader extends StatelessWidget {
|
||||
this.renderingPadding = EdgeInsets.zero,
|
||||
this.isRelativeTime = true,
|
||||
this.isCompact = false,
|
||||
this.hideOverlay = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -606,6 +608,7 @@ class PostHeader extends StatelessWidget {
|
||||
(item.publisher.account != null &&
|
||||
item.publisher.type == 0)
|
||||
? AccountName(
|
||||
hideOverlay: hideOverlay,
|
||||
account: item.publisher.account!,
|
||||
textOverride: item.publisher.nick,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
@@ -618,7 +621,10 @@ class PostHeader extends StatelessWidget {
|
||||
).bold(),
|
||||
),
|
||||
if (item.publisher.verification != null)
|
||||
VerificationMark(mark: item.publisher.verification!),
|
||||
VerificationMark(
|
||||
mark: item.publisher.verification!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (item.realm == null)
|
||||
Flexible(
|
||||
child:
|
||||
@@ -690,6 +696,7 @@ class PostBody extends ConsumerWidget {
|
||||
final bool isInteractive;
|
||||
final EdgeInsets renderingPadding;
|
||||
final bool isRelativeTime;
|
||||
final bool hideOverlay;
|
||||
|
||||
const PostBody({
|
||||
super.key,
|
||||
@@ -700,6 +707,7 @@ class PostBody extends ConsumerWidget {
|
||||
this.isInteractive = true,
|
||||
this.renderingPadding = EdgeInsets.zero,
|
||||
this.isRelativeTime = true,
|
||||
this.hideOverlay = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -771,27 +779,31 @@ class PostBody extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
if (item.editedAt != null) {
|
||||
final text = Text(
|
||||
'editedAt'.tr(
|
||||
args: [
|
||||
!isFullPost && isRelativeTime
|
||||
? item.editedAt!.formatRelative(context)
|
||||
: item.editedAt!.formatSystem(),
|
||||
],
|
||||
),
|
||||
).fontSize(13);
|
||||
|
||||
metadataChildren.add(
|
||||
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(
|
||||
args: [
|
||||
!isFullPost && isRelativeTime
|
||||
? item.editedAt!.formatRelative(context)
|
||||
: item.editedAt!.formatSystem(),
|
||||
],
|
||||
hideOverlay
|
||||
? text
|
||||
: Tooltip(
|
||||
message:
|
||||
!isFullPost && isRelativeTime
|
||||
? item.editedAt!.formatSystem()
|
||||
: item.editedAt!.formatRelative(context),
|
||||
child: text,
|
||||
),
|
||||
).fontSize(13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
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(
|
||||
message: packs[i].name,
|
||||
child: FilterChip(
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: 0,
|
||||
vertical: -4,
|
||||
),
|
||||
label: Text(packs[i].name, overflow: TextOverflow.ellipsis),
|
||||
selected: selected,
|
||||
onSelected: (_) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -30,6 +31,44 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
|
||||
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;
|
||||
@@ -66,6 +105,8 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
position: slideAnimation,
|
||||
child: _UploadOverlayContent(
|
||||
activeTasks: activeTasks,
|
||||
isExpanded: isExpandedLocal.value,
|
||||
onExpansionChanged: (expanded) => isExpandedLocal.value = expanded,
|
||||
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
);
|
||||
@@ -74,12 +115,17 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
|
||||
class _UploadOverlayContent extends HookConsumerWidget {
|
||||
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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isExpanded = useState(false);
|
||||
final animationController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
initialValue: 0.0,
|
||||
@@ -94,15 +140,17 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (isExpanded.value) {
|
||||
if (isExpanded) {
|
||||
animationController.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
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(
|
||||
padding: EdgeInsets.only(
|
||||
@@ -111,7 +159,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
right: isMobile ? 16 : 24,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => isExpanded.value = !isExpanded.value,
|
||||
onTap: () => onExpansionChanged?.call(!isExpanded),
|
||||
child: AnimatedBuilder(
|
||||
animation: animationController,
|
||||
builder: (context, child) {
|
||||
@@ -145,8 +193,8 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
key: ValueKey(isExpanded.value),
|
||||
isExpanded.value
|
||||
key: ValueKey(isExpanded),
|
||||
isExpanded
|
||||
? Symbols.list_rounded
|
||||
: _getOverallStatusIcon(activeTasks),
|
||||
size: 24,
|
||||
@@ -162,7 +210,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isExpanded.value
|
||||
isExpanded
|
||||
? 'uploadTasks'.tr()
|
||||
: _getOverallStatusText(activeTasks),
|
||||
style: Theme.of(context)
|
||||
@@ -172,8 +220,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (!isExpanded.value &&
|
||||
activeTasks.isNotEmpty)
|
||||
if (!isExpanded && activeTasks.isNotEmpty)
|
||||
Text(
|
||||
_getOverallProgressText(activeTasks),
|
||||
style: Theme.of(
|
||||
@@ -190,7 +237,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
),
|
||||
|
||||
// Progress indicator (collapsed)
|
||||
if (!isExpanded.value)
|
||||
if (!isExpanded)
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -210,14 +257,14 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
turns: opacityAnimation * 0.5,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Icon(
|
||||
isExpanded.value
|
||||
isExpanded
|
||||
? Symbols.expand_more
|
||||
: Symbols.chevron_right,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
() => isExpanded.value = !isExpanded.value,
|
||||
() => onExpansionChanged?.call(!isExpanded),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
@@ -226,7 +273,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
),
|
||||
|
||||
// Expanded content
|
||||
if (isExpanded.value)
|
||||
if (isExpanded)
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -246,7 +293,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Clear Completed'),
|
||||
title: const Text('clearCompleted').tr(),
|
||||
leading: Icon(
|
||||
Symbols.clear_all,
|
||||
size: 18,
|
||||
@@ -256,10 +303,35 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.clearCompletedTasks();
|
||||
isExpanded.value = false;
|
||||
taskNotifier.clearCompletedTasks();
|
||||
onExpansionChanged?.call(false);
|
||||
},
|
||||
|
||||
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:
|
||||
Theme.of(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/wallet.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
@@ -56,7 +57,7 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Failed to load fund envelope',
|
||||
'fundEnvelopeLoadFailed'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
@@ -87,7 +88,7 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Fund Envelope',
|
||||
'fundEnvelope'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
@@ -115,7 +116,12 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
||||
const SizedBox(height: 4),
|
||||
if (fund.remainingAmount != fund.totalAmount)
|
||||
Text(
|
||||
'Remaining: ${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency}',
|
||||
'fundEnvelopeRemaining'.tr(
|
||||
args: [
|
||||
fund.remainingAmount.toStringAsFixed(2),
|
||||
fund.currency,
|
||||
],
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
@@ -125,7 +131,13 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Split: ${fund.splitType == 0 ? 'Evenly' : 'Randomly'}',
|
||||
'fundEnvelopeSplit'.tr(
|
||||
args: [
|
||||
fund.splitType == 0
|
||||
? 'fundEnvelopeSplitEvenly'.tr()
|
||||
: 'fundEnvelopeSplitRandomly'.tr(),
|
||||
],
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
@@ -244,20 +256,10 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
||||
|
||||
if (dialogContext.mounted) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(content: Text('Fund claimed successfully!'.tr())),
|
||||
);
|
||||
showSnackBar('fundEnvelopeClaimSuccess'.tr());
|
||||
}
|
||||
} catch (e) {
|
||||
if (dialogContext.mounted) {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to claim fund: $e'),
|
||||
backgroundColor:
|
||||
Theme.of(dialogContext).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
showErrorAlert(e);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -270,23 +272,23 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
||||
|
||||
switch (status) {
|
||||
case 0:
|
||||
text = 'Created';
|
||||
text = 'fundEnvelopeStatusCreated'.tr();
|
||||
color = Colors.blue;
|
||||
break;
|
||||
case 1:
|
||||
text = 'Partially Claimed';
|
||||
text = 'fundEnvelopeStatusPartial'.tr();
|
||||
color = Colors.orange;
|
||||
break;
|
||||
case 2:
|
||||
text = 'Fully Claimed';
|
||||
text = 'fundEnvelopeStatusCompleted'.tr();
|
||||
color = Colors.green;
|
||||
break;
|
||||
case 3:
|
||||
text = 'Expired';
|
||||
text = 'fundEnvelopeStatusExpired'.tr();
|
||||
color = Colors.red;
|
||||
break;
|
||||
default:
|
||||
text = 'Unknown';
|
||||
text = 'fundEnvelopeStatusUnknown'.tr();
|
||||
color = Colors.grey;
|
||||
}
|
||||
|
||||
@@ -348,7 +350,9 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Recipients ($claimedCount/$totalCount claimed)',
|
||||
'fundEnvelopeRecipients'.tr(
|
||||
args: [claimedCount.toString(), totalCount.toString()],
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -371,15 +375,26 @@ class FundEnvelopeWidget extends HookConsumerWidget {
|
||||
final difference = date.difference(now);
|
||||
|
||||
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) {
|
||||
final hours = difference.inHours;
|
||||
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) {
|
||||
return 'Expires in ${difference.inDays} day${difference.inDays == 1 ? '' : 's'}';
|
||||
final days = difference.inDays;
|
||||
return 'fundEnvelopeExpiresInDays'.plural(
|
||||
days,
|
||||
args: [days.toString()],
|
||||
);
|
||||
} else {
|
||||
return '${date.day}/${date.month}/${date.year}';
|
||||
}
|
||||
@@ -458,7 +473,13 @@ class FundClaimDialog extends HookConsumerWidget {
|
||||
|
||||
// Remaining amount
|
||||
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(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -512,7 +533,8 @@ class FundClaimDialog extends HookConsumerWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
recipient.recipientAccount?.nick ?? 'Unknown User',
|
||||
recipient.recipientAccount?.nick ??
|
||||
'fundEnvelopeUnknownUser'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
@@ -560,7 +582,8 @@ class FundClaimDialog extends HookConsumerWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
recipient.recipientAccount?.nick ?? 'Unknown User',
|
||||
recipient.recipientAccount?.nick ??
|
||||
'fundEnvelopeUnknownUser'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <file_saver/file_saver_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_timezone/flutter_timezone_plugin.h>
|
||||
#include <flutter_udid/flutter_udid_plugin.h>
|
||||
@@ -26,7 +25,6 @@
|
||||
#include <syncfusion_pdfviewer_linux/syncfusion_pdfviewer_linux_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <volume_controller/volume_controller_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@@ -39,9 +37,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||
|
||||
@@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_drop
|
||||
file_saver
|
||||
file_selector_linux
|
||||
flutter_platform_alert
|
||||
flutter_secure_storage_linux
|
||||
flutter_timezone
|
||||
flutter_udid
|
||||
@@ -23,7 +22,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
syncfusion_pdfviewer_linux
|
||||
tray_manager
|
||||
url_launcher_linux
|
||||
volume_controller
|
||||
window_manager
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import firebase_crashlytics
|
||||
import firebase_messaging
|
||||
import flutter_inappwebview_macos
|
||||
import flutter_local_notifications
|
||||
import flutter_platform_alert
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_timezone
|
||||
import flutter_udid
|
||||
@@ -30,7 +29,6 @@ import media_kit_libs_macos_video
|
||||
import media_kit_video
|
||||
import package_info_plus
|
||||
import pasteboard
|
||||
import path_provider_foundation
|
||||
import protocol_handler_macos
|
||||
import record_macos
|
||||
import screen_retriever_macos
|
||||
@@ -43,7 +41,6 @@ import super_native_extensions
|
||||
import syncfusion_pdfviewer_macos
|
||||
import tray_manager
|
||||
import url_launcher_macos
|
||||
import volume_controller
|
||||
import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
@@ -60,7 +57,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||
@@ -73,7 +69,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
@@ -86,7 +81,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SyncfusionFlutterPdfViewerPlugin.register(with: registry.registrar(forPlugin: "SyncfusionFlutterPdfViewerPlugin"))
|
||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ PODS:
|
||||
- firebase_core (4.2.1):
|
||||
- Firebase/CoreOnly (~> 12.4.0)
|
||||
- FlutterMacOS
|
||||
- firebase_crashlytics (5.0.4):
|
||||
- firebase_crashlytics (5.0.5):
|
||||
- Firebase/CoreOnly (~> 12.4.0)
|
||||
- Firebase/Crashlytics (~> 12.4.0)
|
||||
- firebase_core
|
||||
@@ -102,15 +102,13 @@ PODS:
|
||||
- OrderedSet (~> 6.0.3)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_platform_alert (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_secure_storage_macos (6.1.3):
|
||||
- FlutterMacOS
|
||||
- flutter_timezone (0.1.0):
|
||||
- FlutterMacOS
|
||||
- flutter_udid (0.0.1):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain
|
||||
- KeychainAccess
|
||||
- flutter_webrtc (1.2.0):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 137.7151.04)
|
||||
@@ -172,6 +170,7 @@ PODS:
|
||||
- GoogleUtilities/Privacy
|
||||
- irondash_engine_context (0.0.1):
|
||||
- FlutterMacOS
|
||||
- KeychainAccess (4.2.2)
|
||||
- livekit_client (2.5.3):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
@@ -188,14 +187,13 @@ PODS:
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- objective_c (0.0.1):
|
||||
- FlutterMacOS
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- pasteboard (0.0.1):
|
||||
- FlutterMacOS
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- PromisesSwift (2.4.0):
|
||||
- PromisesObjC (= 2.4.0)
|
||||
@@ -203,7 +201,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- record_macos (1.1.0):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_retriever_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- share_plus (0.0.1):
|
||||
@@ -249,8 +246,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- volume_controller (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (137.7151.04)
|
||||
@@ -271,7 +266,6 @@ DEPENDENCIES:
|
||||
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/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_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_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/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`)
|
||||
- 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`)
|
||||
- objective_c (from `Flutter/ephemeral/.symlinks/plugins/objective_c/macos`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/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`)
|
||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_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`)
|
||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/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`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
@@ -317,11 +310,11 @@ SPEC REPOS:
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- KeychainAccess
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- SAMKeychain
|
||||
- sqlite3
|
||||
- WebRTC-SDK
|
||||
|
||||
@@ -352,8 +345,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
||||
flutter_local_notifications:
|
||||
: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:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||
flutter_timezone:
|
||||
@@ -376,12 +367,12 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
|
||||
media_kit_video:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
|
||||
objective_c:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/objective_c/macos
|
||||
package_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||
pasteboard:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
||||
path_provider_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
protocol_handler_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
|
||||
record_macos:
|
||||
@@ -406,8 +397,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
volume_controller:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
window_manager:
|
||||
@@ -424,7 +413,7 @@ SPEC CHECKSUMS:
|
||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||
firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e
|
||||
firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc
|
||||
firebase_crashlytics: c2438b5f5bdcacf59d0eaee5852c6b0ab09dab77
|
||||
firebase_crashlytics: add6934cf4413d6c1307ca5251dbdbfe580e4caf
|
||||
firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168
|
||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||
@@ -437,10 +426,9 @@ SPEC CHECKSUMS:
|
||||
FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
|
||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
|
||||
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
|
||||
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
|
||||
flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
|
||||
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
||||
flutter_udid: 00c09e022fd527fd39fef97670b220f2ae8190e7
|
||||
flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
@@ -448,20 +436,20 @@ SPEC CHECKSUMS:
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
|
||||
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||
livekit_client: a6d5ae8aaeebf3e52235da866fea00f43156c72b
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
|
||||
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
objective_c: ec13431e45ba099cb734eb2829a5c1cd37986cba
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_macos: f9cd7b13bcaf6b0425f7410cbe52376cb843a936
|
||||
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
@@ -473,7 +461,6 @@ SPEC CHECKSUMS:
|
||||
syncfusion_pdfviewer_macos: b3b110c68039178ca4105dd03ef38761eca3b36b
|
||||
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
|
||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
|
||||
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
|
||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
||||
|
||||
258
pubspec.lock
258
pubspec.lock
@@ -73,6 +73,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
autotrie:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: autotrie
|
||||
sha256: "55da6faefb53cfcb0abb2f2ca8636123fb40e35286bb57440d2cf467568188f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
avatar_stack:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -149,10 +157,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
|
||||
sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.12.0"
|
||||
version: "8.12.1"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -301,10 +309,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239"
|
||||
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
version: "0.3.5+1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -565,10 +573,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967
|
||||
sha256: "7872545770c277236fd32b022767576c562ba28366204ff1a5628853cf8f2200"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.6"
|
||||
version: "10.3.7"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -581,18 +589,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+2"
|
||||
version: "0.9.4"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "88707a3bec4b988aaed3b4df5d7441ee4e987f20b286cddca5d6a8270cab23f2"
|
||||
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+5"
|
||||
version: "0.9.5"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -605,10 +613,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
||||
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+4"
|
||||
version: "0.9.3+5"
|
||||
firebase_analytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -661,10 +669,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: c3ebe3ed9f3b1d36c0864a4a28b041fcc2686f11fb2a4f7891319ea8d1d161cc
|
||||
sha256: bd4eadd620071ae48ef0e3617499be2a0ea4062d6d5c6fea6c1cec41ebbb2435
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.4"
|
||||
version: "5.0.5"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -766,6 +774,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1003,22 +1019,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687"
|
||||
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.32"
|
||||
version: "2.0.33"
|
||||
flutter_popup_card:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1103,10 +1111,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355"
|
||||
sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.3"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -1132,10 +1140,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_udid
|
||||
sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030"
|
||||
sha256: "31193dbfef74f697e9e5317e59f3c2ae6dc45ce4b9f5d39308a32446e8303acc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.1.0"
|
||||
flutter_web_plugins:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -1253,6 +1261,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
hive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hive
|
||||
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
hooks_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1302,7 +1318,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
@@ -1329,26 +1345,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6
|
||||
sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+7"
|
||||
version: "0.8.13+10"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
||||
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.1"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58
|
||||
sha256: "997d100ce1dda5b1ba4085194c5e36c9f8a1fb7987f6a36ab677a344cd2dc986"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+1"
|
||||
version: "0.8.13+2"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1461,6 +1477,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1497,18 +1521,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: d836715ed95b16b2de3a8c47a88ba5e607976bb1e27c9446d193152ea1429fae
|
||||
sha256: "04dd9050b59cb4bcaf051d44eec65865779a9b2f6daccc523f59f96b565a5d54"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.0.3"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: "15d9db4ad4d58a11d7269e55d46ff8d49ed5e856226c8a5a91280f0d7c37b3a6"
|
||||
sha256: "668ea65edaab17380956e9713f57e34f78ede505ca0cfd8d39db34e2f260bfee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.0.1"
|
||||
local_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1521,10 +1545,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_windows
|
||||
sha256: d95535a73eddf57ce5930d5e78a0fa4f294c31981fdeeee83325b797302be454
|
||||
sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.0.1"
|
||||
logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1649,10 +1673,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_video
|
||||
sha256: "813858c3fe84eb46679eb698695f60665e2bfbef757766fac4d2e683f926e15a"
|
||||
sha256: eac9b5f27310afe6def49f9b785cef155f99f1db6079d2b2b127b8bddafb6472
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "2.0.0"
|
||||
menu_base:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1685,6 +1709,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1725,6 +1757,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1793,18 +1833,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16
|
||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.20"
|
||||
version: "2.2.22"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
|
||||
sha256: "6192e477f34018ef1ea790c56fffc7302e3bc3efede9e798b934c252c8c105ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.5.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2066,18 +2106,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_android
|
||||
sha256: fb54ee4e28f6829b8c580252a9ef49d9c549cfd263b0660ad7eeac0908658e9f
|
||||
sha256: "9aaf3f151e61399b09bd7c31eb5f78253d2962b3f57af019ac5a2d1a3afdcf71"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.4"
|
||||
version: "1.4.5"
|
||||
record_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_ios
|
||||
sha256: "765b42ac1be019b1674ddd809b811fc721fe5a93f7bb1da7803f0d16772fd6d7"
|
||||
sha256: "69fcd37c6185834e90254573599a9165db18a2cbfa266b6d1e46ffffeb06a28c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
version: "1.1.5"
|
||||
record_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2106,10 +2146,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_web
|
||||
sha256: "20ac10d56514cb9f8cecc8f3579383084fdfb43b0d04e05a95244d0d76091d90"
|
||||
sha256: "3feeffbc0913af3021da9810bb8702a068db6bc9da52dde1d19b6ee7cb9edb51"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
record_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2190,22 +2230,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2262,6 +2286,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2298,18 +2330,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713"
|
||||
sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.15"
|
||||
version: "2.4.17"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b"
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2583,18 +2615,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: "8118f13264d1401a7085d12a0aaeac1ebd5cd939046b8c565d195879646daad6"
|
||||
sha256: fea2b5f6c976455d20b19bf77d29bf96a740d14579127b4fc1cdafde42b48177
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_flutter_pdf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_pdf
|
||||
sha256: "34d8b658e9fa7b18c4c16b4a775dc3f634933c4367e5f1ef4854f80cdd22c3ff"
|
||||
sha256: d7852ea65da3e5b64a06b38959cf0de1fb1eaafbfce7ac6950d4a3d59d3cfd57
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_flutter_pdfviewer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2607,50 +2639,50 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_signaturepad
|
||||
sha256: ef891418bee7c79470ff1b6290f7745df8c1b2adf4df6b81ab9cd69ef900c4e8
|
||||
sha256: f74fca0463e0977b7fca6f37fc7dad3cab4b99648594dbe8b99f55f2f5c37ab0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_linux
|
||||
sha256: ec4efb4cdd34462f40b7dafcb5094780a15c988691f28bdec141ea2a01f145bb
|
||||
sha256: "1ffb2e3656694342e29216d7df19961b7fde40d424024efda0610fe5661f1dc3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_macos
|
||||
sha256: "15d4e7d5a5a705b7861bdf7e5758d371973a03fda33a21068dc934569b8fc363"
|
||||
sha256: "166225a4db5c182cd6e18bba69685e15cfe7bd10232c1d5663842636de605437"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_platform_interface
|
||||
sha256: d2b9d4631693503340d5eaef6f42446d6d74f290dca9764e65f5b55b0b4043cf
|
||||
sha256: d630694835967ca78151b22ac88149325e933f1a5de29bf4e845753cc5123585
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_web
|
||||
sha256: "073384338eb9a6370e3ec7b9fbad973b6d0312c027698392c09409e156644807"
|
||||
sha256: "395d3c6f2afb816f9f883b7fe7281a16d3bc38a8b3799e54a1c8399ff91fc059"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_windows
|
||||
sha256: de6254b5b939c17b32498d895aaf272748035fd20a9790b4ee7e8afe915ef233
|
||||
sha256: ac4a5fdf5eae92163933669d5ec80a3553b84421828d6c7beb167519084a42e4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2671,34 +2703,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker
|
||||
sha256: "82de443cadfb6c41d457e7774c7890a91c73af3c2f17f3f7c01670bb58d5f5a1"
|
||||
sha256: f17bde91ef86a51803974804bc6c0e161d14b496522bbc646c531be2c702e390
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
version: "5.1.0"
|
||||
talker_dio_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_dio_logger
|
||||
sha256: "5bbecc237f3d2c4af9348da5a0086321ed6dd6bf9857d723b1f54f61c810cff2"
|
||||
sha256: "322e8fee094d5fdce10f0439fec5625fb73fd282622b37c794bdaab4e1fc2b1b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
version: "5.1.0"
|
||||
talker_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_flutter
|
||||
sha256: "4f7a8d739237a3a3c8ba4dddcdbc1f9d9dec143811641dbafebd6b70f947f8ca"
|
||||
sha256: "5869c80c046a8e0478560fa2ebb40a3f0e025a9948bcdea50c12343cba681820"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
version: "5.1.0"
|
||||
talker_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_logger
|
||||
sha256: "8218836d871ea5ab1ec616cffe3cdae84e8fb44022d5cc04c95d7b220572b8fb"
|
||||
sha256: "87c66fefded42446d8c6365643582f3d180c0ce09485c129e9a88fa14d0d34eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
version: "5.1.0"
|
||||
talker_riverpod_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2784,10 +2816,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
||||
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.3.1"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2816,34 +2848,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9"
|
||||
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.24"
|
||||
version: "6.3.28"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9"
|
||||
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.5"
|
||||
version: "6.3.6"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.2"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9"
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.4"
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2864,10 +2896,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2932,14 +2964,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
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
|
||||
# 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.
|
||||
version: 3.3.0+146
|
||||
version: 3.3.0+147
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
@@ -57,30 +57,30 @@ dependencies:
|
||||
cached_network_image: ^3.4.1
|
||||
web: ^1.1.1
|
||||
flutter_blurhash: ^0.9.1
|
||||
media_kit: ^1.2.1
|
||||
media_kit_video: ^1.3.1
|
||||
media_kit: ^1.2.2
|
||||
media_kit_video: ^2.0.0
|
||||
media_kit_libs_video: ^1.0.7
|
||||
flutter_cache_manager: ^3.4.1
|
||||
flutter_platform_alert: ^0.8.0
|
||||
|
||||
email_validator: ^3.0.0
|
||||
easy_localization: ^3.0.8
|
||||
flutter_inappwebview: ^6.1.5
|
||||
animations: ^2.1.0
|
||||
animations: ^2.1.1
|
||||
package_info_plus: ^9.0.0
|
||||
device_info_plus: ^11.3.0
|
||||
protocol_handler: ^0.2.0
|
||||
tus_client_dart:
|
||||
git: https://github.com/LittleSheep2Code/tus_client.git
|
||||
cross_file: ^0.3.5
|
||||
cross_file: ^0.3.5+1
|
||||
image_picker: ^1.2.1
|
||||
file_picker: ^10.3.6
|
||||
file_picker: ^10.3.7
|
||||
riverpod_annotation: ^2.6.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
|
||||
modal_bottom_sheet: ^3.0.0
|
||||
firebase_messaging: ^16.0.4
|
||||
flutter_udid: ^4.0.0
|
||||
flutter_udid: ^4.1.0
|
||||
firebase_core: ^4.2.1
|
||||
web_socket_channel: ^3.0.3
|
||||
material_symbols_icons: ^4.2874.0
|
||||
@@ -117,7 +117,7 @@ dependencies:
|
||||
flutter_timezone: ^5.0.1
|
||||
fl_chart: ^1.1.1
|
||||
sign_in_with_apple: ^7.0.1
|
||||
flutter_svg: ^2.2.2
|
||||
flutter_svg: ^2.2.3
|
||||
native_exif: ^0.6.2
|
||||
local_auth: ^3.0.0
|
||||
flutter_secure_storage: ^9.2.4
|
||||
@@ -136,7 +136,7 @@ dependencies:
|
||||
flutter_app_update: ^3.2.2
|
||||
archive: ^4.0.7
|
||||
process_run: ^1.2.4
|
||||
firebase_crashlytics: ^5.0.4
|
||||
firebase_crashlytics: ^5.0.5
|
||||
firebase_analytics: ^12.0.4
|
||||
material_color_utilities: ^0.11.1
|
||||
screenshot: ^3.0.0
|
||||
@@ -155,10 +155,10 @@ dependencies:
|
||||
dart_ipc: ^1.0.1
|
||||
pretty_diff_text: ^2.1.0
|
||||
window_manager: ^0.5.1
|
||||
talker: ^5.0.2
|
||||
talker_flutter: ^5.0.2
|
||||
talker_logger: ^5.0.2
|
||||
talker_dio_logger: ^5.0.2
|
||||
talker: ^5.1.0
|
||||
talker_flutter: ^5.1.0
|
||||
talker_logger: ^5.1.0
|
||||
talker_dio_logger: ^5.1.0
|
||||
talker_riverpod_logger: ^5.0.1
|
||||
syncfusion_flutter_pdfviewer: ^31.1.21
|
||||
swipe_to: ^1.0.6
|
||||
@@ -168,7 +168,9 @@ dependencies:
|
||||
event_bus: ^2.0.1
|
||||
convert: ^3.1.2
|
||||
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:
|
||||
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];
|
||||
}
|
||||
1316
test/drift/app_database/generated/schema_v6.dart
Normal file
1316
test/drift/app_database/generated/schema_v6.dart
Normal file
File diff suppressed because it is too large
Load Diff
2672
test/drift/app_database/generated/schema_v7.dart
Normal file
2672
test/drift/app_database/generated/schema_v7.dart
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user