Compare commits
	
		
			32 Commits
		
	
	
		
			3.1.0+115
			...
			a0d8c1a9b3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a0d8c1a9b3 | |||
| 26135d2116 | |||
| 71b67fd22d | |||
| 855072dfea | |||
| b39e2e2d64 | |||
| 84b1d6a346 | |||
| 28335dd548 | |||
| 7253e2d3ef | |||
| 4d489425fa | |||
| 890a8a44cf | |||
| 8e3583f57a | |||
| d0ff14659f | |||
| 1f7caaeaac | |||
| 9f9f42071a | |||
| 6bd6e994cb | |||
| 02e68d76ee | |||
| d04b06089c | |||
| 9be6fea2e0 | |||
| 6b1214a06f | |||
| 4597373ac9 | |||
| 047c8d93aa | |||
| 715f95ca22 | |||
| ba709012d7 | |||
| fd186f8391 | |||
| 262d36cd2d | |||
| f320855348 | |||
| ed90152462 | |||
| 6e5c5f1690 | |||
| 7c92dee097 | |||
| e4bb031138 | |||
| 97226ae96b | |||
| d8cd33e79a | 
| @@ -89,6 +89,13 @@ | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <!-- Livekit Screenshare --> | ||||
|         <service | ||||
|             android:name="de.julianassmann.flutter_background.IsolateHolderService" | ||||
|             android:enabled="true" | ||||
|             android:exported="false" | ||||
|             android:foregroundServiceType="mediaProjection" /> | ||||
|  | ||||
|         <!-- Sign in with Apple --> | ||||
|         <activity | ||||
|             android:name="com.aboutyou.dart_packages.sign_in_with_apple.SignInWithAppleCallback" | ||||
|   | ||||
| @@ -147,6 +147,9 @@ | ||||
|   "addVideo": "Add video", | ||||
|   "addPhoto": "Add photo", | ||||
|   "addFile": "Add file", | ||||
|   "linkAttachment": "Link Attachment", | ||||
|   "fileIdCannotBeEmpty": "File ID cannot be empty", | ||||
|   "failedToFetchFile": "Failed to fetch file: {}", | ||||
|   "createDirectMessage": "Send new DM", | ||||
|   "gotoDirectMessage": "Go to DM", | ||||
|   "react": "React", | ||||
| @@ -352,6 +355,8 @@ | ||||
|   "postTitle": "Title", | ||||
|   "postDescription": "Description", | ||||
|   "call": "Call", | ||||
|   "callLeave": "Leave", | ||||
|   "callEnd": "End this call", | ||||
|   "done": "Done", | ||||
|   "loginResetPasswordSent": "Password reset link sent, please check your email inbox.", | ||||
|   "accountDeletion": "Delete Account", | ||||
| @@ -622,8 +627,8 @@ | ||||
|   "chatJoin": "Join the Chat", | ||||
|   "realmJoin": "Join the Realm", | ||||
|   "realmJoinSuccess": "Successfully joined the realm.", | ||||
|   "discoverRealms": "Discover Realms", | ||||
|   "discoverPublishers": "Discover Publishers", | ||||
|   "discoverRealms": "Discover realms", | ||||
|   "discoverPublishers": "Discover publishers", | ||||
|   "search": "Search", | ||||
|   "publisherMembers": "Collaborators", | ||||
|   "developerHub": "Developer Hub", | ||||
| @@ -702,5 +707,29 @@ | ||||
|   "aboutDeviceName": "Device Name", | ||||
|   "aboutDeviceIdentifier": "Device Identifier", | ||||
|   "donate": "Donate", | ||||
|   "donateDescription": "Support us to continue developing the Solar Network and keep the server up and running." | ||||
| } | ||||
|   "donateDescription": "Support us to continue developing the Solar Network and keep the server up and running.", | ||||
|   "fileId": "File ID", | ||||
|   "fileIdHint": "The file ID is the ID you get after upload the file via the Solar Network Drive.", | ||||
|   "translate": "Translate", | ||||
|   "translating": "Translating", | ||||
|   "translated": "Translated", | ||||
|   "reactionThumbUp": "Thumbs Up", | ||||
|   "reactionThumbDown": "Thumbs Down", | ||||
|   "reactionJustOkay": "Just Okay", | ||||
|   "reactionCry": "Cry", | ||||
|   "reactionConfuse": "Confused", | ||||
|   "reactionClap": "Clap", | ||||
|   "reactionLaugh": "Laugh", | ||||
|   "reactionAngry": "Angry", | ||||
|   "reactionParty": "Party", | ||||
|   "reactionPray": "Pray", | ||||
|   "reactionHeart": "Heart", | ||||
|   "selectMicrophone": "Select Microphone", | ||||
|   "selectCamera": "Select Camera", | ||||
|   "switchedTo": "Switched to {}", | ||||
|   "connecting": "Connecting", | ||||
|   "reconnecting": "Reconnecting", | ||||
|   "disconnected": "Disconnected", | ||||
|   "connected": "Connected", | ||||
|   "repliesLoadMore": "Load more replies" | ||||
| } | ||||
|   | ||||
| @@ -123,6 +123,10 @@ | ||||
|   "addVideo": "添加视频", | ||||
|   "addPhoto": "添加照片", | ||||
|   "addFile": "添加文件", | ||||
|   "addAttachmentById": "通过 ID 添加附件", | ||||
|   "enterFileId": "输入文件 ID", | ||||
|   "fileIdCannotBeEmpty": "文件 ID 不能为空", | ||||
|   "failedToFetchFile": "获取文件失败: {}", | ||||
|   "createDirectMessage": "创建新私人消息", | ||||
|   "gotoDirectMessage": "前往私信", | ||||
|   "react": "反应", | ||||
|   | ||||
| @@ -123,6 +123,10 @@ | ||||
|     "addVideo": "新增影片", | ||||
|     "addPhoto": "新增照片", | ||||
|     "addFile": "新增檔案", | ||||
|     "addAttachmentById": "透過 ID 新增附件", | ||||
|     "enterFileId": "輸入檔案 ID", | ||||
|     "fileIdCannotBeEmpty": "檔案 ID 不能為空", | ||||
|     "failedToFetchFile": "無法取得檔案: {}", | ||||
|     "createDirectMessage": "建立新私人訊息", | ||||
|     "gotoDirectMessage": "Go to DM", | ||||
|     "react": "反應", | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| # Uncomment this line to define a global platform for your project | ||||
| platform :ios, '13.0' | ||||
| platform :ios, '15.0' | ||||
|  | ||||
| # CocoaPods analytics sends network stats synchronously affecting flutter build latency. | ||||
| ENV['COCOAPODS_DISABLE_STATS'] = 'true' | ||||
|   | ||||
| @@ -40,33 +40,33 @@ PODS: | ||||
|   - file_picker (0.0.1): | ||||
|     - DKImagePickerController/PhotoGallery | ||||
|     - Flutter | ||||
|   - Firebase/CoreOnly (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|   - Firebase/Messaging (11.15.0): | ||||
|   - Firebase/CoreOnly (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|   - Firebase/Messaging (12.0.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.15.0) | ||||
|   - firebase_core (3.15.2): | ||||
|     - Firebase/CoreOnly (= 11.15.0) | ||||
|     - FirebaseMessaging (~> 12.0.0) | ||||
|   - firebase_core (4.0.0): | ||||
|     - Firebase/CoreOnly (= 12.0.0) | ||||
|     - Flutter | ||||
|   - firebase_messaging (15.2.10): | ||||
|     - Firebase/Messaging (= 11.15.0) | ||||
|   - firebase_messaging (16.0.0): | ||||
|     - Firebase/Messaging (= 12.0.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - FirebaseCore (11.15.0): | ||||
|     - FirebaseCoreInternal (~> 11.15.0) | ||||
|   - FirebaseCore (12.0.0): | ||||
|     - FirebaseCoreInternal (~> 12.0.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Logger (~> 8.1) | ||||
|   - FirebaseCoreInternal (11.15.0): | ||||
|   - FirebaseCoreInternal (12.0.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|   - FirebaseInstallations (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|   - FirebaseInstallations (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleDataTransport (~> 10.0) | ||||
|   - FirebaseMessaging (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|     - FirebaseInstallations (~> 12.0.0) | ||||
|     - GoogleDataTransport (~> 10.1) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Reachability (~> 8.1) | ||||
| @@ -93,9 +93,9 @@ PODS: | ||||
|   - flutter_udid (0.0.1): | ||||
|     - Flutter | ||||
|     - SAMKeychain | ||||
|   - flutter_webrtc (0.14.0): | ||||
|   - flutter_webrtc (1.0.0): | ||||
|     - Flutter | ||||
|     - WebRTC-SDK (= 125.6422.07) | ||||
|     - WebRTC-SDK (= 137.7151.02) | ||||
|   - gal (1.0.0): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
| @@ -131,10 +131,10 @@ PODS: | ||||
|   - irondash_engine_context (0.0.1): | ||||
|     - Flutter | ||||
|   - Kingfisher (8.5.0) | ||||
|   - livekit_client (2.4.9): | ||||
|   - livekit_client (2.5.0): | ||||
|     - Flutter | ||||
|     - flutter_webrtc | ||||
|     - WebRTC-SDK (= 125.6422.07) | ||||
|     - WebRTC-SDK (= 137.7151.02) | ||||
|   - local_auth_darwin (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
| @@ -191,6 +191,8 @@ PODS: | ||||
|     - sqlite3/common | ||||
|   - sqlite3/rtree (3.50.3): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/session (3.50.3): | ||||
|     - sqlite3/common | ||||
|   - sqlite3_flutter_libs (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
| @@ -200,6 +202,7 @@ PODS: | ||||
|     - sqlite3/math | ||||
|     - sqlite3/perf-threadsafe | ||||
|     - sqlite3/rtree | ||||
|     - sqlite3/session | ||||
|   - super_native_extensions (0.0.1): | ||||
|     - Flutter | ||||
|   - SwiftyGif (5.4.5) | ||||
| @@ -209,7 +212,7 @@ PODS: | ||||
|     - Flutter | ||||
|   - wakelock_plus (0.0.1): | ||||
|     - Flutter | ||||
|   - WebRTC-SDK (125.6422.07) | ||||
|   - WebRTC-SDK (137.7151.02) | ||||
|  | ||||
| DEPENDENCIES: | ||||
|   - Alamofire | ||||
| @@ -361,13 +364,13 @@ SPEC CHECKSUMS: | ||||
|   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be | ||||
|   Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e | ||||
|   firebase_core: 995454a784ff288be5689b796deb9e9fa3601818 | ||||
|   firebase_messaging: f4a41dd102ac18b840eba3f39d67e77922d3f707 | ||||
|   FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e | ||||
|   FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 | ||||
|   FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 | ||||
|   FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09 | ||||
|   Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 | ||||
|   firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4 | ||||
|   firebase_messaging: d17feef781edc84ebefe62624fb384358ad96361 | ||||
|   FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a | ||||
|   FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 | ||||
|   FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 | ||||
|   FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 | ||||
|   flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 | ||||
| @@ -376,14 +379,14 @@ SPEC CHECKSUMS: | ||||
|   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 | ||||
|   flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 | ||||
|   flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 | ||||
|   flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117 | ||||
|   flutter_webrtc: 6f7da106613d52ade777d5b4875a43f48c28b457 | ||||
|   gal: baecd024ebfd13c441269ca7404792a7152fde89 | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||
|   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | ||||
|   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 | ||||
|   Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c | ||||
|   livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e | ||||
|   livekit_client: e3b79b99405428aac439b6b76a254cd9a11dbbfb | ||||
|   local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19 | ||||
|   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 | ||||
|   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 | ||||
| @@ -404,14 +407,14 @@ SPEC CHECKSUMS: | ||||
|   sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 | ||||
|   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 | ||||
|   sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 | ||||
|   sqlite3_flutter_libs: ce0522d143cee6ef5e16587acfce8f476316e005 | ||||
|   sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e | ||||
|   super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 | ||||
|   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 | ||||
|   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d | ||||
|   volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 | ||||
|   wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 | ||||
|   WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e | ||||
|   WebRTC-SDK: d20de357dcbf7c9696b124b39f3ff62125107e4b | ||||
|  | ||||
| PODFILE CHECKSUM: f6df17c2a0cbd7af89692fd3877231eaea40230f | ||||
| PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f | ||||
|  | ||||
| COCOAPODS: 1.16.2 | ||||
|   | ||||
| @@ -3,13 +3,15 @@ | ||||
| 	archiveVersion = 1; | ||||
| 	classes = { | ||||
| 	}; | ||||
| 	objectVersion = 54; | ||||
| 	objectVersion = 77; | ||||
| 	objects = { | ||||
|  | ||||
| /* Begin PBXBuildFile section */ | ||||
| 		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; | ||||
| 		331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; | ||||
| 		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; | ||||
| 		73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; }; | ||||
| 		73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||||
| 		73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||||
| 		73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||||
| 		73D4264B2DEB815D006C0AAE /* NotifyDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */; }; | ||||
| @@ -32,6 +34,13 @@ | ||||
| 			remoteGlobalIDString = 97C146ED1CF9000F007C117D; | ||||
| 			remoteInfo = Runner; | ||||
| 		}; | ||||
| 		73ACDFC12E3D0E6100B63535 /* PBXContainerItemProxy */ = { | ||||
| 			isa = PBXContainerItemProxy; | ||||
| 			containerPortal = 97C146E61CF9000F007C117D /* Project object */; | ||||
| 			proxyType = 1; | ||||
| 			remoteGlobalIDString = 73ACDFAA2E3D0E6100B63535; | ||||
| 			remoteInfo = SolianBroadcastExtension; | ||||
| 		}; | ||||
| 		73C305D62E0BE878009035B9 /* PBXContainerItemProxy */ = { | ||||
| 			isa = PBXContainerItemProxy; | ||||
| 			containerPortal = 97C146E61CF9000F007C117D /* Project object */; | ||||
| @@ -55,6 +64,7 @@ | ||||
| 			dstPath = ""; | ||||
| 			dstSubfolderSpec = 13; | ||||
| 			files = ( | ||||
| 				73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */, | ||||
| 				73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */, | ||||
| 				73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */, | ||||
| 			); | ||||
| @@ -91,6 +101,9 @@ | ||||
| 		3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; | ||||
| 		737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; }; | ||||
| 		73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; }; | ||||
| 		73ACDFB82E3D0E6100B63535 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; | ||||
| 		73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianNotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyDelegate.swift; sourceTree = "<group>"; }; | ||||
| @@ -117,6 +130,13 @@ | ||||
| /* End PBXFileReference section */ | ||||
|  | ||||
| /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ | ||||
| 		73ACDFCA2E3D0E6100B63535 /* Exceptions for "SolianBroadcastExtension" folder in "SolianBroadcastExtension" target */ = { | ||||
| 			isa = PBXFileSystemSynchronizedBuildFileExceptionSet; | ||||
| 			membershipExceptions = ( | ||||
| 				Info.plist, | ||||
| 			); | ||||
| 			target = 73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */; | ||||
| 		}; | ||||
| 		73C305DC2E0BE878009035B9 /* Exceptions for "SolianShareExtension" folder in "SolianShareExtension" target */ = { | ||||
| 			isa = PBXFileSystemSynchronizedBuildFileExceptionSet; | ||||
| 			membershipExceptions = ( | ||||
| @@ -150,6 +170,14 @@ | ||||
| 			path = Services; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */ = { | ||||
| 			isa = PBXFileSystemSynchronizedRootGroup; | ||||
| 			exceptions = ( | ||||
| 				73ACDFCA2E3D0E6100B63535 /* Exceptions for "SolianBroadcastExtension" folder in "SolianBroadcastExtension" target */, | ||||
| 			); | ||||
| 			path = SolianBroadcastExtension; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		73C305CF2E0BE878009035B9 /* SolianShareExtension */ = { | ||||
| 			isa = PBXFileSystemSynchronizedRootGroup; | ||||
| 			exceptions = ( | ||||
| @@ -177,6 +205,14 @@ | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		73ACDFA82E3D0E6100B63535 /* Frameworks */ = { | ||||
| 			isa = PBXFrameworksBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		73C305CB2E0BE878009035B9 /* Frameworks */ = { | ||||
| 			isa = PBXFrameworksBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| @@ -220,6 +256,8 @@ | ||||
| 				AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */, | ||||
| 				39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */, | ||||
| 				7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */, | ||||
| 				73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */, | ||||
| 				73ACDFB82E3D0E6100B63535 /* UIKit.framework */, | ||||
| 			); | ||||
| 			name = Frameworks; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -264,6 +302,7 @@ | ||||
| 				97C146F01CF9000F007C117D /* Runner */, | ||||
| 				73CDD67B2DEC00480059D95D /* SolianNotificationService */, | ||||
| 				73C305CF2E0BE878009035B9 /* SolianShareExtension */, | ||||
| 				73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */, | ||||
| 				97C146EF1CF9000F007C117D /* Products */, | ||||
| 				331C8082294A63A400263BE5 /* RunnerTests */, | ||||
| 				91E124CE95BCB4DCD890160D /* Pods */, | ||||
| @@ -279,6 +318,7 @@ | ||||
| 				331C8081294A63A400263BE5 /* RunnerTests.xctest */, | ||||
| 				73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */, | ||||
| 				73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */, | ||||
| 				73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */, | ||||
| 			); | ||||
| 			name = Products; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -323,6 +363,28 @@ | ||||
| 			productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; | ||||
| 			productType = "com.apple.product-type.bundle.unit-test"; | ||||
| 		}; | ||||
| 		73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = { | ||||
| 			isa = PBXNativeTarget; | ||||
| 			buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */; | ||||
| 			buildPhases = ( | ||||
| 				73ACDFA72E3D0E6100B63535 /* Sources */, | ||||
| 				73ACDFA82E3D0E6100B63535 /* Frameworks */, | ||||
| 				73ACDFA92E3D0E6100B63535 /* Resources */, | ||||
| 			); | ||||
| 			buildRules = ( | ||||
| 			); | ||||
| 			dependencies = ( | ||||
| 			); | ||||
| 			fileSystemSynchronizedGroups = ( | ||||
| 				73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */, | ||||
| 			); | ||||
| 			name = SolianBroadcastExtension; | ||||
| 			packageProductDependencies = ( | ||||
| 			); | ||||
| 			productName = SolianBroadcastExtension; | ||||
| 			productReference = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; | ||||
| 			productType = "com.apple.product-type.app-extension"; | ||||
| 		}; | ||||
| 		73C305CD2E0BE878009035B9 /* SolianShareExtension */ = { | ||||
| 			isa = PBXNativeTarget; | ||||
| 			buildConfigurationList = 73C305DD2E0BE878009035B9 /* Build configuration list for PBXNativeTarget "SolianShareExtension" */; | ||||
| @@ -385,6 +447,7 @@ | ||||
| 			dependencies = ( | ||||
| 				73CDD6802DEC00480059D95D /* PBXTargetDependency */, | ||||
| 				73C305D72E0BE878009035B9 /* PBXTargetDependency */, | ||||
| 				73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */, | ||||
| 			); | ||||
| 			fileSystemSynchronizedGroups = ( | ||||
| 				73268D272DEB012A0076E970 /* Services */, | ||||
| @@ -409,6 +472,9 @@ | ||||
| 						CreatedOnToolsVersion = 14.0; | ||||
| 						TestTargetID = 97C146ED1CF9000F007C117D; | ||||
| 					}; | ||||
| 					73ACDFAA2E3D0E6100B63535 = { | ||||
| 						CreatedOnToolsVersion = 16.4; | ||||
| 					}; | ||||
| 					73C305CD2E0BE878009035B9 = { | ||||
| 						CreatedOnToolsVersion = 16.4; | ||||
| 					}; | ||||
| @@ -438,6 +504,7 @@ | ||||
| 				331C8080294A63A400263BE5 /* RunnerTests */, | ||||
| 				73CDD6792DEC00480059D95D /* SolianNotificationService */, | ||||
| 				73C305CD2E0BE878009035B9 /* SolianShareExtension */, | ||||
| 				73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */, | ||||
| 			); | ||||
| 		}; | ||||
| /* End PBXProject section */ | ||||
| @@ -450,6 +517,13 @@ | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		73ACDFA92E3D0E6100B63535 /* Resources */ = { | ||||
| 			isa = PBXResourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		73C305CC2E0BE878009035B9 /* Resources */ = { | ||||
| 			isa = PBXResourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| @@ -525,10 +599,14 @@ | ||||
| 			inputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", | ||||
| 			); | ||||
| 			inputPaths = ( | ||||
| 			); | ||||
| 			name = "[CP] Copy Pods Resources"; | ||||
| 			outputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", | ||||
| 			); | ||||
| 			outputPaths = ( | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 			shellPath = /bin/sh; | ||||
| 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; | ||||
| @@ -586,10 +664,14 @@ | ||||
| 			inputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", | ||||
| 			); | ||||
| 			inputPaths = ( | ||||
| 			); | ||||
| 			name = "[CP] Embed Pods Frameworks"; | ||||
| 			outputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", | ||||
| 			); | ||||
| 			outputPaths = ( | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 			shellPath = /bin/sh; | ||||
| 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; | ||||
| @@ -643,6 +725,13 @@ | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		73ACDFA72E3D0E6100B63535 /* Sources */ = { | ||||
| 			isa = PBXSourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		73C305CA2E0BE878009035B9 /* Sources */ = { | ||||
| 			isa = PBXSourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| @@ -675,6 +764,11 @@ | ||||
| 			target = 97C146ED1CF9000F007C117D /* Runner */; | ||||
| 			targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; | ||||
| 		}; | ||||
| 		73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */ = { | ||||
| 			isa = PBXTargetDependency; | ||||
| 			target = 73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */; | ||||
| 			targetProxy = 73ACDFC12E3D0E6100B63535 /* PBXContainerItemProxy */; | ||||
| 		}; | ||||
| 		73C305D72E0BE878009035B9 /* PBXTargetDependency */ = { | ||||
| 			isa = PBXTargetDependency; | ||||
| 			target = 73C305CD2E0BE878009035B9 /* SolianShareExtension */; | ||||
| @@ -773,7 +867,7 @@ | ||||
| 				INFOPLIST_FILE = Runner/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -836,6 +930,123 @@ | ||||
| 			}; | ||||
| 			name = Profile; | ||||
| 		}; | ||||
| 		73ACDFC42E3D0E6100B63535 /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||
| 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; | ||||
| 				CLANG_ENABLE_OBJC_WEAK = YES; | ||||
| 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES; | ||||
| 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; | ||||
| 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; | ||||
| 				CODE_SIGN_ENTITLEMENTS = SolianBroadcastExtension/SolianBroadcastExtension.entitlements; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				ENABLE_USER_SCRIPT_SANDBOXING = YES; | ||||
| 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||
| 				GENERATE_INFOPLIST_FILE = YES; | ||||
| 				INFOPLIST_FILE = SolianBroadcastExtension/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianBroadcastExtension; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| 					"@executable_path/../../Frameworks", | ||||
| 				); | ||||
| 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES; | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; | ||||
| 				MTL_FAST_MATH = YES; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TARGETED_DEVICE_FAMILY = "1,2"; | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| 		73ACDFC52E3D0E6100B63535 /* Release */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||
| 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; | ||||
| 				CLANG_ENABLE_OBJC_WEAK = YES; | ||||
| 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES; | ||||
| 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; | ||||
| 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; | ||||
| 				CODE_SIGN_ENTITLEMENTS = SolianBroadcastExtension/SolianBroadcastExtension.entitlements; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				ENABLE_USER_SCRIPT_SANDBOXING = YES; | ||||
| 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||
| 				GENERATE_INFOPLIST_FILE = YES; | ||||
| 				INFOPLIST_FILE = SolianBroadcastExtension/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianBroadcastExtension; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| 					"@executable_path/../../Frameworks", | ||||
| 				); | ||||
| 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES; | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				MTL_FAST_MATH = YES; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TARGETED_DEVICE_FAMILY = "1,2"; | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| 		73ACDFC62E3D0E6100B63535 /* Profile */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||
| 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; | ||||
| 				CLANG_ENABLE_OBJC_WEAK = YES; | ||||
| 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES; | ||||
| 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; | ||||
| 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; | ||||
| 				CODE_SIGN_ENTITLEMENTS = SolianBroadcastExtension/SolianBroadcastExtension.entitlements; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				ENABLE_USER_SCRIPT_SANDBOXING = YES; | ||||
| 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||
| 				GENERATE_INFOPLIST_FILE = YES; | ||||
| 				INFOPLIST_FILE = SolianBroadcastExtension/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianBroadcastExtension; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| 					"@executable_path/../../Frameworks", | ||||
| 				); | ||||
| 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES; | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				MTL_FAST_MATH = YES; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TARGETED_DEVICE_FAMILY = "1,2"; | ||||
| 			}; | ||||
| 			name = Profile; | ||||
| 		}; | ||||
| 		73C305D92E0BE878009035B9 /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = 17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */; | ||||
| @@ -1204,7 +1415,7 @@ | ||||
| 				INFOPLIST_FILE = Runner/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -1232,7 +1443,7 @@ | ||||
| 				INFOPLIST_FILE = Runner/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -1258,6 +1469,16 @@ | ||||
| 			defaultConfigurationIsVisible = 0; | ||||
| 			defaultConfigurationName = Release; | ||||
| 		}; | ||||
| 		73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = { | ||||
| 			isa = XCConfigurationList; | ||||
| 			buildConfigurations = ( | ||||
| 				73ACDFC42E3D0E6100B63535 /* Debug */, | ||||
| 				73ACDFC52E3D0E6100B63535 /* Release */, | ||||
| 				73ACDFC62E3D0E6100B63535 /* Profile */, | ||||
| 			); | ||||
| 			defaultConfigurationIsVisible = 0; | ||||
| 			defaultConfigurationName = Release; | ||||
| 		}; | ||||
| 		73C305DD2E0BE878009035B9 /* Build configuration list for PBXNativeTarget "SolianShareExtension" */ = { | ||||
| 			isa = XCConfigurationList; | ||||
| 			buildConfigurations = ( | ||||
|   | ||||
							
								
								
									
										37
									
								
								ios/SolianBroadcastExtension/Atomic.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								ios/SolianBroadcastExtension/Atomic.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| // | ||||
| //  Atomic.swift | ||||
| //  Broadcast Extension | ||||
| // | ||||
| //  Created by Maksym Shcheglov. | ||||
| //  https://www.onswiftwings.com/posts/atomic-property-wrapper/ | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| @propertyWrapper | ||||
| struct Atomic<Value> { | ||||
|  | ||||
|     private var value: Value | ||||
|     private let lock = NSLock() | ||||
|  | ||||
|     init(wrappedValue value: Value) { | ||||
|         self.value = value | ||||
|     } | ||||
|  | ||||
|     var wrappedValue: Value { | ||||
|         get { load() } | ||||
|         set { store(newValue: newValue) } | ||||
|     } | ||||
|  | ||||
|     func load() -> Value { | ||||
|         lock.lock() | ||||
|         defer { lock.unlock() } | ||||
|         return value | ||||
|     } | ||||
|  | ||||
|     mutating func store(newValue: Value) { | ||||
|         lock.lock() | ||||
|         defer { lock.unlock() } | ||||
|         value = newValue | ||||
|     } | ||||
| } | ||||
							
								
								
									
										29
									
								
								ios/SolianBroadcastExtension/DarwinNotification.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ios/SolianBroadcastExtension/DarwinNotification.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| // | ||||
| //  DarwinNotificationCenter.swift | ||||
| //  Broadcast Extension | ||||
| // | ||||
| //  Created by Alex-Dan Bumbu on 23/03/2021. | ||||
| //  Copyright © 2021 8x8, Inc. All rights reserved. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| enum DarwinNotification: String { | ||||
|     case broadcastStarted = "iOS_BroadcastStarted" | ||||
|     case broadcastStopped = "iOS_BroadcastStopped" | ||||
| } | ||||
|  | ||||
| class DarwinNotificationCenter { | ||||
|      | ||||
|     static let shared = DarwinNotificationCenter() | ||||
|      | ||||
|     private let notificationCenter: CFNotificationCenter | ||||
|      | ||||
|     init() { | ||||
|         notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() | ||||
|     } | ||||
|      | ||||
|     func postNotification(_ name: DarwinNotification) { | ||||
|         CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(rawValue: name.rawValue as CFString), nil, nil, true) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15
									
								
								ios/SolianBroadcastExtension/Info.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								ios/SolianBroadcastExtension/Info.plist
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
| <dict> | ||||
| 	<key>NSExtension</key> | ||||
| 	<dict> | ||||
| 		<key>NSExtensionPointIdentifier</key> | ||||
| 		<string>com.apple.broadcast-services-upload</string> | ||||
| 		<key>NSExtensionPrincipalClass</key> | ||||
| 		<string>$(PRODUCT_MODULE_NAME).SampleHandler</string> | ||||
| 		<key>RPBroadcastProcessMode</key> | ||||
| 		<string>RPBroadcastProcessModeSampleBuffer</string> | ||||
| 	</dict> | ||||
| </dict> | ||||
| </plist> | ||||
							
								
								
									
										103
									
								
								ios/SolianBroadcastExtension/SampleHandler.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								ios/SolianBroadcastExtension/SampleHandler.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| // | ||||
| //  SampleHandler.swift | ||||
| //  Broadcast Extension | ||||
| // | ||||
| //  Created by Alex-Dan Bumbu on 04.06.2021. | ||||
| // | ||||
|  | ||||
| import ReplayKit | ||||
| import OSLog | ||||
|  | ||||
| let broadcastLogger = OSLog(subsystem: "dev.solsynth.solian", category: "Broadcast") | ||||
| private enum Constants { | ||||
|     // the App Group ID value that the app and the broadcast extension targets are setup with. It differs for each app. | ||||
|     static let appGroupIdentifier = "group.solsynth.solian" | ||||
| } | ||||
|  | ||||
| class SampleHandler: RPBroadcastSampleHandler { | ||||
|  | ||||
|     private var clientConnection: SocketConnection? | ||||
|     private var uploader: SampleUploader? | ||||
|  | ||||
|     private var frameCount: Int = 0 | ||||
|  | ||||
|     var socketFilePath: String { | ||||
|       let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier) | ||||
|         return sharedContainer?.appendingPathComponent("rtc_SSFD").path ?? "" | ||||
|     } | ||||
|  | ||||
|     override init() { | ||||
|       super.init() | ||||
|         if let connection = SocketConnection(filePath: socketFilePath) { | ||||
|           clientConnection = connection | ||||
|           setupConnection() | ||||
|  | ||||
|           uploader = SampleUploader(connection: connection) | ||||
|         } | ||||
|         os_log(.debug, log: broadcastLogger, "%{public}s", socketFilePath) | ||||
|     } | ||||
|  | ||||
|     override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { | ||||
|         // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional. | ||||
|         frameCount = 0 | ||||
|  | ||||
|         DarwinNotificationCenter.shared.postNotification(.broadcastStarted) | ||||
|         openConnection() | ||||
|     } | ||||
|  | ||||
|     override func broadcastPaused() { | ||||
|         // User has requested to pause the broadcast. Samples will stop being delivered. | ||||
|     } | ||||
|  | ||||
|     override func broadcastResumed() { | ||||
|         // User has requested to resume the broadcast. Samples delivery will resume. | ||||
|     } | ||||
|  | ||||
|     override func broadcastFinished() { | ||||
|         // User has requested to finish the broadcast. | ||||
|         DarwinNotificationCenter.shared.postNotification(.broadcastStopped) | ||||
|         clientConnection?.close() | ||||
|     } | ||||
|  | ||||
|     override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { | ||||
|         switch sampleBufferType { | ||||
|         case RPSampleBufferType.video: | ||||
|             uploader?.send(sample: sampleBuffer) | ||||
|         default: | ||||
|             break | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private extension SampleHandler { | ||||
|  | ||||
|     func setupConnection() { | ||||
|         clientConnection?.didClose = { [weak self] error in | ||||
|             os_log(.debug, log: broadcastLogger, "client connection did close \(String(describing: error))") | ||||
|  | ||||
|             if let error = error { | ||||
|                 self?.finishBroadcastWithError(error) | ||||
|             } else { | ||||
|                 // the displayed failure message is more user friendly when using NSError instead of Error | ||||
|                 let JMScreenSharingStopped = 10001 | ||||
|                 let customError = NSError(domain: RPRecordingErrorDomain, code: JMScreenSharingStopped, userInfo: [NSLocalizedDescriptionKey: "Screen sharing stopped"]) | ||||
|                 self?.finishBroadcastWithError(customError) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func openConnection() { | ||||
|         let queue = DispatchQueue(label: "broadcast.connectTimer") | ||||
|         let timer = DispatchSource.makeTimerSource(queue: queue) | ||||
|         timer.schedule(deadline: .now(), repeating: .milliseconds(100), leeway: .milliseconds(500)) | ||||
|         timer.setEventHandler { [weak self] in | ||||
|             guard self?.clientConnection?.open() == true else { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             timer.cancel() | ||||
|         } | ||||
|  | ||||
|         timer.resume() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										147
									
								
								ios/SolianBroadcastExtension/SampleUploader.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								ios/SolianBroadcastExtension/SampleUploader.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| // | ||||
| //  SampleUploader.swift | ||||
| //  Broadcast Extension | ||||
| // | ||||
| //  Created by Alex-Dan Bumbu on 22/03/2021. | ||||
| //  Copyright © 2021 8x8, Inc. All rights reserved. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import ReplayKit | ||||
| import OSLog | ||||
|  | ||||
| private enum Constants { | ||||
|     static let bufferMaxLength = 10240 | ||||
| } | ||||
|  | ||||
| class SampleUploader { | ||||
|      | ||||
|     private static var imageContext = CIContext(options: nil) | ||||
|      | ||||
|     @Atomic private var isReady = false | ||||
|     private var connection: SocketConnection | ||||
|    | ||||
|     private var dataToSend: Data? | ||||
|     private var byteIndex = 0 | ||||
|    | ||||
|     private let serialQueue: DispatchQueue | ||||
|      | ||||
|     init(connection: SocketConnection) { | ||||
|         self.connection = connection | ||||
|         self.serialQueue = DispatchQueue(label: "org.jitsi.meet.broadcast.sampleUploader") | ||||
|        | ||||
|         setupConnection() | ||||
|     } | ||||
|    | ||||
|     @discardableResult func send(sample buffer: CMSampleBuffer) -> Bool { | ||||
|         guard isReady else { | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         isReady = false | ||||
|  | ||||
|         dataToSend = prepare(sample: buffer) | ||||
|         byteIndex = 0 | ||||
|  | ||||
|         serialQueue.async { [weak self] in | ||||
|             self?.sendDataChunk() | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
| } | ||||
|  | ||||
| private extension SampleUploader { | ||||
|      | ||||
|     func setupConnection() { | ||||
|         connection.didOpen = { [weak self] in | ||||
|             self?.isReady = true | ||||
|         } | ||||
|         connection.streamHasSpaceAvailable = { [weak self] in | ||||
|             self?.serialQueue.async { | ||||
|                 if let success = self?.sendDataChunk() { | ||||
|                     self?.isReady = !success | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     @discardableResult func sendDataChunk() -> Bool { | ||||
|         guard let dataToSend = dataToSend else { | ||||
|             return false | ||||
|         } | ||||
|        | ||||
|         var bytesLeft = dataToSend.count - byteIndex | ||||
|         var length = bytesLeft > Constants.bufferMaxLength ? Constants.bufferMaxLength : bytesLeft | ||||
|  | ||||
|         length = dataToSend[byteIndex..<(byteIndex + length)].withUnsafeBytes { | ||||
|             guard let ptr = $0.bindMemory(to: UInt8.self).baseAddress else { | ||||
|                 return 0 | ||||
|             } | ||||
|  | ||||
|             return connection.writeToStream(buffer: ptr, maxLength: length) | ||||
|         } | ||||
|  | ||||
|         if length > 0 { | ||||
|             byteIndex += length | ||||
|             bytesLeft -= length | ||||
|  | ||||
|             if bytesLeft == 0 { | ||||
|                 self.dataToSend = nil | ||||
|                 byteIndex = 0 | ||||
|             } | ||||
|         } else { | ||||
|             os_log(.debug, log: broadcastLogger, "writeBufferToStream failure") | ||||
|         } | ||||
|        | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     func prepare(sample buffer: CMSampleBuffer) -> Data? { | ||||
|         guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else { | ||||
|             os_log(.debug, log: broadcastLogger, "image buffer not available") | ||||
|             return nil | ||||
|         } | ||||
|          | ||||
|         CVPixelBufferLockBaseAddress(imageBuffer, .readOnly) | ||||
|          | ||||
|         let scaleFactor = 1.0 | ||||
|         let width = CVPixelBufferGetWidth(imageBuffer)/Int(scaleFactor) | ||||
|         let height = CVPixelBufferGetHeight(imageBuffer)/Int(scaleFactor) | ||||
|         let orientation = CMGetAttachment(buffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil)?.uintValue ?? 0 | ||||
|                                      | ||||
|         let scaleTransform = CGAffineTransform(scaleX: CGFloat(1.0/scaleFactor), y: CGFloat(1.0/scaleFactor)) | ||||
|         let bufferData = self.jpegData(from: imageBuffer, scale: scaleTransform) | ||||
|          | ||||
|         CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly) | ||||
|          | ||||
|         guard let messageData = bufferData else { | ||||
|             os_log(.debug, log: broadcastLogger, "corrupted image buffer") | ||||
|             return nil | ||||
|         } | ||||
|                | ||||
|         let httpResponse = CFHTTPMessageCreateResponse(nil, 200, nil, kCFHTTPVersion1_1).takeRetainedValue() | ||||
|         CFHTTPMessageSetHeaderFieldValue(httpResponse, "Content-Length" as CFString, String(messageData.count) as CFString) | ||||
|         CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Width" as CFString, String(width) as CFString) | ||||
|         CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Height" as CFString, String(height) as CFString) | ||||
|         CFHTTPMessageSetHeaderFieldValue(httpResponse, "Buffer-Orientation" as CFString, String(orientation) as CFString) | ||||
|          | ||||
|         CFHTTPMessageSetBody(httpResponse, messageData as CFData) | ||||
|          | ||||
|         let serializedMessage = CFHTTPMessageCopySerializedMessage(httpResponse)?.takeRetainedValue() as Data? | ||||
|        | ||||
|         return serializedMessage | ||||
|     } | ||||
|      | ||||
|     func jpegData(from buffer: CVPixelBuffer, scale scaleTransform: CGAffineTransform) -> Data? { | ||||
|         let image = CIImage(cvPixelBuffer: buffer).transformed(by: scaleTransform) | ||||
|          | ||||
|         guard let colorSpace = image.colorSpace else { | ||||
|             return nil | ||||
|         } | ||||
|        | ||||
|         let options: [CIImageRepresentationOption: Float] = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0] | ||||
|  | ||||
|         return SampleUploader.imageContext.jpegRepresentation(of: image, colorSpace: colorSpace, options: options) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										199
									
								
								ios/SolianBroadcastExtension/SocketConnection.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								ios/SolianBroadcastExtension/SocketConnection.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| // | ||||
| //  SocketConnection.swift | ||||
| //  Broadcast Extension | ||||
| // | ||||
| //  Created by Alex-Dan Bumbu on 22/03/2021. | ||||
| //  Copyright © 2021 Atlassian Inc. All rights reserved. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import OSLog | ||||
|  | ||||
| class SocketConnection: NSObject { | ||||
|     var didOpen: (() -> Void)? | ||||
|     var didClose: ((Error?) -> Void)? | ||||
|     var streamHasSpaceAvailable: (() -> Void)? | ||||
|  | ||||
|     private let filePath: String | ||||
|     private var socketHandle: Int32 = -1 | ||||
|     private var address: sockaddr_un? | ||||
|  | ||||
|     private var inputStream: InputStream? | ||||
|     private var outputStream: OutputStream? | ||||
|      | ||||
|     private var networkQueue: DispatchQueue? | ||||
|     private var shouldKeepRunning = false | ||||
|  | ||||
|     init?(filePath path: String) { | ||||
|         filePath = path | ||||
|         socketHandle = Darwin.socket(AF_UNIX, SOCK_STREAM, 0) | ||||
|  | ||||
|         guard socketHandle != -1 else { | ||||
|             os_log(.debug, log: broadcastLogger, "failure: create socket") | ||||
|             return nil | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func open() -> Bool { | ||||
|         os_log(.debug, log: broadcastLogger, "open socket connection") | ||||
|  | ||||
|         guard FileManager.default.fileExists(atPath: filePath) else { | ||||
|             os_log(.debug, log: broadcastLogger, "failure: socket file missing") | ||||
|             return false | ||||
|         } | ||||
|        | ||||
|         guard setupAddress() == true else { | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         guard connectSocket() == true else { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         setupStreams() | ||||
|          | ||||
|         inputStream?.open() | ||||
|         outputStream?.open() | ||||
|  | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     func close() { | ||||
|         unscheduleStreams() | ||||
|  | ||||
|         inputStream?.delegate = nil | ||||
|         outputStream?.delegate = nil | ||||
|  | ||||
|         inputStream?.close() | ||||
|         outputStream?.close() | ||||
|          | ||||
|         inputStream = nil | ||||
|         outputStream = nil | ||||
|     } | ||||
|  | ||||
|     func writeToStream(buffer: UnsafePointer<UInt8>, maxLength length: Int) -> Int { | ||||
|         outputStream?.write(buffer, maxLength: length) ?? 0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| extension SocketConnection: StreamDelegate { | ||||
|  | ||||
|     func stream(_ aStream: Stream, handle eventCode: Stream.Event) { | ||||
|         switch eventCode { | ||||
|         case .openCompleted: | ||||
|             os_log(.debug, log: broadcastLogger, "client stream open completed") | ||||
|             if aStream == outputStream { | ||||
|                 didOpen?() | ||||
|             } | ||||
|         case .hasBytesAvailable: | ||||
|             if aStream == inputStream { | ||||
|                 var buffer: UInt8 = 0 | ||||
|                 let numberOfBytesRead = inputStream?.read(&buffer, maxLength: 1) | ||||
|                 if numberOfBytesRead == 0 && aStream.streamStatus == .atEnd { | ||||
|                     os_log(.debug, log: broadcastLogger, "server socket closed") | ||||
|                     close() | ||||
|                     notifyDidClose(error: nil) | ||||
|                 } | ||||
|             } | ||||
|         case .hasSpaceAvailable: | ||||
|             if aStream == outputStream { | ||||
|                 streamHasSpaceAvailable?() | ||||
|             } | ||||
|         case .errorOccurred: | ||||
|             os_log(.debug, log: broadcastLogger, "client stream error occured: \(String(describing: aStream.streamError))") | ||||
|             close() | ||||
|             notifyDidClose(error: aStream.streamError) | ||||
|  | ||||
|         default: | ||||
|             break | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private extension SocketConnection { | ||||
|    | ||||
|     func setupAddress() -> Bool { | ||||
|         var addr = sockaddr_un() | ||||
|         guard filePath.count < MemoryLayout.size(ofValue: addr.sun_path) else { | ||||
|             os_log(.debug, log: broadcastLogger, "failure: fd path is too long") | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         _ = withUnsafeMutablePointer(to: &addr.sun_path.0) { ptr in | ||||
|             filePath.withCString { | ||||
|                 strncpy(ptr, $0, filePath.count) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         address = addr | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     func connectSocket() -> Bool { | ||||
|         guard var addr = address else { | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         let status = withUnsafePointer(to: &addr) { ptr in | ||||
|             ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { | ||||
|                 Darwin.connect(socketHandle, $0, socklen_t(MemoryLayout<sockaddr_un>.size)) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         guard status == noErr else { | ||||
|             os_log(.debug, log: broadcastLogger, "failure: \(status)") | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     func setupStreams() { | ||||
|         var readStream: Unmanaged<CFReadStream>? | ||||
|         var writeStream: Unmanaged<CFWriteStream>? | ||||
|  | ||||
|         CFStreamCreatePairWithSocket(kCFAllocatorDefault, socketHandle, &readStream, &writeStream) | ||||
|  | ||||
|         inputStream = readStream?.takeRetainedValue() | ||||
|         inputStream?.delegate = self | ||||
|         inputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String)) | ||||
|  | ||||
|         outputStream = writeStream?.takeRetainedValue() | ||||
|         outputStream?.delegate = self | ||||
|         outputStream?.setProperty(kCFBooleanTrue, forKey: Stream.PropertyKey(kCFStreamPropertyShouldCloseNativeSocket as String)) | ||||
|  | ||||
|         scheduleStreams() | ||||
|     } | ||||
|    | ||||
|     func scheduleStreams() { | ||||
|         shouldKeepRunning = true | ||||
|          | ||||
|         networkQueue = DispatchQueue.global(qos: .userInitiated) | ||||
|         networkQueue?.async { [weak self] in | ||||
|             self?.inputStream?.schedule(in: .current, forMode: .common) | ||||
|             self?.outputStream?.schedule(in: .current, forMode: .common) | ||||
|             RunLoop.current.run() | ||||
|              | ||||
|             var isRunning = false | ||||
|                          | ||||
|             repeat { | ||||
|                 isRunning = self?.shouldKeepRunning ?? false && RunLoop.current.run(mode: .default, before: .distantFuture) | ||||
|             } while (isRunning) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func unscheduleStreams() { | ||||
|         networkQueue?.sync { [weak self] in | ||||
|             self?.inputStream?.remove(from: .current, forMode: .common) | ||||
|             self?.outputStream?.remove(from: .current, forMode: .common) | ||||
|         } | ||||
|          | ||||
|         shouldKeepRunning = false | ||||
|     } | ||||
|      | ||||
|     func notifyDidClose(error: Error?) { | ||||
|         if didClose != nil { | ||||
|             didClose?(error) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
| <dict> | ||||
| 	<key>com.apple.security.application-groups</key> | ||||
| 	<array> | ||||
| 		<string>group.solsynth.solian</string> | ||||
| 	</array> | ||||
| </dict> | ||||
| </plist> | ||||
| @@ -49,8 +49,7 @@ class AppDatabase extends _$AppDatabase { | ||||
|   } | ||||
|  | ||||
|   Future<int> updateMessage(ChatMessagesCompanion message) { | ||||
|     return (update(chatMessages) | ||||
|       ..where((m) => m.id.equals(message.id.value))).write(message); | ||||
|     return into(chatMessages).insert(message, mode: InsertMode.insertOrReplace); | ||||
|   } | ||||
|  | ||||
|   Future<int> updateMessageStatus(String id, MessageStatus status) { | ||||
|   | ||||
| @@ -20,7 +20,6 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/route.dart'; | ||||
|  | ||||
| import 'package:island/services/notify.dart'; | ||||
| import 'package:island/services/timezone.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| @@ -30,6 +29,7 @@ import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; | ||||
| import 'package:flutter_native_splash/flutter_native_splash.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
| import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; | ||||
|  | ||||
| @pragma('vm:entry-point') | ||||
| Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { | ||||
| @@ -51,6 +51,7 @@ void main() async { | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     await langdetect.initLangDetect(); | ||||
|     await EasyLocalization.ensureInitialized(); | ||||
|     await Firebase.initializeApp( | ||||
|       options: DefaultFirebaseOptions.currentPlatform, | ||||
|   | ||||
| @@ -162,8 +162,6 @@ sealed class CallParticipant with _$CallParticipant { | ||||
|     required String identity, | ||||
|     required String name, | ||||
|     required DateTime joinedAt, | ||||
|     required String? accountId, | ||||
|     required SnChatMember? profile, | ||||
|   }) = _CallParticipant; | ||||
|  | ||||
|   factory CallParticipant.fromJson(Map<String, dynamic> json) => | ||||
|   | ||||
| @@ -2498,7 +2498,7 @@ as List<CallParticipant>, | ||||
| /// @nodoc | ||||
| mixin _$CallParticipant { | ||||
|  | ||||
|  String get identity; String get name; DateTime get joinedAt; String? get accountId; SnChatMember? get profile; | ||||
|  String get identity; String get name; DateTime get joinedAt; | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -2511,16 +2511,16 @@ $CallParticipantCopyWith<CallParticipant> get copyWith => _$CallParticipantCopyW | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.profile, profile) || other.profile == profile)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,identity,name,joinedAt,accountId,profile); | ||||
| int get hashCode => Object.hash(runtimeType,identity,name,joinedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt, accountId: $accountId, profile: $profile)'; | ||||
|   return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -2531,11 +2531,11 @@ abstract mixin class $CallParticipantCopyWith<$Res>  { | ||||
|   factory $CallParticipantCopyWith(CallParticipant value, $Res Function(CallParticipant) _then) = _$CallParticipantCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile | ||||
|  String identity, String name, DateTime joinedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| $SnChatMemberCopyWith<$Res>? get profile; | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -2548,29 +2548,15 @@ class _$CallParticipantCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,Object? accountId = freezed,Object? profile = freezed,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,profile: freezed == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | ||||
| as SnChatMember?, | ||||
| as DateTime, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnChatMemberCopyWith<$Res>? get profile { | ||||
|     if (_self.profile == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnChatMemberCopyWith<$Res>(_self.profile!, (value) { | ||||
|     return _then(_self.copyWith(profile: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -2649,10 +2635,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity,  String name,  DateTime joinedAt,  String? accountId,  SnChatMember? profile)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity,  String name,  DateTime joinedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CallParticipant() when $default != null: | ||||
| return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.profile);case _: | ||||
| return $default(_that.identity,_that.name,_that.joinedAt);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -2670,10 +2656,10 @@ return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.p | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity,  String name,  DateTime joinedAt,  String? accountId,  SnChatMember? profile)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity,  String name,  DateTime joinedAt)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CallParticipant(): | ||||
| return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.profile);} | ||||
| return $default(_that.identity,_that.name,_that.joinedAt);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -2687,10 +2673,10 @@ return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.p | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity,  String name,  DateTime joinedAt,  String? accountId,  SnChatMember? profile)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity,  String name,  DateTime joinedAt)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CallParticipant() when $default != null: | ||||
| return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.profile);case _: | ||||
| return $default(_that.identity,_that.name,_that.joinedAt);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -2702,14 +2688,12 @@ return $default(_that.identity,_that.name,_that.joinedAt,_that.accountId,_that.p | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _CallParticipant implements CallParticipant { | ||||
|   const _CallParticipant({required this.identity, required this.name, required this.joinedAt, required this.accountId, required this.profile}); | ||||
|   const _CallParticipant({required this.identity, required this.name, required this.joinedAt}); | ||||
|   factory _CallParticipant.fromJson(Map<String, dynamic> json) => _$CallParticipantFromJson(json); | ||||
|  | ||||
| @override final  String identity; | ||||
| @override final  String name; | ||||
| @override final  DateTime joinedAt; | ||||
| @override final  String? accountId; | ||||
| @override final  SnChatMember? profile; | ||||
|  | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -2724,16 +2708,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.profile, profile) || other.profile == profile)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,identity,name,joinedAt,accountId,profile); | ||||
| int get hashCode => Object.hash(runtimeType,identity,name,joinedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt, accountId: $accountId, profile: $profile)'; | ||||
|   return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -2744,11 +2728,11 @@ abstract mixin class _$CallParticipantCopyWith<$Res> implements $CallParticipant | ||||
|   factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String identity, String name, DateTime joinedAt, String? accountId, SnChatMember? profile | ||||
|  String identity, String name, DateTime joinedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @override $SnChatMemberCopyWith<$Res>? get profile; | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -2761,30 +2745,16 @@ class __$CallParticipantCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,Object? accountId = freezed,Object? profile = freezed,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,}) { | ||||
|   return _then(_CallParticipant( | ||||
| identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,profile: freezed == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | ||||
| as SnChatMember?, | ||||
| as DateTime, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnChatMemberCopyWith<$Res>? get profile { | ||||
|     if (_self.profile == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnChatMemberCopyWith<$Res>(_self.profile!, (value) { | ||||
|     return _then(_self.copyWith(profile: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -285,11 +285,6 @@ _CallParticipant _$CallParticipantFromJson(Map<String, dynamic> json) => | ||||
|       identity: json['identity'] as String, | ||||
|       name: json['name'] as String, | ||||
|       joinedAt: DateTime.parse(json['joined_at'] as String), | ||||
|       accountId: json['account_id'] as String?, | ||||
|       profile: | ||||
|           json['profile'] == null | ||||
|               ? null | ||||
|               : SnChatMember.fromJson(json['profile'] as Map<String, dynamic>), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) => | ||||
| @@ -297,8 +292,6 @@ Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) => | ||||
|       'identity': instance.identity, | ||||
|       'name': instance.name, | ||||
|       'joined_at': instance.joinedAt.toIso8601String(), | ||||
|       'account_id': instance.accountId, | ||||
|       'profile': instance.profile?.toJson(), | ||||
|     }; | ||||
|  | ||||
| _SnRealtimeCall _$SnRealtimeCallFromJson(Map<String, dynamic> json) => | ||||
|   | ||||
| @@ -11,8 +11,8 @@ sealed class SnEmbedLink with _$SnEmbedLink { | ||||
|     @JsonKey(name: 'Title') required String title, | ||||
|     @JsonKey(name: 'Description') required String? description, | ||||
|     @JsonKey(name: 'ImageUrl') required String? imageUrl, | ||||
|     @JsonKey(name: 'FaviconUrl') required String faviconUrl, | ||||
|     @JsonKey(name: 'SiteName') required String siteName, | ||||
|     @JsonKey(name: 'FaviconUrl') @Default("") String faviconUrl, | ||||
|     @JsonKey(name: 'SiteName') @Default("") String siteName, | ||||
|     @JsonKey(name: 'ContentType') required String? contentType, | ||||
|     @JsonKey(name: 'Author') required String? author, | ||||
|     @JsonKey(name: 'PublishedDate') required DateTime? publishedDate, | ||||
|   | ||||
| @@ -212,7 +212,7 @@ return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUr | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnEmbedLink implements SnEmbedLink { | ||||
|   const _SnEmbedLink({@JsonKey(name: 'Type') required this.type, @JsonKey(name: 'Url') required this.url, @JsonKey(name: 'Title') required this.title, @JsonKey(name: 'Description') required this.description, @JsonKey(name: 'ImageUrl') required this.imageUrl, @JsonKey(name: 'FaviconUrl') required this.faviconUrl, @JsonKey(name: 'SiteName') required this.siteName, @JsonKey(name: 'ContentType') required this.contentType, @JsonKey(name: 'Author') required this.author, @JsonKey(name: 'PublishedDate') required this.publishedDate}); | ||||
|   const _SnEmbedLink({@JsonKey(name: 'Type') required this.type, @JsonKey(name: 'Url') required this.url, @JsonKey(name: 'Title') required this.title, @JsonKey(name: 'Description') required this.description, @JsonKey(name: 'ImageUrl') required this.imageUrl, @JsonKey(name: 'FaviconUrl') this.faviconUrl = "", @JsonKey(name: 'SiteName') this.siteName = "", @JsonKey(name: 'ContentType') required this.contentType, @JsonKey(name: 'Author') required this.author, @JsonKey(name: 'PublishedDate') required this.publishedDate}); | ||||
|   factory _SnEmbedLink.fromJson(Map<String, dynamic> json) => _$SnEmbedLinkFromJson(json); | ||||
|  | ||||
| @override@JsonKey(name: 'Type') final  String type; | ||||
|   | ||||
| @@ -12,8 +12,8 @@ _SnEmbedLink _$SnEmbedLinkFromJson(Map<String, dynamic> json) => _SnEmbedLink( | ||||
|   title: json['Title'] as String, | ||||
|   description: json['Description'] as String?, | ||||
|   imageUrl: json['ImageUrl'] as String?, | ||||
|   faviconUrl: json['FaviconUrl'] as String, | ||||
|   siteName: json['SiteName'] as String, | ||||
|   faviconUrl: json['FaviconUrl'] as String? ?? "", | ||||
|   siteName: json['SiteName'] as String? ?? "", | ||||
|   contentType: json['ContentType'] as String?, | ||||
|   author: json['Author'] as String?, | ||||
|   publishedDate: | ||||
|   | ||||
| @@ -34,6 +34,7 @@ sealed class SnPost with _$SnPost { | ||||
|     @Default([]) List<SnCloudFile> attachments, | ||||
|     required SnPublisher publisher, | ||||
|     @Default({}) Map<String, int> reactionsCount, | ||||
|     @Default({}) Map<String, bool> reactionsMade, | ||||
|     @Default([]) List<dynamic> reactions, | ||||
|     @Default([]) List<PostTag> tags, | ||||
|     @Default([]) List<PostCategory> categories, | ||||
| @@ -77,6 +78,13 @@ sealed class SnSubscriptionStatus with _$SnSubscriptionStatus { | ||||
| sealed class ReactInfo with _$ReactInfo { | ||||
|   const factory ReactInfo({required String icon, required int attitude}) = | ||||
|       _ReactInfo; | ||||
|  | ||||
|   static String getTranslationKey(String templateKey) { | ||||
|     final parts = templateKey.split('_'); | ||||
|     final camelCase = | ||||
|         parts.map((p) => p[0].toUpperCase() + p.substring(1)).join(); | ||||
|     return 'reaction$camelCase'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const Map<String, ReactInfo> kReactionTemplates = { | ||||
|   | ||||
| @@ -15,7 +15,7 @@ T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$SnPost { | ||||
|  | ||||
|  String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; List<dynamic> get reactions; List<PostTag> get tags; List<PostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; | ||||
|  String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<PostTag> get tags; List<PostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; | ||||
| /// Create a copy of SnPost | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -28,16 +28,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]); | ||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; | ||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -48,7 +48,7 @@ abstract mixin class $SnPostCopyWith<$Res>  { | ||||
|   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -65,7 +65,7 @@ class _$SnPostCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnPost | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| @@ -91,7 +91,8 @@ as String?,forwardedPost: freezed == forwardedPost ? _self.forwardedPost : forwa | ||||
| as SnPost?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable | ||||
| as SnPublisher,reactionsCount: null == reactionsCount ? _self.reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, int>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, int>,reactionsMade: null == reactionsMade ? _self.reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, bool>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostTag>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostCategory>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable | ||||
| @@ -226,10 +227,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  List<dynamic> reactions,  List<PostTag> tags,  List<PostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<PostTag> tags,  List<PostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPost() when $default != null: | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -247,10 +248,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  List<dynamic> reactions,  List<PostTag> tags,  List<PostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<PostTag> tags,  List<PostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPost(): | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);} | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -264,10 +265,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  List<dynamic> reactions,  List<PostTag> tags,  List<PostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<PostTag> tags,  List<PostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPost() when $default != null: | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -279,7 +280,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnPost implements SnPost { | ||||
|   const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.type = 0, final  Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, final  List<SnCloudFile> attachments = const [], required this.publisher, final  Map<String, int> reactionsCount = const {}, final  List<dynamic> reactions = const [], final  List<PostTag> tags = const [], final  List<PostCategory> categories = const [], final  List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; | ||||
|   const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.type = 0, final  Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, final  List<SnCloudFile> attachments = const [], required this.publisher, final  Map<String, int> reactionsCount = const {}, final  Map<String, bool> reactionsMade = const {}, final  List<dynamic> reactions = const [], final  List<PostTag> tags = const [], final  List<PostCategory> categories = const [], final  List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; | ||||
|   factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @@ -326,6 +327,13 @@ class _SnPost implements SnPost { | ||||
|   return EqualUnmodifiableMapView(_reactionsCount); | ||||
| } | ||||
|  | ||||
|  final  Map<String, bool> _reactionsMade; | ||||
| @override@JsonKey() Map<String, bool> get reactionsMade { | ||||
|   if (_reactionsMade is EqualUnmodifiableMapView) return _reactionsMade; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableMapView(_reactionsMade); | ||||
| } | ||||
|  | ||||
|  final  List<dynamic> _reactions; | ||||
| @override@JsonKey() List<dynamic> get reactions { | ||||
|   if (_reactions is EqualUnmodifiableListView) return _reactions; | ||||
| @@ -372,16 +380,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]); | ||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; | ||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -392,7 +400,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> { | ||||
|   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -409,7 +417,7 @@ class __$SnPostCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnPost | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { | ||||
|   return _then(_SnPost( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| @@ -435,7 +443,8 @@ as String?,forwardedPost: freezed == forwardedPost ? _self.forwardedPost : forwa | ||||
| as SnPost?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable | ||||
| as SnPublisher,reactionsCount: null == reactionsCount ? _self._reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, int>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, int>,reactionsMade: null == reactionsMade ? _self._reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, bool>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostTag>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostCategory>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable | ||||
|   | ||||
| @@ -54,6 +54,11 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost( | ||||
|         (k, e) => MapEntry(k, (e as num).toInt()), | ||||
|       ) ?? | ||||
|       const {}, | ||||
|   reactionsMade: | ||||
|       (json['reactions_made'] as Map<String, dynamic>?)?.map( | ||||
|         (k, e) => MapEntry(k, e as bool), | ||||
|       ) ?? | ||||
|       const {}, | ||||
|   reactions: json['reactions'] as List<dynamic>? ?? const [], | ||||
|   tags: | ||||
|       (json['tags'] as List<dynamic>?) | ||||
| @@ -106,6 +111,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ | ||||
|   'attachments': instance.attachments.map((e) => e.toJson()).toList(), | ||||
|   'publisher': instance.publisher.toJson(), | ||||
|   'reactions_count': instance.reactionsCount, | ||||
|   'reactions_made': instance.reactionsMade, | ||||
|   'reactions': instance.reactions, | ||||
|   'tags': instance.tags.map((e) => e.toJson()).toList(), | ||||
|   'categories': instance.categories.map((e) => e.toJson()).toList(), | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/screens/chat/chat.dart'; | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter_webrtc/flutter_webrtc.dart'; | ||||
| import 'package:island/widgets/chat/call_button.dart'; | ||||
| import 'package:livekit_client/livekit_client.dart'; | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'dart:async'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
|  | ||||
| part 'call.g.dart'; | ||||
| part 'call.freezed.dart'; | ||||
| @@ -27,6 +28,7 @@ sealed class CallState with _$CallState { | ||||
|     required bool isMicrophoneEnabled, | ||||
|     required bool isCameraEnabled, | ||||
|     required bool isScreenSharing, | ||||
|     required bool isSpeakerphone, | ||||
|     @Default(Duration(seconds: 0)) Duration duration, | ||||
|     String? error, | ||||
|   }) = _CallState; | ||||
| @@ -42,7 +44,8 @@ sealed class CallParticipantLive with _$CallParticipantLive { | ||||
|   }) = _CallParticipantLive; | ||||
|  | ||||
|   bool get isSpeaking => remoteParticipant.isSpeaking; | ||||
|   bool get isMuted => remoteParticipant.isMuted; | ||||
|   bool get isMuted => | ||||
|       remoteParticipant.isMuted || !remoteParticipant.isMicrophoneEnabled(); | ||||
|   bool get isScreenSharing => remoteParticipant.isScreenShareEnabled(); | ||||
|   bool get isScreenSharingWithAudio => | ||||
|       remoteParticipant.isScreenShareAudioEnabled(); | ||||
| @@ -57,13 +60,14 @@ class CallNotifier extends _$CallNotifier { | ||||
|   LocalParticipant? _localParticipant; | ||||
|   List<CallParticipantLive> _participants = []; | ||||
|   final Map<String, CallParticipant> _participantInfoByIdentity = {}; | ||||
|   StreamSubscription? _wsSubscription; | ||||
|   EventsListener? _roomListener; | ||||
|  | ||||
|   List<CallParticipantLive> get participants => | ||||
|       List.unmodifiable(_participants); | ||||
|   LocalParticipant? get localParticipant => _localParticipant; | ||||
|  | ||||
|   Map<String, double> participantsVolumes = {}; | ||||
|  | ||||
|   Timer? _durationTimer; | ||||
|  | ||||
|   Room? get room => _room; | ||||
| @@ -71,36 +75,15 @@ class CallNotifier extends _$CallNotifier { | ||||
|   @override | ||||
|   CallState build() { | ||||
|     // Subscribe to websocket updates | ||||
|     _subscribeToParticipantsUpdate(); | ||||
|     return const CallState( | ||||
|       isConnected: false, | ||||
|       isMicrophoneEnabled: true, | ||||
|       isCameraEnabled: false, | ||||
|       isScreenSharing: false, | ||||
|       isSpeakerphone: true, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _subscribeToParticipantsUpdate() { | ||||
|     // Only subscribe once | ||||
|     if (_wsSubscription != null) return; | ||||
|     final ws = ref.read(websocketProvider); | ||||
|     _wsSubscription = ws.dataStream.listen((packet) { | ||||
|       if (packet.type == 'call.participants.update' && packet.data != null) { | ||||
|         final participantsData = packet.data!["participants"]; | ||||
|         if (participantsData is List) { | ||||
|           final parsed = | ||||
|               participantsData | ||||
|                   .map( | ||||
|                     (e) => | ||||
|                         CallParticipant.fromJson(Map<String, dynamic>.from(e)), | ||||
|                   ) | ||||
|                   .toList(); | ||||
|           _updateLiveParticipants(parsed); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _initRoomListeners() { | ||||
|     if (_room == null) return; | ||||
|     _roomListener?.dispose(); | ||||
| @@ -143,8 +126,6 @@ class CallNotifier extends _$CallNotifier { | ||||
|               identity: remote.identity, | ||||
|               name: remote.identity, | ||||
|               joinedAt: DateTime.now(), | ||||
|               accountId: null, | ||||
|               profile: null, | ||||
|             ); | ||||
|         return CallParticipantLive( | ||||
|           participant: match, | ||||
| @@ -169,16 +150,12 @@ class CallNotifier extends _$CallNotifier { | ||||
|       if (idx != -1) return participants[idx]; | ||||
|     } | ||||
|  | ||||
|     final userInfo = ref.read(userInfoProvider); | ||||
|     final roomIdentity = ref.read(chatroomIdentityProvider(_roomId)); | ||||
|     // Otherwise, use info from the identity map or fallback to minimal | ||||
|     return _participantInfoByIdentity[_localParticipant!.identity] ?? | ||||
|         CallParticipant( | ||||
|           identity: _localParticipant!.identity, | ||||
|           name: _localParticipant!.identity, | ||||
|           joinedAt: DateTime.now(), | ||||
|           accountId: userInfo.value?.id, | ||||
|           profile: roomIdentity.value, | ||||
|         ); | ||||
|   } | ||||
|  | ||||
| @@ -205,6 +182,7 @@ class CallNotifier extends _$CallNotifier { | ||||
|           remoteParticipant: _localParticipant!, | ||||
|         ), | ||||
|       ); | ||||
|       state = state.copyWith(); | ||||
|     } | ||||
|     // Add remote participants | ||||
|     _participants.addAll( | ||||
| @@ -233,7 +211,13 @@ class CallNotifier extends _$CallNotifier { | ||||
|  | ||||
|   Future<void> joinRoom(String roomId) async { | ||||
|     if (_roomId == roomId && _room != null) { | ||||
|       log('[Call] Call skipped. Already has data'); | ||||
|       return; | ||||
|     } else if (_room != null) { | ||||
|       if (!_room!.isDisposed && | ||||
|           _room!.connectionState != ConnectionState.disconnected) { | ||||
|         throw Exception('Call already connected'); | ||||
|       } | ||||
|     } | ||||
|     _roomId = roomId; | ||||
|     if (_room != null) { | ||||
| @@ -264,7 +248,8 @@ class CallNotifier extends _$CallNotifier { | ||||
|             duration: Duration( | ||||
|               milliseconds: | ||||
|                   (DateTime.now().millisecondsSinceEpoch - | ||||
|                       (ongoingCall?.createdAt.millisecondsSinceEpoch ?? 0)), | ||||
|                       (ongoingCall?.createdAt.millisecondsSinceEpoch ?? | ||||
|                           DateTime.now().millisecondsSinceEpoch)), | ||||
|             ), | ||||
|           ); | ||||
|         }); | ||||
| @@ -286,6 +271,10 @@ class CallNotifier extends _$CallNotifier { | ||||
|         _initRoomListeners(); | ||||
|         _updateLiveParticipants(participants); | ||||
|  | ||||
|         if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { | ||||
|           Hardware.instance.setSpeakerphoneOn(true); | ||||
|         } | ||||
|  | ||||
|         // Listen for connection updates | ||||
|         _room!.addListener(() { | ||||
|           state = state.copyWith( | ||||
| @@ -318,6 +307,7 @@ class CallNotifier extends _$CallNotifier { | ||||
|           stopOnMute: autostop, | ||||
|         ); | ||||
|       } | ||||
|       state = state.copyWith(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -326,6 +316,7 @@ class CallNotifier extends _$CallNotifier { | ||||
|       final target = !_localParticipant!.isCameraEnabled(); | ||||
|       state = state.copyWith(isCameraEnabled: target); | ||||
|       await _localParticipant!.setCameraEnabled(target); | ||||
|       state = state.copyWith(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -334,9 +325,16 @@ class CallNotifier extends _$CallNotifier { | ||||
|       final target = !_localParticipant!.isScreenShareEnabled(); | ||||
|       state = state.copyWith(isScreenSharing: target); | ||||
|       await _localParticipant!.setScreenShareEnabled(target); | ||||
|       state = state.copyWith(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> toggleSpeakerphone() async { | ||||
|     state = state.copyWith(isSpeakerphone: !state.isSpeakerphone); | ||||
|     await Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone); | ||||
|     state = state.copyWith(); | ||||
|   } | ||||
|  | ||||
|   Future<void> disconnect() async { | ||||
|     if (_room != null) { | ||||
|       await _room!.disconnect(); | ||||
| @@ -349,11 +347,39 @@ class CallNotifier extends _$CallNotifier { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void setParticipantVolume(CallParticipantLive live, double volume) { | ||||
|     if (participantsVolumes[live.remoteParticipant.sid] == null) { | ||||
|       participantsVolumes[live.remoteParticipant.sid] = 1; | ||||
|     } | ||||
|     Helper.setVolume( | ||||
|       volume, | ||||
|       live | ||||
|           .remoteParticipant | ||||
|           .audioTrackPublications | ||||
|           .first | ||||
|           .track! | ||||
|           .mediaStreamTrack, | ||||
|     ); | ||||
|     participantsVolumes[live.remoteParticipant.sid] = volume; | ||||
|   } | ||||
|  | ||||
|   double getParticipantVolume(CallParticipantLive live) { | ||||
|     return participantsVolumes[live.remoteParticipant.sid] ?? 1; | ||||
|   } | ||||
|  | ||||
|   void dispose() { | ||||
|     _wsSubscription?.cancel(); | ||||
|     state = state.copyWith( | ||||
|       error: null, | ||||
|       isConnected: false, | ||||
|       isMicrophoneEnabled: false, | ||||
|       isCameraEnabled: false, | ||||
|       isScreenSharing: false, | ||||
|     ); | ||||
|     _roomListener?.dispose(); | ||||
|     _room?.removeListener(_onRoomChange); | ||||
|     _room?.dispose(); | ||||
|     _durationTimer?.cancel(); | ||||
|     _roomId = null; | ||||
|     participantsVolumes = {}; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -12,9 +12,9 @@ part of 'call.dart'; | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$CallState { | ||||
| mixin _$CallState implements DiagnosticableTreeMixin { | ||||
|  | ||||
|  bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; Duration get duration; String? get error; | ||||
|  bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; bool get isSpeakerphone; Duration get duration; String? get error; | ||||
| /// Create a copy of CallState | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -22,19 +22,25 @@ mixin _$CallState { | ||||
| $CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>(this as CallState, _$identity); | ||||
|  | ||||
|  | ||||
| @override | ||||
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | ||||
|   properties | ||||
|     ..add(DiagnosticsProperty('type', 'CallState')) | ||||
|     ..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('error', error)); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error)); | ||||
| } | ||||
|  | ||||
|  | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error); | ||||
| int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)'; | ||||
| String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { | ||||
|   return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -45,7 +51,7 @@ abstract mixin class $CallStateCopyWith<$Res>  { | ||||
|   factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error | ||||
|  bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -62,12 +68,13 @@ class _$CallStateCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of CallState | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? error = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isSpeakerphone: null == isSpeakerphone ? _self.isSpeakerphone : isSpeakerphone // ignore: cast_nullable_to_non_nullable | ||||
| as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable | ||||
| as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable | ||||
| as String?, | ||||
| @@ -152,10 +159,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isConnected,  bool isMicrophoneEnabled,  bool isCameraEnabled,  bool isScreenSharing,  Duration duration,  String? error)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isConnected,  bool isMicrophoneEnabled,  bool isCameraEnabled,  bool isScreenSharing,  bool isSpeakerphone,  Duration duration,  String? error)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CallState() when $default != null: | ||||
| return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.duration,_that.error);case _: | ||||
| return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -173,10 +180,10 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnected,  bool isMicrophoneEnabled,  bool isCameraEnabled,  bool isScreenSharing,  Duration duration,  String? error)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnected,  bool isMicrophoneEnabled,  bool isCameraEnabled,  bool isScreenSharing,  bool isSpeakerphone,  Duration duration,  String? error)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CallState(): | ||||
| return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.duration,_that.error);} | ||||
| return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -190,10 +197,10 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isConnected,  bool isMicrophoneEnabled,  bool isCameraEnabled,  bool isScreenSharing,  Duration duration,  String? error)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isConnected,  bool isMicrophoneEnabled,  bool isCameraEnabled,  bool isScreenSharing,  bool isSpeakerphone,  Duration duration,  String? error)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CallState() when $default != null: | ||||
| return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.duration,_that.error);case _: | ||||
| return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -204,14 +211,15 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable | ||||
| /// @nodoc | ||||
|  | ||||
|  | ||||
| class _CallState implements CallState { | ||||
|   const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, this.duration = const Duration(seconds: 0), this.error}); | ||||
| class _CallState with DiagnosticableTreeMixin implements CallState { | ||||
|   const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, required this.isSpeakerphone, this.duration = const Duration(seconds: 0), this.error}); | ||||
|    | ||||
|  | ||||
| @override final  bool isConnected; | ||||
| @override final  bool isMicrophoneEnabled; | ||||
| @override final  bool isCameraEnabled; | ||||
| @override final  bool isScreenSharing; | ||||
| @override final  bool isSpeakerphone; | ||||
| @override@JsonKey() final  Duration duration; | ||||
| @override final  String? error; | ||||
|  | ||||
| @@ -222,19 +230,25 @@ class _CallState implements CallState { | ||||
| _$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallState>(this, _$identity); | ||||
|  | ||||
|  | ||||
| @override | ||||
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | ||||
|   properties | ||||
|     ..add(DiagnosticsProperty('type', 'CallState')) | ||||
|     ..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('error', error)); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error)); | ||||
| } | ||||
|  | ||||
|  | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error); | ||||
| int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)'; | ||||
| String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { | ||||
|   return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -245,7 +259,7 @@ abstract mixin class _$CallStateCopyWith<$Res> implements $CallStateCopyWith<$Re | ||||
|   factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error | ||||
|  bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -262,12 +276,13 @@ class __$CallStateCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of CallState | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? error = freezed,}) { | ||||
|   return _then(_CallState( | ||||
| isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable | ||||
| as bool,isSpeakerphone: null == isSpeakerphone ? _self.isSpeakerphone : isSpeakerphone // ignore: cast_nullable_to_non_nullable | ||||
| as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable | ||||
| as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable | ||||
| as String?, | ||||
| @@ -278,7 +293,7 @@ as String?, | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$CallParticipantLive { | ||||
| mixin _$CallParticipantLive implements DiagnosticableTreeMixin { | ||||
|  | ||||
|  CallParticipant get participant; Participant get remoteParticipant; | ||||
| /// Create a copy of CallParticipantLive | ||||
| @@ -288,6 +303,12 @@ mixin _$CallParticipantLive { | ||||
| $CallParticipantLiveCopyWith<CallParticipantLive> get copyWith => _$CallParticipantLiveCopyWithImpl<CallParticipantLive>(this as CallParticipantLive, _$identity); | ||||
|  | ||||
|  | ||||
| @override | ||||
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | ||||
|   properties | ||||
|     ..add(DiagnosticsProperty('type', 'CallParticipantLive')) | ||||
|     ..add(DiagnosticsProperty('participant', participant))..add(DiagnosticsProperty('remoteParticipant', remoteParticipant)); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
| @@ -299,7 +320,7 @@ bool operator ==(Object other) { | ||||
| int get hashCode => Object.hash(runtimeType,participant,remoteParticipant); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
| String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { | ||||
|   return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)'; | ||||
| } | ||||
|  | ||||
| @@ -475,7 +496,7 @@ return $default(_that.participant,_that.remoteParticipant);case _: | ||||
| /// @nodoc | ||||
|  | ||||
|  | ||||
| class _CallParticipantLive extends CallParticipantLive { | ||||
| class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMixin { | ||||
|   const _CallParticipantLive({required this.participant, required this.remoteParticipant}): super._(); | ||||
|    | ||||
|  | ||||
| @@ -489,6 +510,12 @@ class _CallParticipantLive extends CallParticipantLive { | ||||
| _$CallParticipantLiveCopyWith<_CallParticipantLive> get copyWith => __$CallParticipantLiveCopyWithImpl<_CallParticipantLive>(this, _$identity); | ||||
|  | ||||
|  | ||||
| @override | ||||
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | ||||
|   properties | ||||
|     ..add(DiagnosticsProperty('type', 'CallParticipantLive')) | ||||
|     ..add(DiagnosticsProperty('participant', participant))..add(DiagnosticsProperty('remoteParticipant', remoteParticipant)); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
| @@ -500,7 +527,7 @@ bool operator ==(Object other) { | ||||
| int get hashCode => Object.hash(runtimeType,participant,remoteParticipant); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
| String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { | ||||
|   return 'CallParticipantLive(participant: $participant, remoteParticipant: $remoteParticipant)'; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'call.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$callNotifierHash() => r'107174cd6cfab6bfafe44f8c4a72a67bcb93217b'; | ||||
| String _$callNotifierHash() => r'333a1cd566a339644c83932e15dae03f1c5cc24b'; | ||||
|  | ||||
| /// See also [CallNotifier]. | ||||
| @ProviderFor(CallNotifier) | ||||
|   | ||||
							
								
								
									
										38
									
								
								lib/pods/translate.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/pods/translate.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; | ||||
|  | ||||
| part 'translate.freezed.dart'; | ||||
| part 'translate.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class TranslateQuery with _$TranslateQuery { | ||||
|   const factory TranslateQuery({required String text, required String lang}) = | ||||
|       _TranslateQuery; | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<String> translateString(Ref ref, TranslateQuery query) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   final response = await client.post( | ||||
|     '/sphere/translate', | ||||
|     queryParameters: {'to': query.lang}, | ||||
|     data: jsonEncode(query.text), | ||||
|   ); | ||||
|   return response.data as String; | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| String? detectStringLanguage(Ref ref, String text) { | ||||
|   try { | ||||
|     return langdetect.detectLangs(text).firstOrNull?.lang; | ||||
|   } catch (err) { | ||||
|     log('[Language] Unable to detect text\'s language: $text'); | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										268
									
								
								lib/pods/translate.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								lib/pods/translate.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | ||||
| // 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 'translate.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$TranslateQuery { | ||||
|  | ||||
|  String get text; String get lang; | ||||
| /// Create a copy of TranslateQuery | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $TranslateQueryCopyWith<TranslateQuery> get copyWith => _$TranslateQueryCopyWithImpl<TranslateQuery>(this as TranslateQuery, _$identity); | ||||
|  | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is TranslateQuery&&(identical(other.text, text) || other.text == text)&&(identical(other.lang, lang) || other.lang == lang)); | ||||
| } | ||||
|  | ||||
|  | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,text,lang); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'TranslateQuery(text: $text, lang: $lang)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $TranslateQueryCopyWith<$Res>  { | ||||
|   factory $TranslateQueryCopyWith(TranslateQuery value, $Res Function(TranslateQuery) _then) = _$TranslateQueryCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String text, String lang | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$TranslateQueryCopyWithImpl<$Res> | ||||
|     implements $TranslateQueryCopyWith<$Res> { | ||||
|   _$TranslateQueryCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final TranslateQuery _self; | ||||
|   final $Res Function(TranslateQuery) _then; | ||||
|  | ||||
| /// Create a copy of TranslateQuery | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? text = null,Object? lang = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable | ||||
| as String,lang: null == lang ? _self.lang : lang // ignore: cast_nullable_to_non_nullable | ||||
| as String, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [TranslateQuery]. | ||||
| extension TranslateQueryPatterns on TranslateQuery { | ||||
| /// 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( _TranslateQuery value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _TranslateQuery() 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( _TranslateQuery value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _TranslateQuery(): | ||||
| 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( _TranslateQuery value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _TranslateQuery() 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 text,  String lang)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _TranslateQuery() when $default != null: | ||||
| return $default(_that.text,_that.lang);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 text,  String lang)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _TranslateQuery(): | ||||
| return $default(_that.text,_that.lang);} | ||||
| } | ||||
| /// 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 text,  String lang)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _TranslateQuery() when $default != null: | ||||
| return $default(_that.text,_that.lang);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
|  | ||||
|  | ||||
| class _TranslateQuery implements TranslateQuery { | ||||
|   const _TranslateQuery({required this.text, required this.lang}); | ||||
|    | ||||
|  | ||||
| @override final  String text; | ||||
| @override final  String lang; | ||||
|  | ||||
| /// Create a copy of TranslateQuery | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$TranslateQueryCopyWith<_TranslateQuery> get copyWith => __$TranslateQueryCopyWithImpl<_TranslateQuery>(this, _$identity); | ||||
|  | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _TranslateQuery&&(identical(other.text, text) || other.text == text)&&(identical(other.lang, lang) || other.lang == lang)); | ||||
| } | ||||
|  | ||||
|  | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,text,lang); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'TranslateQuery(text: $text, lang: $lang)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$TranslateQueryCopyWith<$Res> implements $TranslateQueryCopyWith<$Res> { | ||||
|   factory _$TranslateQueryCopyWith(_TranslateQuery value, $Res Function(_TranslateQuery) _then) = __$TranslateQueryCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String text, String lang | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$TranslateQueryCopyWithImpl<$Res> | ||||
|     implements _$TranslateQueryCopyWith<$Res> { | ||||
|   __$TranslateQueryCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _TranslateQuery _self; | ||||
|   final $Res Function(_TranslateQuery) _then; | ||||
|  | ||||
| /// Create a copy of TranslateQuery | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? text = null,Object? lang = null,}) { | ||||
|   return _then(_TranslateQuery( | ||||
| text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable | ||||
| as String,lang: null == lang ? _self.lang : lang // ignore: cast_nullable_to_non_nullable | ||||
| as String, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										274
									
								
								lib/pods/translate.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								lib/pods/translate.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,274 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'translate.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$translateStringHash() => r'51d638cf07cbf3ffa9469298f5bd9c667bc0ccb7'; | ||||
|  | ||||
| /// 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 [translateString]. | ||||
| @ProviderFor(translateString) | ||||
| const translateStringProvider = TranslateStringFamily(); | ||||
|  | ||||
| /// See also [translateString]. | ||||
| class TranslateStringFamily extends Family<AsyncValue<String>> { | ||||
|   /// See also [translateString]. | ||||
|   const TranslateStringFamily(); | ||||
|  | ||||
|   /// See also [translateString]. | ||||
|   TranslateStringProvider call(TranslateQuery query) { | ||||
|     return TranslateStringProvider(query); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   TranslateStringProvider getProviderOverride( | ||||
|     covariant TranslateStringProvider provider, | ||||
|   ) { | ||||
|     return call(provider.query); | ||||
|   } | ||||
|  | ||||
|   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'translateStringProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [translateString]. | ||||
| class TranslateStringProvider extends AutoDisposeFutureProvider<String> { | ||||
|   /// See also [translateString]. | ||||
|   TranslateStringProvider(TranslateQuery query) | ||||
|     : this._internal( | ||||
|         (ref) => translateString(ref as TranslateStringRef, query), | ||||
|         from: translateStringProvider, | ||||
|         name: r'translateStringProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$translateStringHash, | ||||
|         dependencies: TranslateStringFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             TranslateStringFamily._allTransitiveDependencies, | ||||
|         query: query, | ||||
|       ); | ||||
|  | ||||
|   TranslateStringProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.query, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final TranslateQuery query; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<String> Function(TranslateStringRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: TranslateStringProvider._internal( | ||||
|         (ref) => create(ref as TranslateStringRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         query: query, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<String> createElement() { | ||||
|     return _TranslateStringProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is TranslateStringProvider && other.query == query; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, query.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin TranslateStringRef on AutoDisposeFutureProviderRef<String> { | ||||
|   /// The parameter `query` of this provider. | ||||
|   TranslateQuery get query; | ||||
| } | ||||
|  | ||||
| class _TranslateStringProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<String> | ||||
|     with TranslateStringRef { | ||||
|   _TranslateStringProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   TranslateQuery get query => (origin as TranslateStringProvider).query; | ||||
| } | ||||
|  | ||||
| String _$detectStringLanguageHash() => | ||||
|     r'697b68464b3d00927cc43ccc1ba8ba93f2a470ed'; | ||||
|  | ||||
| /// See also [detectStringLanguage]. | ||||
| @ProviderFor(detectStringLanguage) | ||||
| const detectStringLanguageProvider = DetectStringLanguageFamily(); | ||||
|  | ||||
| /// See also [detectStringLanguage]. | ||||
| class DetectStringLanguageFamily extends Family<String?> { | ||||
|   /// See also [detectStringLanguage]. | ||||
|   const DetectStringLanguageFamily(); | ||||
|  | ||||
|   /// See also [detectStringLanguage]. | ||||
|   DetectStringLanguageProvider call(String text) { | ||||
|     return DetectStringLanguageProvider(text); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   DetectStringLanguageProvider getProviderOverride( | ||||
|     covariant DetectStringLanguageProvider provider, | ||||
|   ) { | ||||
|     return call(provider.text); | ||||
|   } | ||||
|  | ||||
|   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'detectStringLanguageProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [detectStringLanguage]. | ||||
| class DetectStringLanguageProvider extends AutoDisposeProvider<String?> { | ||||
|   /// See also [detectStringLanguage]. | ||||
|   DetectStringLanguageProvider(String text) | ||||
|     : this._internal( | ||||
|         (ref) => detectStringLanguage(ref as DetectStringLanguageRef, text), | ||||
|         from: detectStringLanguageProvider, | ||||
|         name: r'detectStringLanguageProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$detectStringLanguageHash, | ||||
|         dependencies: DetectStringLanguageFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             DetectStringLanguageFamily._allTransitiveDependencies, | ||||
|         text: text, | ||||
|       ); | ||||
|  | ||||
|   DetectStringLanguageProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.text, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String text; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     String? Function(DetectStringLanguageRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: DetectStringLanguageProvider._internal( | ||||
|         (ref) => create(ref as DetectStringLanguageRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         text: text, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeProviderElement<String?> createElement() { | ||||
|     return _DetectStringLanguageProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is DetectStringLanguageProvider && other.text == text; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, text.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin DetectStringLanguageRef on AutoDisposeProviderRef<String?> { | ||||
|   /// The parameter `text` of this provider. | ||||
|   String get text; | ||||
| } | ||||
|  | ||||
| class _DetectStringLanguageProviderElement | ||||
|     extends AutoDisposeProviderElement<String?> | ||||
|     with DetectStringLanguageRef { | ||||
|   _DetectStringLanguageProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get text => (origin as DetectStringLanguageProvider).text; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| @@ -46,6 +46,10 @@ class WebSocketService { | ||||
|   final StreamController<WebSocketState> _statusStreamController = | ||||
|       StreamController<WebSocketState>.broadcast(); | ||||
|   Timer? _reconnectTimer; | ||||
|   Timer? _heartbeatTimer; | ||||
|  | ||||
|   DateTime? _heartbeatAt; | ||||
|   Duration? _heartbeatDelay; | ||||
|  | ||||
|   Stream<WebSocketPacket> get dataStream => _streamController.stream; | ||||
|   Stream<WebSocketState> get statusStream => _statusStreamController.stream; | ||||
| @@ -71,6 +75,7 @@ class WebSocketService { | ||||
|       } | ||||
|       await _channel!.ready; | ||||
|       _statusStreamController.sink.add(WebSocketState.connected()); | ||||
|       _scheduleHeartbeat(); | ||||
|       _channel!.stream.listen( | ||||
|         (data) { | ||||
|           final dataStr = | ||||
| @@ -80,6 +85,13 @@ class WebSocketService { | ||||
|           log( | ||||
|             "[WebSocket] Received packet: ${packet.type} ${packet.errorMessage}", | ||||
|           ); | ||||
|           if (packet.type == 'pong' && _heartbeatAt != null) { | ||||
|             var now = DateTime.now(); | ||||
|             _heartbeatDelay = now.difference(_heartbeatAt!); | ||||
|             log( | ||||
|               "[WebSocket] Server respond last heartbeat for ${_heartbeatDelay!.inMilliseconds} ms", | ||||
|             ); | ||||
|           } | ||||
|         }, | ||||
|         onDone: () { | ||||
|           log('[WebSocket] Connection closed, attempting to reconnect...'); | ||||
| @@ -108,6 +120,19 @@ class WebSocketService { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _scheduleHeartbeat() { | ||||
|     _heartbeatTimer?.cancel(); | ||||
|     _heartbeatTimer = Timer.periodic(const Duration(seconds: 60), (_) { | ||||
|       _beatTheHeart(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _beatTheHeart() { | ||||
|     _heartbeatAt = DateTime.now(); | ||||
|     log('[WebSocket] We\'re beating the heart! $_heartbeatAt'); | ||||
|     sendMessage(jsonEncode(WebSocketPacket(type: 'ping', data: null))); | ||||
|   } | ||||
|  | ||||
|   WebSocketChannel? get ws => _channel; | ||||
|  | ||||
|   void sendMessage(String message) { | ||||
|   | ||||
| @@ -287,12 +287,6 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|             builder: (context, state) => const AboutScreen(), | ||||
|           ), | ||||
|  | ||||
|           GoRoute( | ||||
|             name: 'reportList', | ||||
|             path: '/safety/reports/me', | ||||
|             builder: (context, state) => const AbuseReportListScreen(), | ||||
|           ), | ||||
|  | ||||
|           GoRoute( | ||||
|             name: 'reportDetail', | ||||
|             path: '/safety/reports/me/:id', | ||||
| @@ -439,14 +433,6 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                     path: '/account/relationships', | ||||
|                     builder: (context, state) => const RelationshipScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'accountProfile', | ||||
|                     path: '/account/:name', | ||||
|                     builder: (context, state) { | ||||
|                       final name = state.pathParameters['name']!; | ||||
|                       return AccountProfileScreen(name: name); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'profileUpdate', | ||||
|                     path: '/account/me/update', | ||||
| @@ -462,8 +448,22 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                     path: '/account/me/settings', | ||||
|                     builder: (context, state) => const AccountSettingsScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'reportList', | ||||
|                     path: '/safety/reports/me', | ||||
|                     builder: (context, state) => const AbuseReportListScreen(), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|  | ||||
|               GoRoute( | ||||
|                 name: 'accountProfile', | ||||
|                 path: '/account/:name', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return AccountProfileScreen(name: name); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|   | ||||
| @@ -93,6 +93,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> { | ||||
|     final theme = Theme.of(context); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar(title: Text('about'.tr()), elevation: 0), | ||||
|       body: | ||||
|           _isLoading | ||||
|   | ||||
| @@ -64,7 +64,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: isWide, | ||||
|       isNoBackground: isWide, | ||||
|       appBar: AppBar(backgroundColor: Colors.transparent, toolbarHeight: 0), | ||||
|       body: SingleChildScrollView( | ||||
|         padding: getTabbedPadding(context), | ||||
| @@ -231,7 +231,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               title: Text('abuseReports').tr(), | ||||
|               contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|               leading: const Icon(Symbols.gavel), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               onTap: () => context.pushNamed('reportList'), | ||||
|   | ||||
| @@ -46,7 +46,7 @@ class EventCalanderScreen extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('eventCalander').tr(), | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'leveling.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$accountStellarSubscriptionHash() => | ||||
|     r'37fb821460e3ac50b5cf777c933b6779f732daee'; | ||||
|     r'80abcdefb3868775fd8fe3c980215713efff5948'; | ||||
|  | ||||
| /// See also [accountStellarSubscription]. | ||||
| @ProviderFor(accountStellarSubscription) | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import 'package:island/pods/event_calendar.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/services/color.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/services/timezone/native.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| @@ -248,294 +249,367 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|  | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|  | ||||
|     Widget accountBasicInfo(SnAccount data) => Padding( | ||||
|       padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), | ||||
|       child: Row( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           ProfilePictureWidget(file: data.profile.picture, radius: 32), | ||||
|           const Gap(20), | ||||
|           Expanded( | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   children: [ | ||||
|                     AccountName(account: data, style: TextStyle(fontSize: 20)), | ||||
|                     const Gap(6), | ||||
|                     Text('@${data.name}').fontSize(14).opacity(0.85), | ||||
|                   ], | ||||
|                 ), | ||||
|                 AccountStatusWidget(uname: name, padding: EdgeInsets.zero), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     Widget accountProfileDetail(SnAccount data) => Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|       spacing: 24, | ||||
|       children: [ | ||||
|         if (buildSubcolumn(data).isNotEmpty) | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             spacing: 2, | ||||
|             children: buildSubcolumn(data), | ||||
|           ), | ||||
|         Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Text('bio').tr().bold(), | ||||
|             Text( | ||||
|               data.profile.bio.isEmpty | ||||
|                   ? 'descriptionNone'.tr() | ||||
|                   : data.profile.bio, | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         if (data.profile.timeZone.isNotEmpty) | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Text('timeZone').tr().bold(), | ||||
|               Row( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.baseline, | ||||
|                 textBaseline: TextBaseline.alphabetic, | ||||
|                 spacing: 6, | ||||
|                 children: [ | ||||
|                   Text(data.profile.timeZone), | ||||
|                   Text( | ||||
|                     getTzInfo( | ||||
|                       data.profile.timeZone, | ||||
|                     ).$2.formatCustomGlobal('HH:mm'), | ||||
|                   ), | ||||
|                   Text( | ||||
|                     getTzInfo(data.profile.timeZone).$1.formatOffsetLocal(), | ||||
|                   ).fontSize(11), | ||||
|                   Text( | ||||
|                     'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}', | ||||
|                   ).fontSize(11).opacity(0.75), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|       ], | ||||
|     ).padding(horizontal: 24); | ||||
|  | ||||
|     Widget accountAction(SnAccount data) => Card( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           Row( | ||||
|             spacing: 8, | ||||
|             children: [ | ||||
|               if (accountRelationship.value == null || | ||||
|                   accountRelationship.value!.status > -100) | ||||
|                 Expanded( | ||||
|                   child: FilledButton.icon( | ||||
|                     style: ButtonStyle( | ||||
|                       backgroundColor: WidgetStatePropertyAll( | ||||
|                         accountRelationship.value == null | ||||
|                             ? null | ||||
|                             : Theme.of(context).colorScheme.secondary, | ||||
|                       ), | ||||
|                       foregroundColor: WidgetStatePropertyAll( | ||||
|                         accountRelationship.value == null | ||||
|                             ? null | ||||
|                             : Theme.of(context).colorScheme.onSecondary, | ||||
|                       ), | ||||
|                     ), | ||||
|                     onPressed: relationshipAction, | ||||
|                     label: | ||||
|                         Text( | ||||
|                           accountRelationship.value == null | ||||
|                               ? 'addFriendShort' | ||||
|                               : 'added', | ||||
|                         ).tr(), | ||||
|                     icon: | ||||
|                         accountRelationship.value == null | ||||
|                             ? const Icon(Symbols.person_add) | ||||
|                             : const Icon(Symbols.person_check), | ||||
|                   ), | ||||
|                 ), | ||||
|               if (accountRelationship.value == null || | ||||
|                   accountRelationship.value!.status <= -100) | ||||
|                 Expanded( | ||||
|                   child: FilledButton.icon( | ||||
|                     style: ButtonStyle( | ||||
|                       backgroundColor: WidgetStatePropertyAll( | ||||
|                         accountRelationship.value == null | ||||
|                             ? null | ||||
|                             : Theme.of(context).colorScheme.secondary, | ||||
|                       ), | ||||
|                       foregroundColor: WidgetStatePropertyAll( | ||||
|                         accountRelationship.value == null | ||||
|                             ? null | ||||
|                             : Theme.of(context).colorScheme.onSecondary, | ||||
|                       ), | ||||
|                     ), | ||||
|                     onPressed: blockAction, | ||||
|                     label: | ||||
|                         Text( | ||||
|                           accountRelationship.value == null | ||||
|                               ? 'blockUser' | ||||
|                               : 'unblockUser', | ||||
|                         ).tr(), | ||||
|                     icon: | ||||
|                         accountRelationship.value == null | ||||
|                             ? const Icon(Symbols.block) | ||||
|                             : const Icon(Symbols.person_cancel), | ||||
|                   ), | ||||
|                 ), | ||||
|             ], | ||||
|           ).padding(horizontal: 16), | ||||
|           Row( | ||||
|             spacing: 8, | ||||
|             children: [ | ||||
|               Expanded( | ||||
|                 child: FilledButton.icon( | ||||
|                   onPressed: directMessageAction, | ||||
|                   icon: const Icon(Symbols.message), | ||||
|                   label: | ||||
|                       Text( | ||||
|                         accountChat.value == null | ||||
|                             ? 'createDirectMessage' | ||||
|                             : 'gotoDirectMessage', | ||||
|                         maxLines: 1, | ||||
|                       ).tr(), | ||||
|                 ), | ||||
|               ), | ||||
|               IconButton.filled( | ||||
|                 onPressed: () { | ||||
|                   showAbuseReportSheet( | ||||
|                     context, | ||||
|                     resourceIdentifier: 'account/${data.id}', | ||||
|                   ); | ||||
|                 }, | ||||
|                 icon: Icon( | ||||
|                   Symbols.flag, | ||||
|                   color: Theme.of(context).colorScheme.onError, | ||||
|                 ), | ||||
|                 style: ButtonStyle( | ||||
|                   backgroundColor: WidgetStatePropertyAll( | ||||
|                     Theme.of(context).colorScheme.error, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ).padding(horizontal: 16, vertical: 8), | ||||
|     ); | ||||
|  | ||||
|     return account.when( | ||||
|       data: | ||||
|           (data) => AppScaffold( | ||||
|             body: CustomScrollView( | ||||
|               slivers: [ | ||||
|                 SliverAppBar( | ||||
|                   foregroundColor: appbarColor.value, | ||||
|                   expandedHeight: 180, | ||||
|                   pinned: true, | ||||
|                   leading: PageBackButton( | ||||
|                     color: appbarColor.value, | ||||
|                     shadows: [appbarShadow], | ||||
|                   ), | ||||
|                   flexibleSpace: Stack( | ||||
|                     children: [ | ||||
|                       Positioned.fill( | ||||
|                         child: | ||||
|                             data.profile.background?.id != null | ||||
|                                 ? CloudImageWidget( | ||||
|                                   file: data.profile.background, | ||||
|                                 ) | ||||
|                                 : Container( | ||||
|                                   color: | ||||
|                                       Theme.of( | ||||
|                                         context, | ||||
|                                       ).appBarTheme.backgroundColor, | ||||
|                                 ), | ||||
|             isNoBackground: false, | ||||
|             appBar: | ||||
|                 isWideScreen(context) | ||||
|                     ? AppBar( | ||||
|                       foregroundColor: appbarColor.value, | ||||
|                       leading: PageBackButton( | ||||
|                         color: appbarColor.value, | ||||
|                         shadows: [appbarShadow], | ||||
|                       ), | ||||
|                       FlexibleSpaceBar( | ||||
|                         title: Text( | ||||
|                           data.nick, | ||||
|                           style: TextStyle( | ||||
|                             color: | ||||
|                                 appbarColor.value ?? | ||||
|                                 Theme.of(context).appBarTheme.foregroundColor, | ||||
|                             shadows: [appbarShadow], | ||||
|                       flexibleSpace: Stack( | ||||
|                         children: [ | ||||
|                           Positioned.fill( | ||||
|                             child: | ||||
|                                 data.profile.background?.id != null | ||||
|                                     ? CloudImageWidget( | ||||
|                                       file: data.profile.background, | ||||
|                                     ) | ||||
|                                     : Container( | ||||
|                                       color: | ||||
|                                           Theme.of( | ||||
|                                             context, | ||||
|                                           ).appBarTheme.backgroundColor, | ||||
|                                     ), | ||||
|                           ), | ||||
|                           FlexibleSpaceBar( | ||||
|                             title: Text( | ||||
|                               data.nick, | ||||
|                               style: TextStyle( | ||||
|                                 color: | ||||
|                                     appbarColor.value ?? | ||||
|                                     Theme.of( | ||||
|                                       context, | ||||
|                                     ).appBarTheme.foregroundColor, | ||||
|                                 shadows: [appbarShadow], | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ) | ||||
|                     : null, | ||||
|             body: | ||||
|                 isWideScreen(context) | ||||
|                     ? Row( | ||||
|                       children: [ | ||||
|                         Flexible( | ||||
|                           child: CustomScrollView( | ||||
|                             slivers: [ | ||||
|                               SliverToBoxAdapter(child: accountBasicInfo(data)), | ||||
|                               if (data.badges.isNotEmpty) | ||||
|                                 SliverToBoxAdapter( | ||||
|                                   child: BadgeList( | ||||
|                                     badges: data.badges, | ||||
|                                   ).padding(horizontal: 24, bottom: 24), | ||||
|                                 ), | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: Column( | ||||
|                                   spacing: 12, | ||||
|                                   children: [ | ||||
|                                     LevelingProgressCard( | ||||
|                                       level: data.profile.level, | ||||
|                                       experience: data.profile.experience, | ||||
|                                       progress: data.profile.levelingProgress, | ||||
|                                     ), | ||||
|                                     if (data.profile.verification != null) | ||||
|                                       VerificationStatusCard( | ||||
|                                         mark: data.profile.verification!, | ||||
|                                       ), | ||||
|                                   ], | ||||
|                                 ).padding(horizontal: 20), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Padding( | ||||
|                     padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), | ||||
|                     child: Row( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         ProfilePictureWidget( | ||||
|                           file: data.profile.picture, | ||||
|                           radius: 32, | ||||
|                         ), | ||||
|                         const Gap(20), | ||||
|                         Expanded( | ||||
|                           child: Column( | ||||
|                             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                             children: [ | ||||
|                               Row( | ||||
|                                 children: [ | ||||
|                                   AccountName( | ||||
|                                     account: data, | ||||
|                                     style: TextStyle(fontSize: 20), | ||||
|                                   ), | ||||
|                                   const Gap(6), | ||||
|                                   Text( | ||||
|                                     '@${data.name}', | ||||
|                                   ).fontSize(14).opacity(0.85), | ||||
|                                 ], | ||||
|                         Flexible( | ||||
|                           child: CustomScrollView( | ||||
|                             slivers: [ | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: accountProfileDetail(data), | ||||
|                               ), | ||||
|                               AccountStatusWidget( | ||||
|                                 uname: name, | ||||
|                                 padding: EdgeInsets.zero, | ||||
|  | ||||
|                               if (user.value != null) | ||||
|                                 SliverToBoxAdapter(child: accountAction(data)), | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: Card( | ||||
|                                   child: FortuneGraphWidget( | ||||
|                                     events: accountEvents, | ||||
|                                     eventCalanderUser: data.name, | ||||
|                                   ), | ||||
|                                 ).padding(all: 8), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 if (data.badges.isNotEmpty) | ||||
|                   SliverToBoxAdapter( | ||||
|                     child: BadgeList( | ||||
|                       badges: data.badges, | ||||
|                     ).padding(horizontal: 24, bottom: 24), | ||||
|                   ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Column( | ||||
|                     spacing: 12, | ||||
|                     children: [ | ||||
|                       LevelingProgressCard( | ||||
|                         level: data.profile.level, | ||||
|                         experience: data.profile.experience, | ||||
|                         progress: data.profile.levelingProgress, | ||||
|                       ), | ||||
|                       if (data.profile.verification != null) | ||||
|                         VerificationStatusCard( | ||||
|                           mark: data.profile.verification!, | ||||
|                         ), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 20), | ||||
|                 ), | ||||
|  | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: const Divider(height: 1).padding(vertical: 24), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                     spacing: 24, | ||||
|                     children: [ | ||||
|                       if (buildSubcolumn(data).isNotEmpty) | ||||
|                         Column( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           spacing: 2, | ||||
|                           children: buildSubcolumn(data), | ||||
|                         ), | ||||
|                       Column( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Text('bio').tr().bold(), | ||||
|                           Text( | ||||
|                             data.profile.bio.isEmpty | ||||
|                                 ? 'descriptionNone'.tr() | ||||
|                                 : data.profile.bio, | ||||
|                     ) | ||||
|                     : CustomScrollView( | ||||
|                       slivers: [ | ||||
|                         SliverAppBar( | ||||
|                           foregroundColor: appbarColor.value, | ||||
|                           expandedHeight: 180, | ||||
|                           pinned: true, | ||||
|                           leading: PageBackButton( | ||||
|                             color: appbarColor.value, | ||||
|                             shadows: [appbarShadow], | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                       if (data.profile.timeZone.isNotEmpty) | ||||
|                         Column( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           children: [ | ||||
|                             Text('timeZone').tr().bold(), | ||||
|                             Row( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.baseline, | ||||
|                               textBaseline: TextBaseline.alphabetic, | ||||
|                               spacing: 6, | ||||
|                               children: [ | ||||
|                                 Text(data.profile.timeZone), | ||||
|                                 Text( | ||||
|                                   getTzInfo( | ||||
|                                     data.profile.timeZone, | ||||
|                                   ).$2.formatCustomGlobal('HH:mm'), | ||||
|                                 ), | ||||
|                                 Text( | ||||
|                                   getTzInfo( | ||||
|                                     data.profile.timeZone, | ||||
|                                   ).$1.formatOffsetLocal(), | ||||
|                                 ).fontSize(11), | ||||
|                                 Text( | ||||
|                                   'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}', | ||||
|                                 ).fontSize(11).opacity(0.75), | ||||
|                               ], | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 24), | ||||
|                 ), | ||||
|  | ||||
|                 if (user.value != null) | ||||
|                   SliverToBoxAdapter( | ||||
|                     child: const Divider( | ||||
|                       height: 1, | ||||
|                     ).padding(top: 24, bottom: 12), | ||||
|                   ), | ||||
|                 if (user.value != null) | ||||
|                   SliverToBoxAdapter( | ||||
|                     child: Row( | ||||
|                       spacing: 8, | ||||
|                       children: [ | ||||
|                         if (accountRelationship.value == null || | ||||
|                             accountRelationship.value!.status > -100) | ||||
|                           Expanded( | ||||
|                             child: FilledButton.icon( | ||||
|                               style: ButtonStyle( | ||||
|                                 backgroundColor: WidgetStatePropertyAll( | ||||
|                                   accountRelationship.value == null | ||||
|                                       ? null | ||||
|                                       : Theme.of(context).colorScheme.secondary, | ||||
|                                 ), | ||||
|                                 foregroundColor: WidgetStatePropertyAll( | ||||
|                                   accountRelationship.value == null | ||||
|                                       ? null | ||||
|                                       : Theme.of( | ||||
|                                         context, | ||||
|                                       ).colorScheme.onSecondary, | ||||
|                           flexibleSpace: Stack( | ||||
|                             children: [ | ||||
|                               Positioned.fill( | ||||
|                                 child: | ||||
|                                     data.profile.background?.id != null | ||||
|                                         ? CloudImageWidget( | ||||
|                                           file: data.profile.background, | ||||
|                                         ) | ||||
|                                         : Container( | ||||
|                                           color: | ||||
|                                               Theme.of( | ||||
|                                                 context, | ||||
|                                               ).appBarTheme.backgroundColor, | ||||
|                                         ), | ||||
|                               ), | ||||
|                               FlexibleSpaceBar( | ||||
|                                 title: Text( | ||||
|                                   data.nick, | ||||
|                                   style: TextStyle( | ||||
|                                     color: | ||||
|                                         appbarColor.value ?? | ||||
|                                         Theme.of( | ||||
|                                           context, | ||||
|                                         ).appBarTheme.foregroundColor, | ||||
|                                     shadows: [appbarShadow], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                               onPressed: relationshipAction, | ||||
|                               label: | ||||
|                                   Text( | ||||
|                                     accountRelationship.value == null | ||||
|                                         ? 'addFriendShort' | ||||
|                                         : 'added', | ||||
|                                   ).tr(), | ||||
|                               icon: | ||||
|                                   accountRelationship.value == null | ||||
|                                       ? const Icon(Symbols.person_add) | ||||
|                                       : const Icon(Symbols.person_check), | ||||
|                             ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         if (accountRelationship.value == null || | ||||
|                             accountRelationship.value!.status <= -100) | ||||
|                           Expanded( | ||||
|                             child: FilledButton.icon( | ||||
|                               style: ButtonStyle( | ||||
|                                 backgroundColor: WidgetStatePropertyAll( | ||||
|                                   accountRelationship.value == null | ||||
|                                       ? null | ||||
|                                       : Theme.of(context).colorScheme.secondary, | ||||
|                                 ), | ||||
|                                 foregroundColor: WidgetStatePropertyAll( | ||||
|                                   accountRelationship.value == null | ||||
|                                       ? null | ||||
|                                       : Theme.of( | ||||
|                                         context, | ||||
|                                       ).colorScheme.onSecondary, | ||||
|                                 ), | ||||
|                         ), | ||||
|                         SliverToBoxAdapter(child: accountBasicInfo(data)), | ||||
|                         if (data.badges.isNotEmpty) | ||||
|                           SliverToBoxAdapter( | ||||
|                             child: BadgeList( | ||||
|                               badges: data.badges, | ||||
|                             ).padding(horizontal: 24, bottom: 24), | ||||
|                           ), | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: Column( | ||||
|                             spacing: 12, | ||||
|                             children: [ | ||||
|                               LevelingProgressCard( | ||||
|                                 level: data.profile.level, | ||||
|                                 experience: data.profile.experience, | ||||
|                                 progress: data.profile.levelingProgress, | ||||
|                               ), | ||||
|                               onPressed: blockAction, | ||||
|                               label: | ||||
|                                   Text( | ||||
|                                     accountRelationship.value == null | ||||
|                                         ? 'blockUser' | ||||
|                                         : 'unblockUser', | ||||
|                                   ).tr(), | ||||
|                               icon: | ||||
|                                   accountRelationship.value == null | ||||
|                                       ? const Icon(Symbols.block) | ||||
|                                       : const Icon(Symbols.person_cancel), | ||||
|                             ), | ||||
|                           ), | ||||
|                               if (data.profile.verification != null) | ||||
|                                 VerificationStatusCard( | ||||
|                                   mark: data.profile.verification!, | ||||
|                                 ), | ||||
|                             ], | ||||
|                           ).padding(horizontal: 20), | ||||
|                         ), | ||||
|  | ||||
|                         SliverToBoxAdapter(child: accountProfileDetail(data)), | ||||
|  | ||||
|                         if (user.value != null) | ||||
|                           SliverToBoxAdapter(child: accountAction(data)), | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: Column( | ||||
|                             children: [ | ||||
|                               FortuneGraphWidget( | ||||
|                                 events: accountEvents, | ||||
|                                 eventCalanderUser: data.name, | ||||
|                               ), | ||||
|                             ], | ||||
|                           ).padding(all: 8), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ).padding(horizontal: 16), | ||||
|                   ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Row( | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       Expanded( | ||||
|                         child: FilledButton.icon( | ||||
|                           onPressed: directMessageAction, | ||||
|                           icon: const Icon(Symbols.message), | ||||
|                           label: | ||||
|                               Text( | ||||
|                                 accountChat.value == null | ||||
|                                     ? 'createDirectMessage' | ||||
|                                     : 'gotoDirectMessage', | ||||
|                                 maxLines: 1, | ||||
|                               ).tr(), | ||||
|                         ), | ||||
|                       ), | ||||
|                       IconButton.filled( | ||||
|                         onPressed: () { | ||||
|                           showAbuseReportSheet( | ||||
|                             context, | ||||
|                             resourceIdentifier: 'account/${data.id}', | ||||
|                           ); | ||||
|                         }, | ||||
|                         icon: Icon( | ||||
|                           Symbols.flag, | ||||
|                           color: Theme.of(context).colorScheme.onError, | ||||
|                         ), | ||||
|                         style: ButtonStyle( | ||||
|                           backgroundColor: WidgetStatePropertyAll( | ||||
|                             Theme.of(context).colorScheme.error, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 16, top: 4), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: const Divider(height: 1).padding(top: 12), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Column( | ||||
|                     children: [ | ||||
|                       FortuneGraphWidget( | ||||
|                         events: accountEvents, | ||||
|                         eventCalanderUser: data.name, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).padding(all: 8), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|                     ), | ||||
|           ), | ||||
|       error: | ||||
|           (error, stackTrace) => AppScaffold( | ||||
|   | ||||
| @@ -73,7 +73,7 @@ class CreateAccountScreen extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('createAccount').tr(), | ||||
|   | ||||
| @@ -55,7 +55,7 @@ class LoginScreen extends HookConsumerWidget { | ||||
|     final factorPicked = useState<SnAuthFactor?>(null); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('login').tr(), | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| 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/pods/call.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/chat/call_button.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'; | ||||
| @@ -21,17 +23,39 @@ class CallScreen extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final ongoingCall = ref.watch(ongoingCallProvider(roomId)); | ||||
|     final callState = ref.watch(callNotifierProvider); | ||||
|     final callNotifier = ref.read(callNotifierProvider.notifier); | ||||
|     final callNotifier = ref.watch(callNotifierProvider.notifier); | ||||
|  | ||||
|     useEffect(() { | ||||
|       callNotifier.joinRoom(roomId); | ||||
|       log('[Call] Joining the call...'); | ||||
|       callNotifier.joinRoom(roomId).catchError((_) { | ||||
|         showConfirmAlert( | ||||
|           'Seems there already has a call connected, do you want override it?', | ||||
|           'Call already connected', | ||||
|         ).then((value) { | ||||
|           if (value != true) return; | ||||
|           log('[Call] Joining the call... with overrides'); | ||||
|           callNotifier.disconnect(); | ||||
|           callNotifier.dispose(); | ||||
|           callNotifier.joinRoom(roomId); | ||||
|         }); | ||||
|       }); | ||||
|       return null; | ||||
|     }, []); | ||||
|  | ||||
|     final viewMode = useState<String>('grid'); | ||||
|     final allAudioOnly = callNotifier.participants.every( | ||||
|       (p) => | ||||
|           !(p.hasVideo && | ||||
|               p.remoteParticipant.trackPublications.values.any( | ||||
|                 (pub) => | ||||
|                     pub.track != null && | ||||
|                     pub.kind == TrackType.VIDEO && | ||||
|                     !pub.muted && | ||||
|                     !pub.isDisposed, | ||||
|               )), | ||||
|     ); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         leading: PageBackButton(), | ||||
|         title: Column( | ||||
| @@ -44,45 +68,55 @@ class CallScreen extends HookConsumerWidget { | ||||
|             Text( | ||||
|               callState.isConnected | ||||
|                   ? formatDuration(callState.duration) | ||||
|                   : 'Connecting', | ||||
|                   : (switch (callNotifier.room?.connectionState) { | ||||
|                     ConnectionState.connected => 'connected', | ||||
|                     ConnectionState.connecting => 'connecting', | ||||
|                     ConnectionState.reconnecting => 'reconnecting', | ||||
|                     _ => 'disconnected', | ||||
|                   }).tr(), | ||||
|               style: const TextStyle(fontSize: 14), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         actions: [ | ||||
|           Row( | ||||
|             mainAxisAlignment: MainAxisAlignment.end, | ||||
|             children: [ | ||||
|               IconButton( | ||||
|                 icon: Icon(Symbols.grid_view), | ||||
|                 tooltip: 'Grid View', | ||||
|                 onPressed: () => viewMode.value = 'grid', | ||||
|                 color: | ||||
|                     viewMode.value == 'grid' | ||||
|                         ? Theme.of(context).colorScheme.primary | ||||
|                         : null, | ||||
|           if (!allAudioOnly) | ||||
|             SingleChildScrollView( | ||||
|               child: Row( | ||||
|                 spacing: 4, | ||||
|                 children: [ | ||||
|                   for (final live in callNotifier.participants) | ||||
|                     SpeakingRippleAvatar(live: live, size: 30), | ||||
|                   const Gap(8), | ||||
|                 ], | ||||
|               ), | ||||
|               IconButton( | ||||
|                 icon: Icon(Symbols.view_agenda), | ||||
|                 tooltip: 'Stage View', | ||||
|                 onPressed: () => viewMode.value = 'stage', | ||||
|                 color: | ||||
|                     viewMode.value == 'stage' | ||||
|                         ? Theme.of(context).colorScheme.primary | ||||
|                         : null, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           const Gap(8), | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|       body: | ||||
|           callState.error != null | ||||
|               ? Center( | ||||
|                 child: Text( | ||||
|                   callState.error!, | ||||
|                   textAlign: TextAlign.center, | ||||
|                   style: const TextStyle(color: Colors.red), | ||||
|                 child: ConstrainedBox( | ||||
|                   constraints: const BoxConstraints(maxWidth: 320), | ||||
|                   child: Column( | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.error_outline, size: 48), | ||||
|                       const Gap(4), | ||||
|                       Text( | ||||
|                         callState.error!, | ||||
|                         textAlign: TextAlign.center, | ||||
|                         style: const TextStyle(color: Color(0xFF757575)), | ||||
|                       ), | ||||
|                       const Gap(8), | ||||
|                       TextButton( | ||||
|                         onPressed: () { | ||||
|                           callNotifier.disconnect(); | ||||
|                           callNotifier.dispose(); | ||||
|                           callNotifier.joinRoom(roomId); | ||||
|                         }, | ||||
|                         child: Text('retry').tr(), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ) | ||||
|               : Column( | ||||
| @@ -100,17 +134,8 @@ class CallScreen extends HookConsumerWidget { | ||||
|                             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, | ||||
|                                       )), | ||||
|                         ); | ||||
|                         if (allAudioOnly) { | ||||
|                           // Audio-only: show avatars in a compact row | ||||
|                           return Center( | ||||
| @@ -123,138 +148,41 @@ class CallScreen extends HookConsumerWidget { | ||||
|                                 runSpacing: 8, | ||||
|                                 children: [ | ||||
|                                   for (final live in participants) | ||||
|                                     Padding( | ||||
|                                       padding: const EdgeInsets.symmetric( | ||||
|                                         horizontal: 8, | ||||
|                                       ), | ||||
|                                       child: SpeakingRippleAvatar( | ||||
|                                         isSpeaking: live.isSpeaking, | ||||
|                                         audioLevel: | ||||
|                                             live.remoteParticipant.audioLevel, | ||||
|                                         pictureId: | ||||
|                                             live | ||||
|                                                 .participant | ||||
|                                                 .profile | ||||
|                                                 ?.account | ||||
|                                                 .profile | ||||
|                                                 .picture | ||||
|                                                 ?.id, | ||||
|                                         size: 72, | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                     SpeakingRippleAvatar( | ||||
|                                       live: live, | ||||
|                                       size: 72, | ||||
|                                     ).padding(horizontal: 4), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                           ); | ||||
|                         } | ||||
|                         if (viewMode.value == 'stage') { | ||||
|                           // 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); | ||||
|                           } | ||||
|                           final others = | ||||
|                               participants | ||||
|                                   .where((p) => !mainSpeakers.contains(p)) | ||||
|                                   .toList(); | ||||
|                           return Column( | ||||
|                             children: [ | ||||
|                               Expanded( | ||||
|                                 child: Row( | ||||
|                                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                                   children: [ | ||||
|                                     for (final speaker in mainSpeakers) | ||||
|                                       Expanded( | ||||
|                                         child: | ||||
|                                             AspectRatio( | ||||
|                                               aspectRatio: 16 / 9, | ||||
|                                               child: Card( | ||||
|                                                 margin: EdgeInsets.zero, | ||||
|                                                 child: ClipRRect( | ||||
|                                                   borderRadius: | ||||
|                                                       BorderRadius.circular(8), | ||||
|                                                   child: Column( | ||||
|                                                     children: [ | ||||
|                                                       CallParticipantTile( | ||||
|                                                         live: speaker, | ||||
|                                                       ), | ||||
|                                                     ], | ||||
|                                                   ), | ||||
|                                                 ), | ||||
|                                               ), | ||||
|                                             ).center(), | ||||
|  | ||||
|                         // 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, | ||||
|                                       ), | ||||
|                                   ], | ||||
|                                 ).padding(horizontal: 12), | ||||
|                               ), | ||||
|                               if (others.isNotEmpty) | ||||
|                                 SizedBox( | ||||
|                                   height: 100, | ||||
|                                   child: ListView( | ||||
|                                     scrollDirection: Axis.horizontal, | ||||
|                                     children: [ | ||||
|                                       for (final other in others) | ||||
|                                         Padding( | ||||
|                                           padding: const EdgeInsets.symmetric( | ||||
|                                             horizontal: 8, | ||||
|                                           ), | ||||
|                                           child: CallParticipantTile( | ||||
|                                             live: other, | ||||
|                                           ), | ||||
|                                         ), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                             ], | ||||
|                           ); | ||||
|                                 ) | ||||
|                                 .toList(); | ||||
|                         if (mainSpeakers.isEmpty && participants.isNotEmpty) { | ||||
|                           mainSpeakers.add(participants.first); | ||||
|                         } | ||||
|                         // Default: grid view | ||||
|                         return GridView.builder( | ||||
|                           padding: const EdgeInsets.symmetric( | ||||
|                             horizontal: 12, | ||||
|                             vertical: 8, | ||||
|                           ), | ||||
|                           gridDelegate: | ||||
|                               SliverGridDelegateWithFixedCrossAxisCount( | ||||
|                                 crossAxisCount: | ||||
|                                     isWidestScreen(context) | ||||
|                                         ? 4 | ||||
|                                         : isWiderScreen(context) | ||||
|                                         ? 3 | ||||
|                                         : 2, | ||||
|                                 childAspectRatio: 16 / 9, | ||||
|                                 crossAxisSpacing: 8, | ||||
|                                 mainAxisSpacing: 8, | ||||
|                         return Column( | ||||
|                           children: [ | ||||
|                             for (final speaker in mainSpeakers) | ||||
|                               Expanded( | ||||
|                                 child: CallParticipantTile(live: speaker), | ||||
|                               ), | ||||
|                           itemCount: participants.length, | ||||
|                           itemBuilder: (context, idx) { | ||||
|                             final live = participants[idx]; | ||||
|                             return AspectRatio( | ||||
|                               aspectRatio: 16 / 9, | ||||
|                               child: Card( | ||||
|                                 margin: EdgeInsets.zero, | ||||
|                                 child: ClipRRect( | ||||
|                                   borderRadius: BorderRadius.circular(8), | ||||
|                                   child: Column( | ||||
|                                     children: [CallParticipantTile(live: live)], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ).center(); | ||||
|                           }, | ||||
|                           ], | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|   | ||||
| @@ -21,7 +21,6 @@ import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/account/account_picker.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/chat/call_overlay.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:island/widgets/realms/selection_dropdown.dart'; | ||||
| @@ -346,91 +345,79 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|         child: const Icon(Symbols.add), | ||||
|       ), | ||||
|       floatingActionButtonLocation: TabbedFabLocation(context), | ||||
|       body: Stack( | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           Column( | ||||
|             children: [ | ||||
|               Consumer( | ||||
|                 builder: (context, ref, _) { | ||||
|                   final summaryState = ref.watch(chatSummaryProvider); | ||||
|                   return summaryState.maybeWhen( | ||||
|                     loading: | ||||
|                         () => const LinearProgressIndicator( | ||||
|                           minHeight: 2, | ||||
|                           borderRadius: BorderRadius.zero, | ||||
|                         ), | ||||
|                     orElse: () => const SizedBox.shrink(), | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|               Expanded( | ||||
|                 child: chats.when( | ||||
|                   data: | ||||
|                       (items) => RefreshIndicator( | ||||
|                         onRefresh: | ||||
|                             () => Future.sync(() { | ||||
|                               ref.invalidate(chatroomsJoinedProvider); | ||||
|                             }), | ||||
|                         child: ListView.builder( | ||||
|                           padding: getTabbedPadding( | ||||
|                             context, | ||||
|                             bottom: callState.isConnected ? 96 : null, | ||||
|                           ), | ||||
|                           itemCount: | ||||
|                               items | ||||
|                                   .where( | ||||
|                                     (item) => | ||||
|                                         selectedTab.value == 0 || | ||||
|                                         (selectedTab.value == 1 && | ||||
|                                             item.type == 1) || | ||||
|                                         (selectedTab.value == 2 && | ||||
|                                             item.type != 1), | ||||
|                                   ) | ||||
|                                   .length, | ||||
|                           itemBuilder: (context, index) { | ||||
|                             final filteredItems = | ||||
|                                 items | ||||
|                                     .where( | ||||
|                                       (item) => | ||||
|                                           selectedTab.value == 0 || | ||||
|                                           (selectedTab.value == 1 && | ||||
|                                               item.type == 1) || | ||||
|                                           (selectedTab.value == 2 && | ||||
|                                               item.type != 1), | ||||
|                                     ) | ||||
|                                     .toList(); | ||||
|                             final item = filteredItems[index]; | ||||
|                             return ChatRoomListTile( | ||||
|                               room: item, | ||||
|                               isDirect: item.type == 1, | ||||
|                               onTap: () { | ||||
|                                 context.pushNamed( | ||||
|                                   'chatRoom', | ||||
|                                   pathParameters: {'id': item.id}, | ||||
|                                 ); | ||||
|                               }, | ||||
|           Consumer( | ||||
|             builder: (context, ref, _) { | ||||
|               final summaryState = ref.watch(chatSummaryProvider); | ||||
|               return summaryState.maybeWhen( | ||||
|                 loading: | ||||
|                     () => const LinearProgressIndicator( | ||||
|                       minHeight: 2, | ||||
|                       borderRadius: BorderRadius.zero, | ||||
|                     ), | ||||
|                 orElse: () => const SizedBox.shrink(), | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|           Expanded( | ||||
|             child: chats.when( | ||||
|               data: | ||||
|                   (items) => RefreshIndicator( | ||||
|                     onRefresh: | ||||
|                         () => Future.sync(() { | ||||
|                           ref.invalidate(chatroomsJoinedProvider); | ||||
|                         }), | ||||
|                     child: ListView.builder( | ||||
|                       padding: getTabbedPadding( | ||||
|                         context, | ||||
|                         bottom: callState.isConnected ? 96 : null, | ||||
|                       ), | ||||
|                       itemCount: | ||||
|                           items | ||||
|                               .where( | ||||
|                                 (item) => | ||||
|                                     selectedTab.value == 0 || | ||||
|                                     (selectedTab.value == 1 && | ||||
|                                         item.type == 1) || | ||||
|                                     (selectedTab.value == 2 && item.type != 1), | ||||
|                               ) | ||||
|                               .length, | ||||
|                       itemBuilder: (context, index) { | ||||
|                         final filteredItems = | ||||
|                             items | ||||
|                                 .where( | ||||
|                                   (item) => | ||||
|                                       selectedTab.value == 0 || | ||||
|                                       (selectedTab.value == 1 && | ||||
|                                           item.type == 1) || | ||||
|                                       (selectedTab.value == 2 && | ||||
|                                           item.type != 1), | ||||
|                                 ) | ||||
|                                 .toList(); | ||||
|                         final item = filteredItems[index]; | ||||
|                         return ChatRoomListTile( | ||||
|                           room: item, | ||||
|                           isDirect: item.type == 1, | ||||
|                           onTap: () { | ||||
|                             context.pushNamed( | ||||
|                               'chatRoom', | ||||
|                               pathParameters: {'id': item.id}, | ||||
|                             ); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ), | ||||
|                   loading: | ||||
|                       () => const Center(child: CircularProgressIndicator()), | ||||
|                   error: | ||||
|                       (error, stack) => ResponseErrorWidget( | ||||
|                         error: error, | ||||
|                         onRetry: () { | ||||
|                           ref.invalidate(chatroomsJoinedProvider); | ||||
|                         }, | ||||
|                       ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           Positioned( | ||||
|             left: 0, | ||||
|             right: 0, | ||||
|             bottom: getTabbedPadding(context).bottom + 8, | ||||
|             child: const CallOverlayBar().padding(horizontal: 16, vertical: 12), | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ), | ||||
|               loading: () => const Center(child: CircularProgressIndicator()), | ||||
|               error: | ||||
|                   (error, stack) => ResponseErrorWidget( | ||||
|                     error: error, | ||||
|                     onRetry: () { | ||||
|                       ref.invalidate(chatroomsJoinedProvider); | ||||
|                     }, | ||||
|                   ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|   | ||||
| @@ -41,7 +41,7 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|       try { | ||||
|         final client = ref.watch(apiClientProvider); | ||||
|         await client.patch( | ||||
|           '/chat/$id/members/me/notify', | ||||
|           '/sphere/chat/$id/members/me/notify', | ||||
|           data: {'notify_level': level}, | ||||
|         ); | ||||
|         ref.invalidate(chatroomIdentityProvider(id)); | ||||
| @@ -59,7 +59,7 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|       try { | ||||
|         final client = ref.watch(apiClientProvider); | ||||
|         await client.patch( | ||||
|           '/chat/$id/members/me/notify', | ||||
|           '/sphere/chat/$id/members/me/notify', | ||||
|           data: {'break_until': until.toUtc().toIso8601String()}, | ||||
|         ); | ||||
|         ref.invalidate(chatroomProvider(id)); | ||||
| @@ -421,10 +421,10 @@ class _ChatRoomActionMenu extends HookConsumerWidget { | ||||
|                   showConfirmAlert( | ||||
|                     'deleteChatRoomHint'.tr(), | ||||
|                     'deleteChatRoom'.tr(), | ||||
|                   ).then((confirm) { | ||||
|                   ).then((confirm) async { | ||||
|                     if (confirm) { | ||||
|                       final client = ref.watch(apiClientProvider); | ||||
|                       client.delete('/sphere/chat/$id'); | ||||
|                       await client.delete('/sphere/chat/$id'); | ||||
|                       ref.invalidate(chatroomsJoinedProvider); | ||||
|                       if (context.mounted) { | ||||
|                         context.pop(); | ||||
| @@ -454,10 +454,10 @@ class _ChatRoomActionMenu extends HookConsumerWidget { | ||||
|                   showConfirmAlert( | ||||
|                     'leaveChatRoomHint'.tr(), | ||||
|                     'leaveChatRoom'.tr(), | ||||
|                   ).then((confirm) { | ||||
|                   ).then((confirm) async { | ||||
|                     if (confirm) { | ||||
|                       final client = ref.watch(apiClientProvider); | ||||
|                       client.delete('/sphere/chat/$id/members/me'); | ||||
|                       await client.delete('/sphere/chat/$id/members/me'); | ||||
|                       ref.invalidate(chatroomsJoinedProvider); | ||||
|                       if (context.mounted) { | ||||
|                         context.pop(); | ||||
|   | ||||
| @@ -114,9 +114,9 @@ class CreatorHubShellScreen extends StatelessWidget { | ||||
|         isRoot: true, | ||||
|         child: Row( | ||||
|           children: [ | ||||
|             SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)), | ||||
|             Flexible(flex: 2, child: const CreatorHubScreen(isAside: true)), | ||||
|             const VerticalDivider(width: 1), | ||||
|             Expanded(child: child), | ||||
|             Flexible(flex: 3, child: child), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|   | ||||
| @@ -26,7 +26,7 @@ part 'pack_detail.freezed.dart'; | ||||
| @riverpod | ||||
| Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get('/stickers/$packId/content'); | ||||
|   final resp = await apiClient.get('/sphere/stickers/$packId/content'); | ||||
|   return resp.data | ||||
|       .map<SnSticker>((e) => SnSticker.fromJson(e)) | ||||
|       .cast<SnSticker>() | ||||
| @@ -74,13 +74,16 @@ class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.add_circle), | ||||
|             onPressed: () { | ||||
|               context.pushNamed('creatorStickerNew', pathParameters: {'packId': id}).then(( | ||||
|                 value, | ||||
|               ) { | ||||
|                 if (value != null) { | ||||
|                   ref.invalidate(stickerPackContentProvider(id)); | ||||
|                 } | ||||
|               }); | ||||
|               context | ||||
|                   .pushNamed( | ||||
|                     'creatorStickerNew', | ||||
|                     pathParameters: {'name': pubName, 'packId': id}, | ||||
|                   ) | ||||
|                   .then((value) { | ||||
|                     if (value != null) { | ||||
|                       ref.invalidate(stickerPackContentProvider(id)); | ||||
|                     } | ||||
|                   }); | ||||
|             }, | ||||
|           ), | ||||
|           _StickerPackActionMenu( | ||||
| @@ -173,9 +176,13 @@ class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|                                         title: 'edit'.tr(), | ||||
|                                         image: MenuImage.icon(Symbols.edit), | ||||
|                                         callback: () { | ||||
|                                           context.pushNamed( | ||||
|                                           context | ||||
|                                               .pushNamed( | ||||
|                                                 'creatorStickerEdit', | ||||
|                                                 pathParameters: {'packId': id, 'id': sticker.id}, | ||||
|                                                 pathParameters: { | ||||
|                                                   'packId': id, | ||||
|                                                   'id': sticker.id, | ||||
|                                                 }, | ||||
|                                               ) | ||||
|                                               .then((value) { | ||||
|                                                 if (value != null) { | ||||
| @@ -259,9 +266,7 @@ class _StickerPackActionMenu extends HookConsumerWidget { | ||||
|           (context) => [ | ||||
|             PopupMenuItem( | ||||
|               onTap: () { | ||||
|                 context.push( | ||||
|                   '/creators/$pubName/stickers/$packId/edit', | ||||
|                 ); | ||||
|                 context.push('/creators/$pubName/stickers/$packId/edit'); | ||||
|               }, | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'pack_detail.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$stickerPackContentHash() => | ||||
|     r'78de848fba1f341f217f8ae4b9eef2d8afa67964'; | ||||
|     r'42d74f51022e67e35cb601c2f30f4f02e1f2be9d'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -31,7 +31,7 @@ class StickersScreen extends HookConsumerWidget { | ||||
|               context | ||||
|                   .pushNamed( | ||||
|                     'creatorStickerPackNew', | ||||
|                     queryParameters: {'pubName': pubName}, | ||||
|                     queryParameters: {'name': pubName}, | ||||
|                   ) | ||||
|                   .then((value) { | ||||
|                     if (value != null) { | ||||
| @@ -76,7 +76,7 @@ class SliverStickerPacksList extends HookConsumerWidget { | ||||
|                 onTap: () { | ||||
|                   context.pushNamed( | ||||
|                     'creatorStickerPackDetail', | ||||
|                     pathParameters: {'pubName': pubName, 'packId': sticker.id}, | ||||
|                     pathParameters: {'name': pubName, 'packId': sticker.id}, | ||||
|                   ); | ||||
|                 }, | ||||
|               ); | ||||
|   | ||||
| @@ -51,12 +51,9 @@ class DeveloperHubShellScreen extends StatelessWidget { | ||||
|         isRoot: true, | ||||
|         child: Row( | ||||
|           children: [ | ||||
|             SizedBox( | ||||
|               width: 360, | ||||
|               child: const DeveloperHubScreen(isAside: true), | ||||
|             ), | ||||
|             Flexible(flex: 2, child: const DeveloperHubScreen(isAside: true)), | ||||
|             const VerticalDivider(width: 1), | ||||
|             Expanded(child: child), | ||||
|             Flexible(flex: 3, child: child), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
| @@ -114,7 +111,7 @@ class DeveloperHubScreen extends HookConsumerWidget { | ||||
|     ); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         leading: !isWide ? const PageBackButton() : null, | ||||
|         title: Text('developerHub').tr(), | ||||
|   | ||||
| @@ -17,7 +17,7 @@ class DiscoveryRealmsScreen extends HookConsumerWidget { | ||||
|     final currentQuery = useState<String?>(null); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar(title: Text('discoverRealms'.tr())), | ||||
|       body: Stack( | ||||
|         children: [ | ||||
|   | ||||
| @@ -84,8 +84,10 @@ class ExploreScreen extends HookConsumerWidget { | ||||
|       selectedDay.value = day; | ||||
|     } | ||||
|  | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         toolbarHeight: 0, | ||||
|         bottom: PreferredSize( | ||||
| @@ -167,67 +169,100 @@ class ExploreScreen extends HookConsumerWidget { | ||||
|               ), | ||||
|         ), | ||||
|       ), | ||||
|       floatingActionButton: FloatingActionButton( | ||||
|         heroTag: Key("explore-page-fab"), | ||||
|         onPressed: () { | ||||
|           context.pushNamed('postCompose').then((value) { | ||||
|             if (value != null) { | ||||
|               activitiesNotifier.forceRefresh(); | ||||
|             } | ||||
|           }); | ||||
|       floatingActionButton: InkWell( | ||||
|         onLongPress: () { | ||||
|           context.pushNamed('postCompose', queryParameters: {'type': '1'}).then( | ||||
|             (value) { | ||||
|               if (value != null) { | ||||
|                 activitiesNotifier.forceRefresh(); | ||||
|               } | ||||
|             }, | ||||
|           ); | ||||
|         }, | ||||
|         child: const Icon(Symbols.edit), | ||||
|         child: FloatingActionButton( | ||||
|           heroTag: Key("explore-page-fab"), | ||||
|           onPressed: () { | ||||
|             context.pushNamed('postCompose').then((value) { | ||||
|               if (value != null) { | ||||
|                 activitiesNotifier.forceRefresh(); | ||||
|               } | ||||
|             }); | ||||
|           }, | ||||
|           child: const Icon(Symbols.edit), | ||||
|         ), | ||||
|       ), | ||||
|       floatingActionButtonLocation: TabbedFabLocation(context), | ||||
|       body: Builder( | ||||
|         builder: (context) { | ||||
|           final isWider = isWiderScreen(context); | ||||
|  | ||||
|           final bodyView = TabBarView( | ||||
|             controller: tabController, | ||||
|             physics: const NeverScrollableScrollPhysics(), | ||||
|             children: [ | ||||
|               _buildActivityList(context, ref, null), | ||||
|               _buildActivityList(context, ref, 'subscriptions'), | ||||
|               _buildActivityList(context, ref, 'friends'), | ||||
|             ], | ||||
|           final bodyView = _buildActivityList( | ||||
|             context, | ||||
|             ref, | ||||
|             currentFilter.value, | ||||
|           ); | ||||
|  | ||||
|           if (isWider) { | ||||
|             return Row( | ||||
|               children: [ | ||||
|                 Flexible(flex: 3, child: bodyView), | ||||
|                 const VerticalDivider(width: 1), | ||||
|                 Flexible( | ||||
|                   flex: 2, | ||||
|                   child: SingleChildScrollView( | ||||
|                     child: Column( | ||||
|                       children: [ | ||||
|                         CheckInWidget(), | ||||
|                         Card( | ||||
|                           margin: EdgeInsets.only(left: 16, right: 16, top: 8), | ||||
|                           child: Column( | ||||
|                             children: [ | ||||
|                               // Use the reusable EventCalendarWidget | ||||
|                               EventCalendarWidget( | ||||
|                                 events: events, | ||||
|                                 initialDate: now, | ||||
|                                 showEventDetails: true, | ||||
|                                 onMonthChanged: onMonthChanged, | ||||
|                                 onDaySelected: onDaySelected, | ||||
|                               ), | ||||
|                             ], | ||||
|                 Flexible(flex: 3, child: bodyView.padding(left: 8)), | ||||
|                 if (user.value != null) | ||||
|                   Flexible( | ||||
|                     flex: 2, | ||||
|                     child: SingleChildScrollView( | ||||
|                       child: Column( | ||||
|                         children: [ | ||||
|                           CheckInWidget( | ||||
|                             margin: EdgeInsets.only( | ||||
|                               left: 8, | ||||
|                               right: 12, | ||||
|                               top: 16, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                         FortuneGraphWidget( | ||||
|                           events: events, | ||||
|                           constrainWidth: true, | ||||
|                           onPointSelected: onDaySelected, | ||||
|                           Card( | ||||
|                             margin: EdgeInsets.only(left: 8, right: 12, top: 8), | ||||
|                             child: Column( | ||||
|                               children: [ | ||||
|                                 // Use the reusable EventCalendarWidget | ||||
|                                 EventCalendarWidget( | ||||
|                                   events: events, | ||||
|                                   initialDate: now, | ||||
|                                   showEventDetails: true, | ||||
|                                   onMonthChanged: onMonthChanged, | ||||
|                                   onDaySelected: onDaySelected, | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                           ), | ||||
|                           FortuneGraphWidget( | ||||
|                             margin: EdgeInsets.only(left: 8, right: 12, top: 8), | ||||
|                             events: events, | ||||
|                             constrainWidth: true, | ||||
|                             onPointSelected: onDaySelected, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ) | ||||
|                 else | ||||
|                   Flexible( | ||||
|                     flex: 2, | ||||
|                     child: Column( | ||||
|                       mainAxisAlignment: MainAxisAlignment.center, | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         Text( | ||||
|                           'Welcome to\nthe Solar Network', | ||||
|                           style: Theme.of(context).textTheme.titleLarge, | ||||
|                         ).bold(), | ||||
|                         const Gap(2), | ||||
|                         Text( | ||||
|                           'Login to explore more!', | ||||
|                           style: Theme.of(context).textTheme.bodyLarge, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     ).padding(horizontal: 36, vertical: 16), | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ); | ||||
|           } | ||||
| @@ -280,56 +315,62 @@ class _DiscoveryActivityItem extends StatelessWidget { | ||||
|     final items = data['items'] as List; | ||||
|     final type = items.firstOrNull?['type'] ?? 'unknown'; | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: [ | ||||
|             const Icon(Symbols.explore, size: 19), | ||||
|             const Gap(8), | ||||
|             Text( | ||||
|               (switch (type) { | ||||
|                 'realm' => 'discoverRealms', | ||||
|                 'publisher' => 'discoverPublishers', | ||||
|                 'article' => 'discoverWebArticles', | ||||
|                 _ => 'unknown', | ||||
|               }).tr(), | ||||
|               style: Theme.of(context).textTheme.titleMedium, | ||||
|             ).padding(top: 1), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 8, bottom: 4), | ||||
|         SizedBox( | ||||
|           height: 180, | ||||
|           child: ListView.builder( | ||||
|             scrollDirection: Axis.horizontal, | ||||
|             itemCount: items.length, | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 8), | ||||
|             itemBuilder: (context, index) { | ||||
|               final item = items[index]; | ||||
|               switch (type) { | ||||
|                 case 'realm': | ||||
|                   return RealmCard( | ||||
|                     realm: SnRealm.fromJson(item['data']), | ||||
|                     maxWidth: 280, | ||||
|                   ); | ||||
|                 case 'publisher': | ||||
|                   return PublisherCard( | ||||
|                     publisher: SnPublisher.fromJson(item['data']), | ||||
|                     maxWidth: 280, | ||||
|                   ); | ||||
|                 case 'article': | ||||
|                   return WebArticleCard( | ||||
|                     article: SnWebArticle.fromJson(item['data']), | ||||
|                     maxWidth: 280, | ||||
|                   ); | ||||
|                 default: | ||||
|                   return Placeholder(); | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|         ).padding(bottom: 4), | ||||
|       ], | ||||
|     return Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: [ | ||||
|               const Icon(Symbols.explore, size: 19), | ||||
|               const Gap(8), | ||||
|               Text( | ||||
|                 (switch (type) { | ||||
|                   'realm' => 'discoverRealms', | ||||
|                   'publisher' => 'discoverPublishers', | ||||
|                   'article' => 'discoverWebArticles', | ||||
|                   _ => 'unknown', | ||||
|                 }).tr(), | ||||
|                 style: Theme.of(context).textTheme.titleMedium, | ||||
|               ).padding(top: 1), | ||||
|             ], | ||||
|           ).padding(horizontal: 20, top: 8, bottom: 4), | ||||
|           SizedBox( | ||||
|             height: 180, | ||||
|             child: ConstrainedBox( | ||||
|               constraints: const BoxConstraints(maxHeight: 200), | ||||
|               child: CarouselView.weighted( | ||||
|                 flexWeights: | ||||
|                     isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1], | ||||
|                 consumeMaxWeight: false, | ||||
|                 enableSplash: false, | ||||
|                 shape: RoundedRectangleBorder( | ||||
|                   borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                 ), | ||||
|                 children: [ | ||||
|                   for (final item in items) | ||||
|                     switch (type) { | ||||
|                       'realm' => RealmCard( | ||||
|                         realm: SnRealm.fromJson(item['data']), | ||||
|                         maxWidth: 280, | ||||
|                       ), | ||||
|                       'publisher' => PublisherCard( | ||||
|                         publisher: SnPublisher.fromJson(item['data']), | ||||
|                         maxWidth: 280, | ||||
|                       ), | ||||
|                       'article' => WebArticleCard( | ||||
|                         article: SnWebArticle.fromJson(item['data']), | ||||
|                         maxWidth: 280, | ||||
|                       ), | ||||
|                       _ => Placeholder(), | ||||
|                     }, | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ).padding(bottom: 8, horizontal: 8), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -355,8 +396,13 @@ class _ActivityListView extends HookConsumerWidget { | ||||
|  | ||||
|     return CustomScrollView( | ||||
|       slivers: [ | ||||
|         SliverGap(12), | ||||
|         if (user.value != null && !contentOnly) | ||||
|           SliverToBoxAdapter(child: CheckInWidget()), | ||||
|           SliverToBoxAdapter( | ||||
|             child: CheckInWidget( | ||||
|               margin: EdgeInsets.only(left: 8, right: 8, bottom: 4), | ||||
|             ), | ||||
|           ), | ||||
|         SliverList.builder( | ||||
|           itemCount: widgetCount, | ||||
|           itemBuilder: (context, index) { | ||||
| @@ -373,19 +419,9 @@ class _ActivityListView extends HookConsumerWidget { | ||||
|             switch (item.type) { | ||||
|               case 'posts.new': | ||||
|               case 'posts.new.replies': | ||||
|                 final isReply = item.type == 'posts.new.replies'; | ||||
|                 itemWidget = PostItem( | ||||
|                   backgroundColor: | ||||
|                       isWideScreen(context) ? Colors.transparent : null, | ||||
|                 itemWidget = PostActionableItem( | ||||
|                   borderRadius: 8, | ||||
|                   item: SnPost.fromJson(item.data!), | ||||
|                   padding: | ||||
|                       isReply | ||||
|                           ? const EdgeInsets.only( | ||||
|                             left: 16, | ||||
|                             right: 16, | ||||
|                             bottom: 16, | ||||
|                           ) | ||||
|                           : null, | ||||
|                   onRefresh: () { | ||||
|                     activitiesNotifier.forceRefresh(); | ||||
|                   }, | ||||
| @@ -396,21 +432,10 @@ class _ActivityListView extends HookConsumerWidget { | ||||
|                     ); | ||||
|                   }, | ||||
|                 ); | ||||
|                 if (isReply) { | ||||
|                   itemWidget = Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                     children: [ | ||||
|                       Row( | ||||
|                         children: [ | ||||
|                           const Icon(Symbols.reply), | ||||
|                           const Gap(8), | ||||
|                           Text('Replying your post'), | ||||
|                         ], | ||||
|                       ).padding(horizontal: 20, vertical: 8), | ||||
|                       itemWidget, | ||||
|                     ], | ||||
|                   ); | ||||
|                 } | ||||
|                 itemWidget = Card( | ||||
|                   margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|                   child: itemWidget, | ||||
|                 ); | ||||
|                 break; | ||||
|               case 'discovery': | ||||
|                 itemWidget = _DiscoveryActivityItem(data: item.data!); | ||||
| @@ -419,7 +444,7 @@ class _ActivityListView extends HookConsumerWidget { | ||||
|                 itemWidget = const Placeholder(); | ||||
|             } | ||||
|  | ||||
|             return Column(children: [itemWidget, const Divider(height: 1)]); | ||||
|             return itemWidget; | ||||
|           }, | ||||
|         ), | ||||
|         SliverGap(getTabbedPadding(context).bottom), | ||||
|   | ||||
| @@ -18,7 +18,8 @@ import 'package:island/widgets/post/publishers_modal.dart'; | ||||
| import 'package:island/screens/posts/post_detail.dart'; | ||||
| import 'package:island/widgets/post/compose_settings_sheet.dart'; | ||||
| import 'package:island/services/compose_storage_db.dart'; | ||||
| import 'package:island/widgets/post/draft_manager.dart'; | ||||
| // DraftManagerSheet is now imported through compose_toolbar.dart | ||||
| import 'package:island/widgets/post/compose_toolbar.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @@ -52,13 +53,13 @@ class PostEditScreen extends HookConsumerWidget { | ||||
|       data: (post) => PostComposeScreen(originalPost: post), | ||||
|       loading: | ||||
|           () => AppScaffold( | ||||
|             noBackground: false, | ||||
|             isNoBackground: false, | ||||
|             appBar: AppBar(leading: const PageBackButton()), | ||||
|             body: const Center(child: CircularProgressIndicator()), | ||||
|           ), | ||||
|       error: | ||||
|           (e, _) => AppScaffold( | ||||
|             noBackground: false, | ||||
|             isNoBackground: false, | ||||
|             appBar: AppBar(leading: const PageBackButton()), | ||||
|             body: Text('Error: $e', textAlign: TextAlign.center), | ||||
|           ), | ||||
| @@ -92,7 +93,6 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|  | ||||
|     // Otherwise, continue with regular post compose | ||||
|     final theme = Theme.of(context); | ||||
|     final colorScheme = theme.colorScheme; | ||||
|  | ||||
|     // When editing, preserve the original replied/forwarded post references | ||||
|     final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost; | ||||
| @@ -287,43 +287,10 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|         } | ||||
|       }, | ||||
|       child: AppScaffold( | ||||
|         noBackground: false, | ||||
|         isNoBackground: false, | ||||
|         appBar: AppBar( | ||||
|           leading: const PageBackButton(), | ||||
|           actions: [ | ||||
|             if (originalPost == null) // Only show drafts for new posts | ||||
|               IconButton( | ||||
|                 icon: const Icon(Symbols.draft), | ||||
|                 onPressed: () { | ||||
|                   showModalBottomSheet( | ||||
|                     context: context, | ||||
|                     isScrollControlled: true, | ||||
|                     builder: | ||||
|                         (context) => DraftManagerSheet( | ||||
|                           onDraftSelected: (draftId) { | ||||
|                             final draft = | ||||
|                                 ref.read( | ||||
|                                   composeStorageNotifierProvider, | ||||
|                                 )[draftId]; | ||||
|                             if (draft != null) { | ||||
|                               state.titleController.text = draft.title ?? ''; | ||||
|                               state.descriptionController.text = | ||||
|                                   draft.description ?? ''; | ||||
|                               state.contentController.text = | ||||
|                                   draft.content ?? ''; | ||||
|                               state.visibility.value = draft.visibility; | ||||
|                             } | ||||
|                           }, | ||||
|                         ), | ||||
|                   ); | ||||
|                 }, | ||||
|                 tooltip: 'drafts'.tr(), | ||||
|               ), | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.save), | ||||
|               onPressed: () => ComposeLogic.saveDraft(ref, state), | ||||
|               tooltip: 'saveDraft'.tr(), | ||||
|             ), | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.settings), | ||||
|               onPressed: showSettingsSheet, | ||||
| @@ -455,27 +422,7 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|             ), | ||||
|  | ||||
|             // Bottom toolbar | ||||
|             Material( | ||||
|               elevation: 4, | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   IconButton( | ||||
|                     onPressed: () => ComposeLogic.pickPhotoMedia(ref, state), | ||||
|                     icon: const Icon(Symbols.add_a_photo), | ||||
|                     color: colorScheme.primary, | ||||
|                   ), | ||||
|                   IconButton( | ||||
|                     onPressed: () => ComposeLogic.pickVideoMedia(ref, state), | ||||
|                     icon: const Icon(Symbols.videocam), | ||||
|                     color: colorScheme.primary, | ||||
|                   ), | ||||
|                 ], | ||||
|               ).padding( | ||||
|                 bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|                 horizontal: 16, | ||||
|                 top: 8, | ||||
|               ), | ||||
|             ), | ||||
|             ComposeToolbar(state: state, originalPost: originalPost), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
| @@ -650,7 +597,7 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                             child: SingleChildScrollView( | ||||
|                               controller: scrollController, | ||||
|                               padding: const EdgeInsets.all(16), | ||||
|                               child: PostItem(item: post, isOpenable: false), | ||||
|                               child: PostItem(item: post), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|   | ||||
| @@ -19,8 +19,8 @@ import 'package:island/widgets/content/markdown.dart'; | ||||
| import 'package:island/widgets/post/compose_shared.dart'; | ||||
| import 'package:island/widgets/post/compose_settings_sheet.dart'; | ||||
| import 'package:island/services/compose_storage_db.dart'; | ||||
| import 'package:island/widgets/post/compose_toolbar.dart'; | ||||
| import 'package:island/widgets/post/publishers_modal.dart'; | ||||
| import 'package:island/widgets/post/draft_manager.dart'; | ||||
|  | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| @@ -153,6 +153,57 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     Widget buildPreviewPane() { | ||||
|       final widgetItem = SingleChildScrollView( | ||||
|         padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8), | ||||
|         child: ValueListenableBuilder<TextEditingValue>( | ||||
|           valueListenable: state.titleController, | ||||
|           builder: (context, titleValue, _) { | ||||
|             return ValueListenableBuilder<TextEditingValue>( | ||||
|               valueListenable: state.descriptionController, | ||||
|               builder: (context, descriptionValue, _) { | ||||
|                 return ValueListenableBuilder<TextEditingValue>( | ||||
|                   valueListenable: state.contentController, | ||||
|                   builder: (context, contentValue, _) { | ||||
|                     return Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         if (titleValue.text.isNotEmpty) ...[ | ||||
|                           Text( | ||||
|                             titleValue.text, | ||||
|                             style: theme.textTheme.headlineSmall?.copyWith( | ||||
|                               fontWeight: FontWeight.bold, | ||||
|                             ), | ||||
|                           ), | ||||
|                           const Gap(16), | ||||
|                         ], | ||||
|                         if (descriptionValue.text.isNotEmpty) ...[ | ||||
|                           Text( | ||||
|                             descriptionValue.text, | ||||
|                             style: theme.textTheme.bodyLarge?.copyWith( | ||||
|                               color: colorScheme.onSurface.withOpacity(0.7), | ||||
|                             ), | ||||
|                           ), | ||||
|                           const Gap(16), | ||||
|                         ], | ||||
|                         if (contentValue.text.isNotEmpty) | ||||
|                           MarkdownTextContent( | ||||
|                             content: contentValue.text, | ||||
|                             textStyle: theme.textTheme.bodyMedium, | ||||
|                           ), | ||||
|                       ], | ||||
|                     ); | ||||
|                   }, | ||||
|                 ); | ||||
|               }, | ||||
|             ); | ||||
|           }, | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       if (isWideScreen(context)) { | ||||
|         return Align(alignment: Alignment.topLeft, child: widgetItem); | ||||
|       } | ||||
|  | ||||
|       return Container( | ||||
|         decoration: BoxDecoration( | ||||
|           border: Border.all(color: colorScheme.outline.withOpacity(0.3)), | ||||
| @@ -178,210 +229,119 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|             Expanded( | ||||
|               child: SingleChildScrollView( | ||||
|                 padding: const EdgeInsets.all(16), | ||||
|                 child: ValueListenableBuilder<TextEditingValue>( | ||||
|                   valueListenable: state.titleController, | ||||
|                   builder: (context, titleValue, _) { | ||||
|                     return ValueListenableBuilder<TextEditingValue>( | ||||
|                       valueListenable: state.descriptionController, | ||||
|                       builder: (context, descriptionValue, _) { | ||||
|                         return ValueListenableBuilder<TextEditingValue>( | ||||
|                           valueListenable: state.contentController, | ||||
|                           builder: (context, contentValue, _) { | ||||
|                             return Column( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                               children: [ | ||||
|                                 if (titleValue.text.isNotEmpty) ...[ | ||||
|                                   Text( | ||||
|                                     titleValue.text, | ||||
|                                     style: theme.textTheme.headlineSmall | ||||
|                                         ?.copyWith(fontWeight: FontWeight.bold), | ||||
|                                   ), | ||||
|                                   const Gap(16), | ||||
|                                 ], | ||||
|                                 if (descriptionValue.text.isNotEmpty) ...[ | ||||
|                                   Text( | ||||
|                                     descriptionValue.text, | ||||
|                                     style: theme.textTheme.bodyLarge?.copyWith( | ||||
|                                       color: colorScheme.onSurface.withOpacity( | ||||
|                                         0.7, | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                   const Gap(16), | ||||
|                                 ], | ||||
|                                 if (contentValue.text.isNotEmpty) | ||||
|                                   MarkdownTextContent( | ||||
|                                     content: contentValue.text, | ||||
|                                     textStyle: theme.textTheme.bodyMedium, | ||||
|                                   ), | ||||
|                               ], | ||||
|                             ); | ||||
|                           }, | ||||
|                         ); | ||||
|                       }, | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             Expanded(child: widgetItem), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     Widget buildEditorPane() { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           // Publisher row | ||||
|           Card( | ||||
|             margin: EdgeInsets.only(top: 8), | ||||
|             elevation: 1, | ||||
|             child: Padding( | ||||
|               padding: const EdgeInsets.all(12), | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   GestureDetector( | ||||
|                     child: ProfilePictureWidget( | ||||
|                       fileId: state.currentPublisher.value?.picture?.id, | ||||
|                       radius: 20, | ||||
|                       fallbackIcon: | ||||
|                           state.currentPublisher.value == null | ||||
|                               ? Symbols.question_mark | ||||
|                               : null, | ||||
|       return Center( | ||||
|         child: ConstrainedBox( | ||||
|           constraints: const BoxConstraints(maxWidth: 560), | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Expanded( | ||||
|                 child: RawKeyboardListener( | ||||
|                   focusNode: FocusNode(), | ||||
|                   onKey: | ||||
|                       (event) => _handleKeyPress( | ||||
|                         event, | ||||
|                         state, | ||||
|                         ref, | ||||
|                         context, | ||||
|                         originalPost: originalPost, | ||||
|                       ), | ||||
|                   child: TextField( | ||||
|                     controller: state.contentController, | ||||
|                     style: theme.textTheme.bodyMedium, | ||||
|                     decoration: InputDecoration( | ||||
|                       border: InputBorder.none, | ||||
|                       hintText: 'postContent'.tr(), | ||||
|                       contentPadding: const EdgeInsets.symmetric( | ||||
|                         vertical: 16, | ||||
|                         horizontal: 8, | ||||
|                       ), | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       showModalBottomSheet( | ||||
|                         isScrollControlled: true, | ||||
|                         context: context, | ||||
|                         builder: (context) => const PublisherModal(), | ||||
|                       ).then((value) { | ||||
|                         if (value != null) { | ||||
|                           state.currentPublisher.value = value; | ||||
|                         } | ||||
|                       }); | ||||
|                     }, | ||||
|                     maxLines: null, | ||||
|                     expands: true, | ||||
|                     textAlignVertical: TextAlignVertical.top, | ||||
|                     onTapOutside: | ||||
|                         (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   ), | ||||
|                   const Gap(16), | ||||
|                   if (state.currentPublisher.value == null) | ||||
|                     Text( | ||||
|                       'postPublisherUnselected'.tr(), | ||||
|                       style: theme.textTheme.bodyMedium, | ||||
|                     ) | ||||
|                   else | ||||
|                     Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         Text(state.currentPublisher.value!.nick).bold(), | ||||
|                         Text( | ||||
|                           '@${state.currentPublisher.value!.name}', | ||||
|                         ).fontSize(12), | ||||
|                       ], | ||||
|                     ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|  | ||||
|           // Content field with keyboard listener | ||||
|           Expanded( | ||||
|             child: RawKeyboardListener( | ||||
|               focusNode: FocusNode(), | ||||
|               onKey: | ||||
|                   (event) => _handleKeyPress( | ||||
|                     event, | ||||
|                     state, | ||||
|                     ref, | ||||
|                     context, | ||||
|                     originalPost: originalPost, | ||||
|                   ), | ||||
|               child: TextField( | ||||
|                 controller: state.contentController, | ||||
|                 style: theme.textTheme.bodyMedium, | ||||
|                 decoration: InputDecoration( | ||||
|                   border: InputBorder.none, | ||||
|                   hintText: 'postContent'.tr(), | ||||
|                   contentPadding: const EdgeInsets.all(8), | ||||
|                 ), | ||||
|                 maxLines: null, | ||||
|                 expands: true, | ||||
|                 textAlignVertical: TextAlignVertical.top, | ||||
|                 onTapOutside: | ||||
|                     (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|  | ||||
|           // Attachments preview | ||||
|           ValueListenableBuilder<List<UniversalFile>>( | ||||
|             valueListenable: state.attachments, | ||||
|             builder: (context, attachments, _) { | ||||
|               if (attachments.isEmpty) return const SizedBox.shrink(); | ||||
|               return Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   const Gap(16), | ||||
|                   Text( | ||||
|                     'articleAttachmentHint'.tr(), | ||||
|                     style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||
|                       color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                     ), | ||||
|                   ).padding(bottom: 8), | ||||
|                   ValueListenableBuilder<Map<int, double>>( | ||||
|                     valueListenable: state.attachmentProgress, | ||||
|                     builder: (context, progressMap, _) { | ||||
|                       return Wrap( | ||||
|                         spacing: 8, | ||||
|                         runSpacing: 8, | ||||
|                         children: [ | ||||
|                           for (var idx = 0; idx < attachments.length; idx++) | ||||
|                             SizedBox( | ||||
|                               width: 280, | ||||
|                               height: 280, | ||||
|                               child: AttachmentPreview( | ||||
|                                 item: attachments[idx], | ||||
|                                 progress: progressMap[idx], | ||||
|                                 onRequestUpload: | ||||
|                                     () => ComposeLogic.uploadAttachment( | ||||
|                                       ref, | ||||
|                                       state, | ||||
|                                       idx, | ||||
|                                     ), | ||||
|                                 onDelete: | ||||
|                                     () => ComposeLogic.deleteAttachment( | ||||
|                                       ref, | ||||
|                                       state, | ||||
|                                       idx, | ||||
|                                     ), | ||||
|                                 onMove: (delta) { | ||||
|                                   state | ||||
|                                       .attachments | ||||
|                                       .value = ComposeLogic.moveAttachment( | ||||
|                                     state.attachments.value, | ||||
|                                     idx, | ||||
|                                     delta, | ||||
|                                   ); | ||||
|                                 }, | ||||
|                                 onInsert: | ||||
|                                     () => ComposeLogic.insertAttachment( | ||||
|                                       ref, | ||||
|                                       state, | ||||
|                                       idx, | ||||
|                                     ), | ||||
|                               ), | ||||
|                             ), | ||||
|                         ], | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ); | ||||
|             }, | ||||
|               // Attachments preview | ||||
|               ValueListenableBuilder<List<UniversalFile>>( | ||||
|                 valueListenable: state.attachments, | ||||
|                 builder: (context, attachments, _) { | ||||
|                   if (attachments.isEmpty) return const SizedBox.shrink(); | ||||
|                   return Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       const Gap(16), | ||||
|                       Text( | ||||
|                         'articleAttachmentHint'.tr(), | ||||
|                         style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||
|                           color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                         ), | ||||
|                       ).padding(bottom: 8), | ||||
|                       ValueListenableBuilder<Map<int, double>>( | ||||
|                         valueListenable: state.attachmentProgress, | ||||
|                         builder: (context, progressMap, _) { | ||||
|                           return Wrap( | ||||
|                             spacing: 8, | ||||
|                             runSpacing: 8, | ||||
|                             children: [ | ||||
|                               for (var idx = 0; idx < attachments.length; idx++) | ||||
|                                 SizedBox( | ||||
|                                   width: 280, | ||||
|                                   height: 280, | ||||
|                                   child: AttachmentPreview( | ||||
|                                     item: attachments[idx], | ||||
|                                     progress: progressMap[idx], | ||||
|                                     onRequestUpload: | ||||
|                                         () => ComposeLogic.uploadAttachment( | ||||
|                                           ref, | ||||
|                                           state, | ||||
|                                           idx, | ||||
|                                         ), | ||||
|                                     onDelete: | ||||
|                                         () => ComposeLogic.deleteAttachment( | ||||
|                                           ref, | ||||
|                                           state, | ||||
|                                           idx, | ||||
|                                         ), | ||||
|                                     onMove: (delta) { | ||||
|                                       state | ||||
|                                           .attachments | ||||
|                                           .value = ComposeLogic.moveAttachment( | ||||
|                                         state.attachments.value, | ||||
|                                         idx, | ||||
|                                         delta, | ||||
|                                       ); | ||||
|                                     }, | ||||
|                                     onInsert: | ||||
|                                         () => ComposeLogic.insertAttachment( | ||||
|                                           ref, | ||||
|                                           state, | ||||
|                                           idx, | ||||
|                                         ), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                             ], | ||||
|                           ); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
| @@ -392,7 +352,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|         } | ||||
|       }, | ||||
|       child: AppScaffold( | ||||
|         noBackground: false, | ||||
|         isNoBackground: false, | ||||
|         appBar: AppBar( | ||||
|           leading: const PageBackButton(), | ||||
|           title: ValueListenableBuilder<TextEditingValue>( | ||||
| @@ -406,38 +366,26 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|           actions: [ | ||||
|             // Info banner for article compose | ||||
|             const SizedBox.shrink(), | ||||
|             if (originalPost == null) // Only show drafts for new articles | ||||
|               IconButton( | ||||
|                 icon: const Icon(Symbols.draft), | ||||
|                 onPressed: () { | ||||
|                   showModalBottomSheet( | ||||
|                     context: context, | ||||
|                     isScrollControlled: true, | ||||
|                     builder: | ||||
|                         (context) => DraftManagerSheet( | ||||
|                           onDraftSelected: (draftId) { | ||||
|                             final draft = | ||||
|                                 ref.read( | ||||
|                                   composeStorageNotifierProvider, | ||||
|                                 )[draftId]; | ||||
|                             if (draft != null) { | ||||
|                               state.titleController.text = draft.title ?? ''; | ||||
|                               state.descriptionController.text = | ||||
|                                   draft.description ?? ''; | ||||
|                               state.contentController.text = | ||||
|                                   draft.content ?? ''; | ||||
|                               state.visibility.value = draft.visibility; | ||||
|                             } | ||||
|                           }, | ||||
|                         ), | ||||
|                   ); | ||||
|                 }, | ||||
|                 tooltip: 'drafts'.tr(), | ||||
|               ), | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.save), | ||||
|               onPressed: () => ComposeLogic.saveDraft(ref, state), | ||||
|               tooltip: 'saveDraft'.tr(), | ||||
|               icon: ProfilePictureWidget( | ||||
|                 fileId: state.currentPublisher.value?.picture?.id, | ||||
|                 radius: 12, | ||||
|                 fallbackIcon: | ||||
|                     state.currentPublisher.value == null | ||||
|                         ? Symbols.question_mark | ||||
|                         : null, | ||||
|               ), | ||||
|               onPressed: () { | ||||
|                 showModalBottomSheet( | ||||
|                   isScrollControlled: true, | ||||
|                   context: context, | ||||
|                   builder: (context) => const PublisherModal(), | ||||
|                 ).then((value) { | ||||
|                   if (value != null) { | ||||
|                     state.currentPublisher.value = value; | ||||
|                   } | ||||
|                 }); | ||||
|               }, | ||||
|             ), | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.settings), | ||||
| @@ -499,6 +447,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|                               flex: showPreview.value ? 1 : 2, | ||||
|                               child: buildEditorPane(), | ||||
|                             ), | ||||
|                             const VerticalDivider(), | ||||
|                             if (showPreview.value) | ||||
|                               Expanded(child: buildPreviewPane()), | ||||
|                           ], | ||||
| @@ -510,27 +459,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|             ), | ||||
|  | ||||
|             // Bottom toolbar | ||||
|             Material( | ||||
|               elevation: 4, | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   IconButton( | ||||
|                     onPressed: () => ComposeLogic.pickPhotoMedia(ref, state), | ||||
|                     icon: const Icon(Symbols.add_a_photo), | ||||
|                     color: colorScheme.primary, | ||||
|                   ), | ||||
|                   IconButton( | ||||
|                     onPressed: () => ComposeLogic.pickVideoMedia(ref, state), | ||||
|                     icon: const Icon(Symbols.videocam), | ||||
|                     color: colorScheme.primary, | ||||
|                   ), | ||||
|                 ], | ||||
|               ).padding( | ||||
|                 bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|                 horizontal: 16, | ||||
|                 top: 8, | ||||
|               ), | ||||
|             ), | ||||
|             ComposeToolbar(state: state, originalPost: originalPost), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/post/post_quick_reply.dart'; | ||||
| @@ -54,10 +53,8 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|     final postState = ref.watch(postStateProvider(id)); | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|  | ||||
|     final isWide = isWideScreen(context); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar(title: const Text('Post')), | ||||
|       body: postState.when( | ||||
|         data: (post) { | ||||
| @@ -67,13 +64,13 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|               CustomScrollView( | ||||
|                 slivers: [ | ||||
|                   SliverToBoxAdapter( | ||||
|                     child: Column( | ||||
|                       children: [ | ||||
|                         PostItem( | ||||
|                     child: Center( | ||||
|                       child: ConstrainedBox( | ||||
|                         constraints: BoxConstraints(maxWidth: 600), | ||||
|                         child: PostItem( | ||||
|                           item: post!, | ||||
|                           isOpenable: false, | ||||
|                           isFullPost: true, | ||||
|                           backgroundColor: isWide ? Colors.transparent : null, | ||||
|                           isEmbedReply: false, | ||||
|                           onUpdate: (newItem) { | ||||
|                             // Update the local state with the new post data | ||||
|                             ref | ||||
| @@ -81,11 +78,10 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|                                 .updatePost(newItem); | ||||
|                           }, | ||||
|                         ), | ||||
|                         const Divider(height: 1), | ||||
|                       ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   PostRepliesList(postId: id), | ||||
|                   PostRepliesList(postId: id, maxWidth: 600), | ||||
|                   SliverGap(MediaQuery.of(context).padding.bottom + 80), | ||||
|                 ], | ||||
|               ), | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| final postSearchNotifierProvider = StateNotifierProvider.autoDispose< | ||||
| @@ -55,7 +56,7 @@ class PostSearchNotifier | ||||
|           'query': _currentQuery, | ||||
|           'offset': offset, | ||||
|           'take': _pageSize, | ||||
|           'useVector': true, | ||||
|           'useVector': false, | ||||
|         }, | ||||
|       ); | ||||
|  | ||||
| @@ -109,7 +110,7 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         title: TextField( | ||||
|           controller: _searchController, | ||||
| @@ -141,6 +142,7 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> { | ||||
|               } | ||||
|  | ||||
|               return ListView.builder( | ||||
|                 padding: EdgeInsets.zero, | ||||
|                 itemCount: data.items.length + (data.hasMore ? 1 : 0), | ||||
|                 itemBuilder: (context, index) { | ||||
|                   if (index >= data.items.length) { | ||||
| @@ -151,14 +153,27 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> { | ||||
|                   } | ||||
|  | ||||
|                   final post = data.items[index]; | ||||
|                   return Column( | ||||
|                     children: [PostItem(item: post), const Divider(height: 1)], | ||||
|                   return Center( | ||||
|                     child: ConstrainedBox( | ||||
|                       constraints: BoxConstraints(maxWidth: 600), | ||||
|                       child: Card( | ||||
|                         margin: EdgeInsets.symmetric( | ||||
|                           horizontal: 8, | ||||
|                           vertical: 4, | ||||
|                         ), | ||||
|                         child: PostActionableItem(item: post, borderRadius: 8), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|             loading: () => const Center(child: CircularProgressIndicator()), | ||||
|             error: (error, stack) => Center(child: Text('Error: $error')), | ||||
|             error: | ||||
|                 (error, stack) => ResponseErrorWidget( | ||||
|                   error: error, | ||||
|                   onRetry: () => ref.invalidate(postSearchNotifierProvider), | ||||
|                 ), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import 'package:island/models/user.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/services/color.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| import 'package:island/widgets/account/badge.dart'; | ||||
| import 'package:island/widgets/account/status.dart'; | ||||
| @@ -121,210 +122,280 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|       offset: Offset(1.0, 1.0), | ||||
|     ); | ||||
|  | ||||
|     Widget publisherBasisWidget(SnPublisher data) => Row( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       spacing: 20, | ||||
|       children: [ | ||||
|         GestureDetector( | ||||
|           child: Badge( | ||||
|             isLabelVisible: data.type == 0, | ||||
|             padding: EdgeInsets.all(4), | ||||
|             label: Icon( | ||||
|               Symbols.launch, | ||||
|               size: 16, | ||||
|               color: Theme.of(context).colorScheme.onPrimary, | ||||
|             ), | ||||
|             backgroundColor: Theme.of(context).colorScheme.primary, | ||||
|             offset: Offset(0, 48), | ||||
|             child: ProfilePictureWidget(file: data.picture, radius: 32), | ||||
|           ), | ||||
|           onTap: () { | ||||
|             Navigator.pop(context, true); | ||||
|             if (data.account?.name != null) { | ||||
|               context.pushNamed( | ||||
|                 'accountProfile', | ||||
|                 pathParameters: {'name': data.account!.name}, | ||||
|               ); | ||||
|             } | ||||
|           }, | ||||
|         ), | ||||
|         Expanded( | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|             children: [ | ||||
|               Row( | ||||
|                 spacing: 6, | ||||
|                 children: [ | ||||
|                   Text(data.nick).fontSize(20), | ||||
|                   if (data.verification != null) | ||||
|                     VerificationMark(mark: data.verification!), | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       '@${data.name}', | ||||
|                       maxLines: 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ).fontSize(14).opacity(0.85), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               if (data.type == 0 && data.account != null) | ||||
|                 Row( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   spacing: 6, | ||||
|                   children: [ | ||||
|                     Icon( | ||||
|                       data.type == 0 ? Symbols.person : Symbols.workspaces, | ||||
|                       fill: 1, | ||||
|                       size: 17, | ||||
|                     ), | ||||
|                     Text( | ||||
|                       'publisherBelongsTo'.tr(args: ['@${data.account!.name}']), | ||||
|                     ).fontSize(14), | ||||
|                   ], | ||||
|                 ).opacity(0.85).padding(bottom: 6), | ||||
|               if (data.type == 0 && data.account != null) | ||||
|                 AccountStatusWidget( | ||||
|                   uname: data.account!.name, | ||||
|                   padding: EdgeInsets.zero, | ||||
|                 ), | ||||
|               subStatus | ||||
|                   .when( | ||||
|                     data: | ||||
|                         (status) => FilledButton.icon( | ||||
|                           onPressed: | ||||
|                               subscribing.value | ||||
|                                   ? null | ||||
|                                   : (status.isSubscribed | ||||
|                                       ? unsubscribe | ||||
|                                       : subscribe), | ||||
|                           icon: Icon( | ||||
|                             status.isSubscribed | ||||
|                                 ? Symbols.remove_circle | ||||
|                                 : Symbols.add_circle, | ||||
|                           ), | ||||
|                           label: | ||||
|                               Text( | ||||
|                                 status.isSubscribed | ||||
|                                     ? 'unsubscribe' | ||||
|                                     : 'subscribe', | ||||
|                               ).tr(), | ||||
|                           style: ButtonStyle( | ||||
|                             visualDensity: VisualDensity(vertical: -2), | ||||
|                           ), | ||||
|                         ), | ||||
|                     error: (_, _) => const SizedBox(), | ||||
|                     loading: | ||||
|                         () => const SizedBox( | ||||
|                           height: 36, | ||||
|                           child: Center( | ||||
|                             child: SizedBox( | ||||
|                               width: 20, | ||||
|                               height: 20, | ||||
|                               child: CircularProgressIndicator(strokeWidth: 2), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                   ) | ||||
|                   .padding(top: 8), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ).padding(horizontal: 24, top: 24); | ||||
|  | ||||
|     Widget publisherVerificationWidget(SnPublisher data) => Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           if (badges.value?.isNotEmpty ?? false) | ||||
|             BadgeList(badges: badges.value!).padding(top: 16), | ||||
|           if (data.verification != null) | ||||
|             VerificationStatusCard(mark: data.verification!), | ||||
|         ], | ||||
|       ), | ||||
|     ).padding(top: 16); | ||||
|  | ||||
|     Widget publisherDetailWidget(SnPublisher data) => Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         children: [ | ||||
|           Text('bio').tr().bold().padding(bottom: 2), | ||||
|           Text(data.bio.isEmpty ? 'descriptionNone'.tr() : data.bio), | ||||
|         ], | ||||
|       ).padding(horizontal: 20, vertical: 16), | ||||
|     ); | ||||
|  | ||||
|     return publisher.when( | ||||
|       data: | ||||
|           (data) => AppScaffold( | ||||
|             noBackground: false, | ||||
|             body: CustomScrollView( | ||||
|               slivers: [ | ||||
|                 SliverAppBar( | ||||
|                   foregroundColor: appbarColor.value, | ||||
|                   expandedHeight: 180, | ||||
|                   pinned: true, | ||||
|                   leading: PageBackButton( | ||||
|                     color: appbarColor.value, | ||||
|                     shadows: [appbarShadow], | ||||
|                   ), | ||||
|                   flexibleSpace: Stack( | ||||
|                     children: [ | ||||
|                       Positioned.fill( | ||||
|                         child: | ||||
|                             data.background?.id != null | ||||
|                                 ? CloudImageWidget(file: data.background) | ||||
|                                 : Container( | ||||
|                                   color: | ||||
|                                       Theme.of( | ||||
|                                         context, | ||||
|                                       ).appBarTheme.backgroundColor, | ||||
|                                 ), | ||||
|             isNoBackground: false, | ||||
|             appBar: | ||||
|                 isWideScreen(context) | ||||
|                     ? AppBar( | ||||
|                       foregroundColor: appbarColor.value, | ||||
|                       leading: PageBackButton( | ||||
|                         color: appbarColor.value, | ||||
|                         shadows: [appbarShadow], | ||||
|                       ), | ||||
|                       FlexibleSpaceBar( | ||||
|                         title: Text( | ||||
|                           data.nick, | ||||
|                           style: TextStyle( | ||||
|                             color: | ||||
|                                 appbarColor.value ?? | ||||
|                                 Theme.of(context).appBarTheme.foregroundColor, | ||||
|                       flexibleSpace: Stack( | ||||
|                         children: [ | ||||
|                           Positioned.fill( | ||||
|                             child: | ||||
|                                 data.background?.id != null | ||||
|                                     ? CloudImageWidget(file: data.background) | ||||
|                                     : Container( | ||||
|                                       color: | ||||
|                                           Theme.of( | ||||
|                                             context, | ||||
|                                           ).appBarTheme.backgroundColor, | ||||
|                                     ), | ||||
|                           ), | ||||
|                           FlexibleSpaceBar( | ||||
|                             title: Text( | ||||
|                               data.nick, | ||||
|                               style: TextStyle( | ||||
|                                 color: | ||||
|                                     appbarColor.value ?? | ||||
|                                     Theme.of( | ||||
|                                       context, | ||||
|                                     ).appBarTheme.foregroundColor, | ||||
|                                 shadows: [appbarShadow], | ||||
|                               ), | ||||
|                             ), | ||||
|                             background: | ||||
|                                 Container(), // Empty container since background is handled by Stack | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ) | ||||
|                     : null, | ||||
|             body: | ||||
|                 isWideScreen(context) | ||||
|                     ? Row( | ||||
|                       children: [ | ||||
|                         Flexible( | ||||
|                           flex: 4, | ||||
|                           child: CustomScrollView( | ||||
|                             slivers: [ | ||||
|                               SliverGap(16), | ||||
|                               SliverPostList(pubName: name), | ||||
|                               SliverGap( | ||||
|                                 MediaQuery.of(context).padding.bottom + 16, | ||||
|                               ), | ||||
|                             ], | ||||
|                           ).padding(left: 8), | ||||
|                         ), | ||||
|                         Flexible( | ||||
|                           flex: 3, | ||||
|                           child: Align( | ||||
|                             alignment: Alignment.topLeft, | ||||
|                             child: SingleChildScrollView( | ||||
|                               child: Column( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                 children: [ | ||||
|                                   publisherBasisWidget(data), | ||||
|                                   publisherVerificationWidget(data), | ||||
|                                   publisherDetailWidget(data), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ) | ||||
|                     : CustomScrollView( | ||||
|                       slivers: [ | ||||
|                         SliverAppBar( | ||||
|                           foregroundColor: appbarColor.value, | ||||
|                           expandedHeight: 180, | ||||
|                           pinned: true, | ||||
|                           leading: PageBackButton( | ||||
|                             color: appbarColor.value, | ||||
|                             shadows: [appbarShadow], | ||||
|                           ), | ||||
|                         ), | ||||
|                         background: | ||||
|                             Container(), // Empty container since background is handled by Stack | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Row( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     spacing: 20, | ||||
|                     children: [ | ||||
|                       GestureDetector( | ||||
|                         child: Badge( | ||||
|                           isLabelVisible: data.type == 0, | ||||
|                           padding: EdgeInsets.all(4), | ||||
|                           label: Icon( | ||||
|                             Symbols.launch, | ||||
|                             size: 16, | ||||
|                             color: Theme.of(context).colorScheme.onPrimary, | ||||
|                           ), | ||||
|                           backgroundColor: | ||||
|                               Theme.of(context).colorScheme.primary, | ||||
|                           offset: Offset(0, 48), | ||||
|                           child: ProfilePictureWidget( | ||||
|                             file: data.picture, | ||||
|                             radius: 32, | ||||
|                           ), | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           Navigator.pop(context, true); | ||||
|                           if (data.account?.name != null) { | ||||
|                             context.pushNamed( | ||||
|                               'accountProfile', | ||||
|                               pathParameters: {'name': data.account!.name}, | ||||
|                             ); | ||||
|                           } | ||||
|                         }, | ||||
|                       ), | ||||
|                       Expanded( | ||||
|                         child: Column( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                           children: [ | ||||
|                             Row( | ||||
|                               spacing: 6, | ||||
|                               children: [ | ||||
|                                 Text(data.nick).fontSize(20), | ||||
|                                 if (data.verification != null) | ||||
|                                   VerificationMark(mark: data.verification!), | ||||
|                                 Text( | ||||
|                                   '@${data.name}', | ||||
|                                 ).fontSize(14).opacity(0.85), | ||||
|                               ], | ||||
|                             ), | ||||
|                             if (data.type == 0 && data.account != null) | ||||
|                               Row( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                                 spacing: 6, | ||||
|                                 children: [ | ||||
|                                   Icon( | ||||
|                                     data.type == 0 | ||||
|                                         ? Symbols.person | ||||
|                                         : Symbols.workspaces, | ||||
|                                     fill: 1, | ||||
|                                     size: 17, | ||||
|                                   ), | ||||
|                                   Text( | ||||
|                                     'publisherBelongsTo'.tr( | ||||
|                                       args: ['@${data.account!.name}'], | ||||
|                                     ), | ||||
|                                   ).fontSize(14), | ||||
|                                 ], | ||||
|                               ).opacity(0.85).padding(bottom: 6), | ||||
|                             if (data.type == 0 && data.account != null) | ||||
|                               AccountStatusWidget( | ||||
|                                 uname: data.account!.name, | ||||
|                                 padding: EdgeInsets.zero, | ||||
|                           flexibleSpace: Stack( | ||||
|                             children: [ | ||||
|                               Positioned.fill( | ||||
|                                 child: | ||||
|                                     data.background?.id != null | ||||
|                                         ? CloudImageWidget( | ||||
|                                           file: data.background, | ||||
|                                         ) | ||||
|                                         : Container( | ||||
|                                           color: | ||||
|                                               Theme.of( | ||||
|                                                 context, | ||||
|                                               ).appBarTheme.backgroundColor, | ||||
|                                         ), | ||||
|                               ), | ||||
|                             subStatus | ||||
|                                 .when( | ||||
|                                   data: | ||||
|                                       (status) => FilledButton.icon( | ||||
|                                         onPressed: | ||||
|                                             subscribing.value | ||||
|                                                 ? null | ||||
|                                                 : (status.isSubscribed | ||||
|                                                     ? unsubscribe | ||||
|                                                     : subscribe), | ||||
|                                         icon: Icon( | ||||
|                                           status.isSubscribed | ||||
|                                               ? Symbols.remove_circle | ||||
|                                               : Symbols.add_circle, | ||||
|                                         ), | ||||
|                                         label: | ||||
|                                             Text( | ||||
|                                               status.isSubscribed | ||||
|                                                   ? 'unsubscribe' | ||||
|                                                   : 'subscribe', | ||||
|                                             ).tr(), | ||||
|                                         style: ButtonStyle( | ||||
|                                           visualDensity: VisualDensity( | ||||
|                                             vertical: -2, | ||||
|                                           ), | ||||
|                                         ), | ||||
|                                       ), | ||||
|                                   error: (_, _) => const SizedBox(), | ||||
|                                   loading: | ||||
|                                       () => const SizedBox( | ||||
|                                         height: 36, | ||||
|                                         child: Center( | ||||
|                                           child: SizedBox( | ||||
|                                             width: 20, | ||||
|                                             height: 20, | ||||
|                                             child: CircularProgressIndicator( | ||||
|                                               strokeWidth: 2, | ||||
|                                             ), | ||||
|                                           ), | ||||
|                                         ), | ||||
|                                       ), | ||||
|                                 ) | ||||
|                                 .padding(top: 8), | ||||
|                           ], | ||||
|                               FlexibleSpaceBar( | ||||
|                                 title: Text( | ||||
|                                   data.nick, | ||||
|                                   style: TextStyle( | ||||
|                                     color: | ||||
|                                         appbarColor.value ?? | ||||
|                                         Theme.of( | ||||
|                                           context, | ||||
|                                         ).appBarTheme.foregroundColor, | ||||
|                                     shadows: [appbarShadow], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 background: | ||||
|                                     Container(), // Empty container since background is handled by Stack | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 24, top: 24), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Column( | ||||
|                     children: [ | ||||
|                       if (badges.value?.isNotEmpty ?? false) | ||||
|                         BadgeList(badges: badges.value!).padding(top: 16), | ||||
|                       if (data.verification != null) | ||||
|                         VerificationStatusCard( | ||||
|                           mark: data.verification!, | ||||
|                         ).padding(top: 16), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 24), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: const Divider(height: 1).padding(vertical: 24), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                     children: [ | ||||
|                       Text('bio').tr().bold(), | ||||
|                       Text( | ||||
|                         data.bio.isEmpty ? 'descriptionNone'.tr() : data.bio, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 24), | ||||
|                 ), | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: const Divider(height: 1).padding(top: 24), | ||||
|                 ), | ||||
|                 SliverPostList(pubName: name), | ||||
|                 SliverGap(MediaQuery.of(context).padding.bottom + 16), | ||||
|               ], | ||||
|             ), | ||||
|                         SliverToBoxAdapter(child: publisherBasisWidget(data)), | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: publisherVerificationWidget(data), | ||||
|                         ), | ||||
|                         SliverToBoxAdapter(child: publisherDetailWidget(data)), | ||||
|                         SliverPostList(pubName: name), | ||||
|                         SliverGap(MediaQuery.of(context).padding.bottom + 16), | ||||
|                       ], | ||||
|                     ), | ||||
|           ), | ||||
|       error: | ||||
|           (error, stackTrace) => AppScaffold( | ||||
|             isNoBackground: false, | ||||
|             appBar: AppBar(leading: const PageBackButton()), | ||||
|             body: Center(child: Text(error.toString())), | ||||
|           ), | ||||
|       loading: | ||||
|           () => AppScaffold( | ||||
|             isNoBackground: false, | ||||
|             appBar: AppBar(leading: const PageBackButton()), | ||||
|             body: Center(child: CircularProgressIndicator()), | ||||
|           ), | ||||
|   | ||||
| @@ -79,7 +79,7 @@ class RealmDetailScreen extends HookConsumerWidget { | ||||
|     ); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       body: realmState.when( | ||||
|         loading: () => const Center(child: CircularProgressIndicator()), | ||||
|         error: (error, _) => Center(child: Text('Error: $error')), | ||||
| @@ -321,10 +321,10 @@ class _RealmActionMenu extends HookConsumerWidget { | ||||
|                               showConfirmAlert( | ||||
|                                 'leaveRealmHint'.tr(), | ||||
|                                 'leaveRealm'.tr(), | ||||
|                               ).then((confirm) { | ||||
|                               ).then((confirm) async { | ||||
|                                 if (confirm) { | ||||
|                                   final client = ref.watch(apiClientProvider); | ||||
|                                   client.delete( | ||||
|                                   await client.delete( | ||||
|                                     '/sphere/realms/$realmSlug/members/me', | ||||
|                                   ); | ||||
|                                   ref.invalidate(realmsJoinedProvider); | ||||
| @@ -361,10 +361,12 @@ class _RealmActionMenu extends HookConsumerWidget { | ||||
|                       showConfirmAlert( | ||||
|                         'leaveRealmHint'.tr(), | ||||
|                         'leaveRealm'.tr(), | ||||
|                       ).then((confirm) { | ||||
|                       ).then((confirm) async { | ||||
|                         if (confirm) { | ||||
|                           final client = ref.watch(apiClientProvider); | ||||
|                           client.delete('/sphere/realms/$realmSlug/members/me'); | ||||
|                           await client.delete( | ||||
|                             '/sphere/realms/$realmSlug/members/me', | ||||
|                           ); | ||||
|                           ref.invalidate(realmsJoinedProvider); | ||||
|                           if (context.mounted) { | ||||
|                             context.pop(true); | ||||
|   | ||||
| @@ -41,7 +41,7 @@ class RealmListScreen extends HookConsumerWidget { | ||||
|     final realmInvites = ref.watch(realmInvitesProvider); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         title: const Text('realms').tr(), | ||||
|         actions: [ | ||||
| @@ -279,7 +279,7 @@ class EditRealmScreen extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()), | ||||
|         leading: const PageBackButton(), | ||||
|   | ||||
| @@ -552,7 +552,7 @@ class SettingsScreen extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     return AppScaffold( | ||||
|       noBackground: false, | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         title: Text('settings').tr(), | ||||
|         actions: | ||||
|   | ||||
| @@ -140,30 +140,27 @@ class VerificationStatusCard extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       margin: EdgeInsets.zero, | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Icon( | ||||
|             mark.type == 4 | ||||
|                 ? Symbols.play_circle | ||||
|                 : mark.type == 0 | ||||
|                 ? Symbols.build_circle | ||||
|                 : Symbols.verified, | ||||
|             size: 32, | ||||
|             color: kVerificationMarkColors[mark.type], | ||||
|             fill: 1, | ||||
|           ), | ||||
|           const Gap(8), | ||||
|           Text(mark.title ?? 'No title').bold(), | ||||
|           Text(mark.description ?? 'descriptionNone'.tr()), | ||||
|           const Gap(6), | ||||
|           Text( | ||||
|             'Verified by\n${mark.verifiedBy ?? 'No one verified it'}', | ||||
|           ).fontSize(11).opacity(0.8), | ||||
|         ], | ||||
|       ).padding(horizontal: 24, vertical: 16), | ||||
|     ); | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Icon( | ||||
|           mark.type == 4 | ||||
|               ? Symbols.play_circle | ||||
|               : mark.type == 0 | ||||
|               ? Symbols.build_circle | ||||
|               : Symbols.verified, | ||||
|           size: 32, | ||||
|           color: kVerificationMarkColors[mark.type], | ||||
|           fill: 1, | ||||
|         ), | ||||
|         const Gap(8), | ||||
|         Text(mark.title ?? 'No title').bold(), | ||||
|         Text(mark.description ?? 'descriptionNone'.tr()), | ||||
|         const Gap(6), | ||||
|         Text( | ||||
|           'Verified by\n${mark.verifiedBy ?? 'No one verified it'}', | ||||
|         ).fontSize(11).opacity(0.8), | ||||
|       ], | ||||
|     ).padding(horizontal: 24, vertical: 16); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -167,6 +167,7 @@ Future<void> showAccountProfileCard( | ||||
|     offset: offset ?? Offset.zero, | ||||
|     context: context, | ||||
|     builder: (context) => AccountProfileCard(uname: uname), | ||||
|     alignment: Alignment.center, | ||||
|     dimBackground: true, | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,8 @@ class FortuneGraphWidget extends HookConsumerWidget { | ||||
|  | ||||
|   final String? eventCalanderUser; | ||||
|  | ||||
|   final EdgeInsets? margin; | ||||
|  | ||||
|   const FortuneGraphWidget({ | ||||
|     super.key, | ||||
|     required this.events, | ||||
| @@ -34,6 +36,7 @@ class FortuneGraphWidget extends HookConsumerWidget { | ||||
|     this.height = 180, | ||||
|     this.onPointSelected, | ||||
|     this.eventCalanderUser, | ||||
|     this.margin, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -249,7 +252,7 @@ class FortuneGraphWidget extends HookConsumerWidget { | ||||
|     if (constrainWidth) { | ||||
|       return ConstrainedBox( | ||||
|         constraints: BoxConstraints(maxWidth: maxWidth), | ||||
|         child: Card(margin: EdgeInsets.all(16), child: content), | ||||
|         child: Card(margin: margin ?? EdgeInsets.all(16), child: content), | ||||
|       ).center(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -86,6 +86,7 @@ class AccountStatusCreationWidget extends HookConsumerWidget { | ||||
|       onTap: () { | ||||
|         showModalBottomSheet( | ||||
|           context: context, | ||||
|           useRootNavigator: true, | ||||
|           builder: | ||||
|               (context) => AccountStatusCreationSheet( | ||||
|                 initialStatus: | ||||
|   | ||||
| @@ -49,7 +49,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget { | ||||
|         final user = ref.watch(userInfoProvider); | ||||
|         final apiClient = ref.read(apiClientProvider); | ||||
|         await apiClient.request( | ||||
|           '/accounts/me/statuses', | ||||
|           '/id/accounts/me/statuses', | ||||
|           data: { | ||||
|             'attitude': attitude.value, | ||||
|             'is_invisible': isInvisible.value, | ||||
|   | ||||
| @@ -165,7 +165,7 @@ class AppScaffold extends StatelessWidget { | ||||
|   final AppBar? appBar; | ||||
|   final DrawerCallback? onDrawerChanged; | ||||
|   final DrawerCallback? onEndDrawerChanged; | ||||
|   final bool? noBackground; | ||||
|   final bool? isNoBackground; | ||||
|   final bool? extendBody; | ||||
|  | ||||
|   const AppScaffold({ | ||||
| @@ -181,7 +181,7 @@ class AppScaffold extends StatelessWidget { | ||||
|     this.endDrawer, | ||||
|     this.onDrawerChanged, | ||||
|     this.onEndDrawerChanged, | ||||
|     this.noBackground, | ||||
|     this.isNoBackground, | ||||
|     this.extendBody, | ||||
|   }); | ||||
|  | ||||
| @@ -190,7 +190,7 @@ class AppScaffold extends StatelessWidget { | ||||
|     final appBarHeight = appBar?.preferredSize.height ?? 0; | ||||
|     final safeTop = MediaQuery.of(context).padding.top; | ||||
|  | ||||
|     final noBackground = this.noBackground ?? isWideScreen(context); | ||||
|     final noBackground = isNoBackground ?? isWideScreen(context); | ||||
|  | ||||
|     final content = Column( | ||||
|       children: [ | ||||
|   | ||||
| @@ -4,9 +4,11 @@ import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/call.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/chat/call_participant_tile.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:livekit_client/livekit_client.dart'; | ||||
|  | ||||
| @@ -20,8 +22,10 @@ class CallControlsBar extends HookConsumerWidget { | ||||
|  | ||||
|     return Container( | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), | ||||
|       child: Row( | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|       child: Wrap( | ||||
|         alignment: WrapAlignment.center, | ||||
|         runSpacing: 16, | ||||
|         spacing: 16, | ||||
|         children: [ | ||||
|           _buildCircularButtonWithDropdown( | ||||
|             context: context, | ||||
| @@ -33,7 +37,6 @@ class CallControlsBar extends HookConsumerWidget { | ||||
|             hasDropdown: true, | ||||
|             deviceType: 'videoinput', | ||||
|           ), | ||||
|           const Gap(16), | ||||
|           _buildCircularButton( | ||||
|             icon: | ||||
|                 callState.isScreenSharing | ||||
| @@ -42,7 +45,6 @@ class CallControlsBar extends HookConsumerWidget { | ||||
|             onPressed: () => callNotifier.toggleScreenShare(), | ||||
|             backgroundColor: const Color(0xFF424242), | ||||
|           ), | ||||
|           const Gap(16), | ||||
|           _buildCircularButtonWithDropdown( | ||||
|             context: context, | ||||
|             ref: ref, | ||||
| @@ -52,10 +54,62 @@ class CallControlsBar extends HookConsumerWidget { | ||||
|             hasDropdown: true, | ||||
|             deviceType: 'audioinput', | ||||
|           ), | ||||
|           const Gap(16), | ||||
|           _buildCircularButton( | ||||
|             icon: | ||||
|                 callState.isSpeakerphone | ||||
|                     ? Symbols.mobile_speaker | ||||
|                     : Symbols.ear_sound, | ||||
|             onPressed: () => callNotifier.toggleSpeakerphone(), | ||||
|             backgroundColor: const Color(0xFF424242), | ||||
|           ), | ||||
|           _buildCircularButton( | ||||
|             icon: Icons.call_end, | ||||
|             onPressed: () => callNotifier.disconnect(), | ||||
|             onPressed: | ||||
|                 () => showModalBottomSheet( | ||||
|                   context: context, | ||||
|                   isScrollControlled: true, | ||||
|                   useRootNavigator: true, | ||||
|                   builder: | ||||
|                       (innerContext) => Column( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         children: [ | ||||
|                           ListTile( | ||||
|                             leading: const Icon(Symbols.logout, fill: 1), | ||||
|                             title: Text('callLeave').tr(), | ||||
|                             onTap: () { | ||||
|                               callNotifier.disconnect(); | ||||
|                               Navigator.of(context).pop(); | ||||
|                               Navigator.of(innerContext).pop(); | ||||
|                             }, | ||||
|                           ), | ||||
|                           ListTile( | ||||
|                             leading: const Icon(Symbols.call_end, fill: 1), | ||||
|                             iconColor: Colors.red, | ||||
|                             title: Text('callEnd').tr(), | ||||
|                             onTap: () async { | ||||
|                               callNotifier.disconnect(); | ||||
|                               final apiClient = ref.watch(apiClientProvider); | ||||
|                               try { | ||||
|                                 showLoadingModal(context); | ||||
|                                 await apiClient.delete( | ||||
|                                   '/sphere/chat/realtime/${callNotifier.roomId}', | ||||
|                                 ); | ||||
|                                 callNotifier.dispose(); | ||||
|                                 if (context.mounted) { | ||||
|                                   Navigator.of(context).pop(); | ||||
|                                   Navigator.of(innerContext).pop(); | ||||
|                                 } | ||||
|                               } catch (err) { | ||||
|                                 showErrorAlert(err); | ||||
|                               } finally { | ||||
|                                 if (context.mounted) hideLoadingModal(context); | ||||
|                               } | ||||
|                             }, | ||||
|                           ), | ||||
|                           Gap(MediaQuery.of(context).padding.bottom), | ||||
|                         ], | ||||
|                       ), | ||||
|                 ), | ||||
|             backgroundColor: const Color(0xFFE53E3E), | ||||
|             iconColor: Colors.white, | ||||
|           ), | ||||
| @@ -212,24 +266,14 @@ class CallControlsBar extends HookConsumerWidget { | ||||
|       } | ||||
|  | ||||
|       if (context.mounted) { | ||||
|         ScaffoldMessenger.of(context).showSnackBar( | ||||
|           SnackBar( | ||||
|             content: Text( | ||||
|               '${'switchedTo'.tr()} ${device.label.isNotEmpty ? device.label : 'selectedDevice'.tr()}', | ||||
|             ), | ||||
|             backgroundColor: Colors.green, | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       if (context.mounted) { | ||||
|         ScaffoldMessenger.of(context).showSnackBar( | ||||
|           SnackBar( | ||||
|             content: Text('${'failedToSwitchDevice'.tr()}: $e'), | ||||
|             backgroundColor: Colors.red, | ||||
|         showSnackBar( | ||||
|           'switchedTo'.tr( | ||||
|             args: [device.label.isNotEmpty ? device.label : 'device'], | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       showErrorAlert(err); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -279,7 +323,7 @@ class CallOverlayBar extends HookConsumerWidget { | ||||
|       child: Card( | ||||
|         margin: EdgeInsets.zero, | ||||
|         child: Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: [ | ||||
|             Expanded( | ||||
|               child: Row( | ||||
| @@ -294,17 +338,7 @@ class CallOverlayBar extends HookConsumerWidget { | ||||
|                         height: 40, | ||||
|                         child: | ||||
|                             SpeakingRippleAvatar( | ||||
|                               isSpeaking: lastSpeaker.isSpeaking, | ||||
|                               audioLevel: | ||||
|                                   lastSpeaker.remoteParticipant.audioLevel, | ||||
|                               pictureId: | ||||
|                                   lastSpeaker | ||||
|                                       .participant | ||||
|                                       .profile | ||||
|                                       ?.account | ||||
|                                       .profile | ||||
|                                       .picture | ||||
|                                       ?.id, | ||||
|                               live: lastSpeaker, | ||||
|                               size: 36, | ||||
|                             ).center(), | ||||
|                       ); | ||||
| @@ -314,10 +348,7 @@ class CallOverlayBar extends HookConsumerWidget { | ||||
|                   Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         lastSpeaker.participant.profile?.account.nick ?? | ||||
|                             'unknown'.tr(), | ||||
|                       ).bold(), | ||||
|                       Text('@${lastSpeaker.participant.identity}').bold(), | ||||
|                       Text( | ||||
|                         formatDuration(callState.duration), | ||||
|                         style: Theme.of(context).textTheme.bodySmall, | ||||
| @@ -360,7 +391,10 @@ class CallOverlayBar extends HookConsumerWidget { | ||||
|         ).padding(all: 16), | ||||
|       ), | ||||
|       onTap: () { | ||||
|         context.pushNamed('chatCall', pathParameters: {'id': callNotifier.roomId!}); | ||||
|         context.pushNamed( | ||||
|           'chatCall', | ||||
|           pathParameters: {'id': callNotifier.roomId!}, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										123
									
								
								lib/widgets/chat/call_participant_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								lib/widgets/chat/call_participant_card.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:flutter_popup_card/flutter_popup_card.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/call.dart'; | ||||
| import 'package:island/widgets/account/account_nameplate.dart'; | ||||
| import 'package:livekit_client/livekit_client.dart'; | ||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class CallParticipantCard extends HookConsumerWidget { | ||||
|   final CallParticipantLive live; | ||||
|   const CallParticipantCard({super.key, required this.live}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final width = | ||||
|         math.min(MediaQuery.of(context).size.width - 80, 360).toDouble(); | ||||
|     final callNotifier = ref.watch(callNotifierProvider.notifier); | ||||
|  | ||||
|     final volumeSliderValue = useState(callNotifier.getParticipantVolume(live)); | ||||
|  | ||||
|     return PopupCard( | ||||
|       elevation: 8, | ||||
|       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), | ||||
|       child: SizedBox( | ||||
|         width: width, | ||||
|         child: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|           children: [ | ||||
|             Column( | ||||
|               spacing: 4, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.sound_detection_loud_sound, size: 16), | ||||
|                     const Gap(8), | ||||
|                     Expanded( | ||||
|                       child: Slider( | ||||
|                         value: volumeSliderValue.value, | ||||
|                         onChanged: (value) { | ||||
|                           volumeSliderValue.value = value; | ||||
|                         }, | ||||
|                         onChangeEnd: (value) { | ||||
|                           callNotifier.setParticipantVolume(live, value); | ||||
|                         }, | ||||
|                         year2023: true, | ||||
|                         padding: EdgeInsets.zero, | ||||
|                       ), | ||||
|                     ), | ||||
|                     const Gap(8), | ||||
|                     Text( | ||||
|                       '${(volumeSliderValue.value * 100).toStringAsFixed(0)}%', | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 Row( | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.wifi, size: 16), | ||||
|                     const Gap(8), | ||||
|                     Text(switch (live.remoteParticipant.connectionQuality) { | ||||
|                       ConnectionQuality.excellent => 'Excellent', | ||||
|                       ConnectionQuality.good => 'Good', | ||||
|                       ConnectionQuality.poor => 'Bad', | ||||
|                       ConnectionQuality.lost => 'Lost', | ||||
|                       _ => 'Connecting', | ||||
|                     }), | ||||
|                   ], | ||||
|                 ), | ||||
|               ], | ||||
|             ).padding(horizontal: 20, top: 16), | ||||
|             AccountNameplate( | ||||
|               name: live.participant.identity, | ||||
|               isOutlined: false, | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class CallParticipantGestureDetector extends StatelessWidget { | ||||
|   final CallParticipantLive participant; | ||||
|   final Widget child; | ||||
|   const CallParticipantGestureDetector({ | ||||
|     super.key, | ||||
|     required this.participant, | ||||
|     required this.child, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return GestureDetector( | ||||
|       child: child, | ||||
|       onTapDown: (details) { | ||||
|         showCallParticipantCard( | ||||
|           context, | ||||
|           participant, | ||||
|           offset: details.localPosition, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| Future<void> showCallParticipantCard( | ||||
|   BuildContext context, | ||||
|   CallParticipantLive participant, { | ||||
|   Offset? offset, | ||||
| }) async { | ||||
|   await showPopupCard<void>( | ||||
|     offset: offset ?? Offset.zero, | ||||
|     context: context, | ||||
|     builder: (context) => CallParticipantCard(live: participant), | ||||
|     alignment: Alignment.center, | ||||
|     dimBackground: true, | ||||
|   ); | ||||
| } | ||||
| @@ -1,92 +1,127 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/call.dart'; | ||||
| import 'package:island/screens/account/profile.dart'; | ||||
| import 'package:island/widgets/chat/call_participant_card.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:livekit_client/livekit_client.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class SpeakingRippleAvatar extends StatelessWidget { | ||||
|   final bool isSpeaking; | ||||
|   final double audioLevel; | ||||
|   final String? pictureId; | ||||
| class SpeakingRippleAvatar extends HookConsumerWidget { | ||||
|   final CallParticipantLive live; | ||||
|   final double size; | ||||
|  | ||||
|   const SpeakingRippleAvatar({ | ||||
|     super.key, | ||||
|     required this.isSpeaking, | ||||
|     required this.audioLevel, | ||||
|     required this.pictureId, | ||||
|     this.size = 96, | ||||
|   }); | ||||
|   const SpeakingRippleAvatar({super.key, required this.live, this.size = 96}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final account = ref.watch(accountProvider(live.participant.identity)); | ||||
|  | ||||
|     final avatarRadius = size / 2; | ||||
|     final clampedLevel = audioLevel.clamp(0.0, 1.0); | ||||
|     final clampedLevel = live.remoteParticipant.audioLevel.clamp(0.0, 1.0); | ||||
|     final rippleRadius = avatarRadius + clampedLevel * (size * 0.333); | ||||
|     return 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) | ||||
|     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( | ||||
|             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), | ||||
|                   ), | ||||
|                 ), | ||||
|               Container( | ||||
|                 width: animatedRadius * 2, | ||||
|                 height: animatedRadius * 2, | ||||
|                 decoration: BoxDecoration( | ||||
|                   shape: BoxShape.circle, | ||||
|                   color: Colors.green.withOpacity(0.75 + 0.25 * clampedLevel), | ||||
|                 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(), | ||||
|                       ), | ||||
|                 ), | ||||
|               ), | ||||
|             Container( | ||||
|               width: size, | ||||
|               height: size, | ||||
|               alignment: Alignment.center, | ||||
|               decoration: BoxDecoration(shape: BoxShape.circle), | ||||
|               child: ProfilePictureWidget(fileId: pictureId, radius: size / 2), | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|               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), | ||||
|                   ), | ||||
|                 ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class CallParticipantTile extends StatelessWidget { | ||||
| class CallParticipantTile extends HookConsumerWidget { | ||||
|   final CallParticipantLive live; | ||||
|  | ||||
|   const CallParticipantTile({super.key, required this.live}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final hasVideo = | ||||
|         live.hasVideo && | ||||
|         live.remoteParticipant.trackPublications.values | ||||
|             .where((pub) => pub.track != null && pub.kind == TrackType.VIDEO) | ||||
|             .isNotEmpty; | ||||
|     final audioLevel = live.remoteParticipant.audioLevel; | ||||
|  | ||||
|     if (hasVideo) { | ||||
|       return Stack( | ||||
|         fit: StackFit.loose, | ||||
|         children: [ | ||||
|           Container( | ||||
|             color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|             child: AspectRatio( | ||||
|               aspectRatio: 16 / 9, | ||||
|               child: VideoTrackRenderer( | ||||
|                 live.remoteParticipant.trackPublications.values | ||||
|                         .where((track) => track.kind == TrackType.VIDEO) | ||||
|                         .first | ||||
|                         .track | ||||
|                     as VideoTrack, | ||||
|                 renderMode: VideoRenderMode.platformView, | ||||
|               ), | ||||
|           AspectRatio( | ||||
|             aspectRatio: 16 / 9, | ||||
|             child: VideoTrackRenderer( | ||||
|               live.remoteParticipant.trackPublications.values | ||||
|                       .where((track) => track.kind == TrackType.VIDEO) | ||||
|                       .first | ||||
|                       .track | ||||
|                   as VideoTrack, | ||||
|               renderMode: VideoRenderMode.platformView, | ||||
|             ), | ||||
|           ), | ||||
|           Positioned( | ||||
| @@ -94,21 +129,26 @@ class CallParticipantTile extends StatelessWidget { | ||||
|             right: 8, | ||||
|             bottom: 8, | ||||
|             child: Text( | ||||
|               live.participant.profile?.account.nick ?? | ||||
|                   '${'unknown'.tr()}\'s video', | ||||
|               '@${live.participant.name}', | ||||
|               textAlign: TextAlign.center, | ||||
|               style: const TextStyle(fontSize: 14, color: Colors.white), | ||||
|               style: const TextStyle( | ||||
|                 fontSize: 14, | ||||
|                 color: Colors.white, | ||||
|                 shadows: [ | ||||
|                   BoxShadow( | ||||
|                     color: Colors.black54, | ||||
|                     offset: Offset(1, 1), | ||||
|                     spreadRadius: 8, | ||||
|                     blurRadius: 8, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
|     } else { | ||||
|       return SpeakingRippleAvatar( | ||||
|         isSpeaking: live.isSpeaking, | ||||
|         audioLevel: audioLevel, | ||||
|         pictureId: live.participant.profile?.account.profile.picture?.id, | ||||
|         size: 84, | ||||
|       ); | ||||
|       return SpeakingRippleAvatar(size: 84, live: live); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -5,16 +5,19 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/database/message.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/models/embed.dart'; | ||||
| import 'package:island/pods/call.dart'; | ||||
| import 'package:island/pods/translate.dart'; | ||||
| import 'package:island/screens/chat/room.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| import 'package:island/widgets/account/account_pfc.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/content/alert.native.dart'; | ||||
| import 'package:island/widgets/content/cloud_file_collection.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/content/embed/link.dart'; | ||||
| @@ -67,6 +70,46 @@ class MessageItem extends HookConsumerWidget { | ||||
|  | ||||
|     final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS); | ||||
|  | ||||
|     final messageLanguage = | ||||
|         remoteMessage.content != null | ||||
|             ? ref.watch(detectStringLanguageProvider(remoteMessage.content!)) | ||||
|             : null; | ||||
|  | ||||
|     final currentLanguage = context.locale.toString(); | ||||
|     final translatableLanguage = | ||||
|         messageLanguage != null | ||||
|             ? messageLanguage.substring(0, 2) != currentLanguage.substring(0, 2) | ||||
|             : false; | ||||
|  | ||||
|     final translating = useState(false); | ||||
|     final translatedText = useState<String?>(null); | ||||
|  | ||||
|     Future<void> translate() async { | ||||
|       if (translatedText.value != null) { | ||||
|         translatedText.value = null; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (translating.value) return; | ||||
|       if (remoteMessage.content == null) return; | ||||
|       translating.value = true; | ||||
|       try { | ||||
|         final text = await ref.watch( | ||||
|           translateStringProvider( | ||||
|             TranslateQuery( | ||||
|               text: remoteMessage.content!, | ||||
|               lang: currentLanguage.substring(0, 2), | ||||
|             ), | ||||
|           ).future, | ||||
|         ); | ||||
|         translatedText.value = text; | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|         translating.value = false; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return ContextMenuWidget( | ||||
|       menuProvider: (_) { | ||||
|         if (onAction == null) return Menu(children: []); | ||||
| @@ -103,6 +146,18 @@ class MessageItem extends HookConsumerWidget { | ||||
|                 onAction!.call(MessageItemAction.forward); | ||||
|               }, | ||||
|             ), | ||||
|             if (translatableLanguage) MenuSeparator(), | ||||
|             if (translatableLanguage) | ||||
|               MenuAction( | ||||
|                 title: | ||||
|                     translatedText.value == null | ||||
|                         ? 'translate'.tr() | ||||
|                         : translating.value | ||||
|                         ? 'translating'.tr() | ||||
|                         : 'translated'.tr(), | ||||
|                 image: MenuImage.icon(Symbols.translate), | ||||
|                 callback: translate, | ||||
|               ), | ||||
|             if (isMobile) MenuSeparator(), | ||||
|             if (isMobile) | ||||
|               MenuAction( | ||||
| @@ -221,14 +276,18 @@ class MessageItem extends HookConsumerWidget { | ||||
|                               isReply: false, | ||||
|                             ).padding(vertical: 4), | ||||
|                           if (_MessageItemContent.hasContent(remoteMessage)) | ||||
|                             _MessageItemContent(item: remoteMessage), | ||||
|                             _MessageItemContent( | ||||
|                               item: remoteMessage, | ||||
|                               translatedText: translatedText.value, | ||||
|                             ), | ||||
|                           if (remoteMessage.attachments.isNotEmpty) | ||||
|                             LayoutBuilder( | ||||
|                               builder: (context, constraints) { | ||||
|                                 return CloudFileList( | ||||
|                                   files: remoteMessage.attachments, | ||||
|                                   maxWidth: constraints.maxWidth, | ||||
|                                 ).padding(vertical: 4); | ||||
|                                   padding: EdgeInsets.symmetric(vertical: 4), | ||||
|                                 ); | ||||
|                               }, | ||||
|                             ), | ||||
|                           if (remoteMessage.meta['embeds'] != null) | ||||
| @@ -481,7 +540,8 @@ class MessageQuoteWidget extends HookConsumerWidget { | ||||
|  | ||||
| class _MessageItemContent extends StatelessWidget { | ||||
|   final SnChatMessage item; | ||||
|   const _MessageItemContent({required this.item}); | ||||
|   final String? translatedText; | ||||
|   const _MessageItemContent({required this.item, this.translatedText}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -494,10 +554,40 @@ class _MessageItemContent extends StatelessWidget { | ||||
|         ); | ||||
|       case 'text': | ||||
|       default: | ||||
|         return MarkdownTextContent( | ||||
|           content: item.content!, | ||||
|           isSelectable: true, | ||||
|           linesMargin: EdgeInsets.zero, | ||||
|         return Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             MarkdownTextContent( | ||||
|               content: item.content!, | ||||
|               isSelectable: true, | ||||
|               linesMargin: EdgeInsets.zero, | ||||
|             ), | ||||
|             if (translatedText?.isNotEmpty ?? false) | ||||
|               ...([ | ||||
|                 ConstrainedBox( | ||||
|                   constraints: BoxConstraints( | ||||
|                     maxWidth: math.min( | ||||
|                       280, | ||||
|                       MediaQuery.of(context).size.width * 0.4, | ||||
|                     ), | ||||
|                   ), | ||||
|                   child: Row( | ||||
|                     mainAxisSize: MainAxisSize.min, | ||||
|                     children: [ | ||||
|                       Text('translated').tr().fontSize(11).opacity(0.75), | ||||
|                       const Gap(8), | ||||
|                       Flexible(child: Divider()), | ||||
|                     ], | ||||
|                   ).padding(vertical: 4), | ||||
|                 ), | ||||
|                 MarkdownTextContent( | ||||
|                   content: translatedText!, | ||||
|                   isSelectable: true, | ||||
|                   linesMargin: EdgeInsets.zero, | ||||
|                 ), | ||||
|               ]), | ||||
|           ], | ||||
|         ); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -35,7 +35,8 @@ Future<SnCheckInResult?> checkInResultToday(Ref ref) async { | ||||
| } | ||||
|  | ||||
| class CheckInWidget extends HookConsumerWidget { | ||||
|   const CheckInWidget({super.key}); | ||||
|   final EdgeInsets? margin; | ||||
|   const CheckInWidget({super.key, this.margin}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -66,7 +67,8 @@ class CheckInWidget extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     return Card( | ||||
|       margin: EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8), | ||||
|       margin: | ||||
|           margin ?? EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8), | ||||
|       child: Row( | ||||
|         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|         spacing: 16, | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:path/path.dart' show extension; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:photo_view/photo_view.dart'; | ||||
| @@ -27,14 +28,16 @@ class CloudFileList extends HookConsumerWidget { | ||||
|   final double? minWidth; | ||||
|   final bool disableZoomIn; | ||||
|   final bool disableConstraint; | ||||
|   final EdgeInsets? padding; | ||||
|   const CloudFileList({ | ||||
|     super.key, | ||||
|     required this.files, | ||||
|     this.maxHeight = 360, | ||||
|     this.maxHeight = 560, | ||||
|     this.maxWidth = double.infinity, | ||||
|     this.minWidth, | ||||
|     this.disableZoomIn = false, | ||||
|     this.disableConstraint = false, | ||||
|     this.padding, | ||||
|   }); | ||||
|  | ||||
|   double calculateAspectRatio() { | ||||
| @@ -60,22 +63,17 @@ class CloudFileList extends HookConsumerWidget { | ||||
|     if (files.isEmpty) return const SizedBox.shrink(); | ||||
|     if (files.length == 1) { | ||||
|       final isImage = files.first.mimeType?.startsWith('image') ?? false; | ||||
|       return ConstrainedBox( | ||||
|       return Container( | ||||
|         padding: padding, | ||||
|         constraints: BoxConstraints( | ||||
|           maxHeight: disableConstraint ? double.infinity : maxHeight, | ||||
|           minWidth: minWidth ?? 0, | ||||
|           maxWidth: | ||||
|               files.length == 1 | ||||
|                   ? math.max( | ||||
|                     math.min(520, MediaQuery.of(context).size.width * 0.85), | ||||
|                     minWidth ?? 0, | ||||
|                   ) | ||||
|                   : double.infinity, | ||||
|           maxWidth: files.length == 1 ? maxWidth : double.infinity, | ||||
|         ), | ||||
|         child: AspectRatio( | ||||
|           aspectRatio: calculateAspectRatio(), | ||||
|           child: ClipRRect( | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|             child: _CloudFileListEntry( | ||||
|               file: files.first, | ||||
|               heroTag: heroTags.first, | ||||
| @@ -95,7 +93,7 @@ class CloudFileList extends HookConsumerWidget { | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ).padding(horizontal: 3); | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final allImages = | ||||
| @@ -109,22 +107,37 @@ class CloudFileList extends HookConsumerWidget { | ||||
|         child: AspectRatio( | ||||
|           aspectRatio: calculateAspectRatio(), | ||||
|           child: CarouselView( | ||||
|             padding: padding, | ||||
|             itemSnapping: true, | ||||
|             itemExtent: math.min( | ||||
|               MediaQuery.of(context).size.width * 0.85, | ||||
|               maxWidth * 0.85, | ||||
|             ), | ||||
|             itemSnapping: true, | ||||
|             shape: RoundedRectangleBorder( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|             ), | ||||
|             children: [ | ||||
|               for (var i = 0; i < files.length; i++) | ||||
|                 _CloudFileListEntry( | ||||
|                   file: files[i], | ||||
|                   heroTag: heroTags[i], | ||||
|                   isImage: files[i].mimeType?.startsWith('image') ?? false, | ||||
|                   disableZoomIn: disableZoomIn, | ||||
|                   fit: BoxFit.cover, | ||||
|                 Stack( | ||||
|                   children: [ | ||||
|                     _CloudFileListEntry( | ||||
|                       file: files[i], | ||||
|                       heroTag: heroTags[i], | ||||
|                       isImage: files[i].mimeType?.startsWith('image') ?? false, | ||||
|                       disableZoomIn: disableZoomIn, | ||||
|                     ), | ||||
|                     Positioned( | ||||
|                       bottom: 12, | ||||
|                       left: 16, | ||||
|                       child: Text('${i + 1}/${files.length}') | ||||
|                           .textColor(Colors.white) | ||||
|                           .textShadow( | ||||
|                             color: Colors.black54, | ||||
|                             offset: Offset(1, 1), | ||||
|                             blurRadius: 3, | ||||
|                           ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|             ], | ||||
|             onTap: (i) { | ||||
| @@ -150,28 +163,52 @@ class CloudFileList extends HookConsumerWidget { | ||||
|         child: ListView.separated( | ||||
|           scrollDirection: Axis.horizontal, | ||||
|           itemCount: files.length, | ||||
|           padding: EdgeInsets.symmetric(horizontal: 3), | ||||
|           padding: padding, | ||||
|           itemBuilder: (context, index) { | ||||
|             return ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|               child: _CloudFileListEntry( | ||||
|                 file: files[index], | ||||
|                 heroTag: heroTags[index], | ||||
|                 isImage: files[index].mimeType?.startsWith('image') ?? false, | ||||
|                 disableZoomIn: disableZoomIn, | ||||
|                 onTap: () { | ||||
|                   if (!(files[index].mimeType?.startsWith('image') ?? false)) { | ||||
|                     return; | ||||
|                   } | ||||
|                   if (!disableZoomIn) { | ||||
|                     context.pushTransparentRoute( | ||||
|                       CloudFileZoomIn( | ||||
|                         item: files[index], | ||||
|                         heroTag: heroTags[index], | ||||
|                       ), | ||||
|                     ); | ||||
|                   } | ||||
|                 }, | ||||
|             return AspectRatio( | ||||
|               aspectRatio: | ||||
|                   files[index].fileMeta?['ratio'] is num | ||||
|                       ? files[index].fileMeta!['ratio'].toDouble() | ||||
|                       : 1.0, | ||||
|               child: Stack( | ||||
|                 children: [ | ||||
|                   ClipRRect( | ||||
|                     borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|                     child: _CloudFileListEntry( | ||||
|                       file: files[index], | ||||
|                       heroTag: heroTags[index], | ||||
|                       isImage: | ||||
|                           files[index].mimeType?.startsWith('image') ?? false, | ||||
|                       disableZoomIn: disableZoomIn, | ||||
|                       onTap: () { | ||||
|                         if (!(files[index].mimeType?.startsWith('image') ?? | ||||
|                             false)) { | ||||
|                           return; | ||||
|                         } | ||||
|                         if (!disableZoomIn) { | ||||
|                           context.pushTransparentRoute( | ||||
|                             CloudFileZoomIn( | ||||
|                               item: files[index], | ||||
|                               heroTag: heroTags[index], | ||||
|                             ), | ||||
|                             rootNavigator: true, | ||||
|                           ); | ||||
|                         } | ||||
|                       }, | ||||
|                     ), | ||||
|                   ), | ||||
|                   Positioned( | ||||
|                     bottom: 12, | ||||
|                     left: 16, | ||||
|                     child: Text('${index + 1}/${files.length}') | ||||
|                         .textColor(Colors.white) | ||||
|                         .textShadow( | ||||
|                           color: Colors.black54, | ||||
|                           offset: Offset(1, 1), | ||||
|                           blurRadius: 3, | ||||
|                         ), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
| @@ -193,6 +230,8 @@ class CloudFileZoomIn extends HookConsumerWidget { | ||||
|     final photoViewController = useMemoized(() => PhotoViewController(), []); | ||||
|     final rotation = useState(0); | ||||
|  | ||||
|     final showOriginal = useState(false); | ||||
|  | ||||
|     Future<void> saveToGallery() async { | ||||
|       try { | ||||
|         // Show loading indicator | ||||
| @@ -206,7 +245,7 @@ class CloudFileZoomIn extends HookConsumerWidget { | ||||
|         final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}'; | ||||
|  | ||||
|         await client.download( | ||||
|           '/files/${item.id}', | ||||
|           '/drive/files/${item.id}', | ||||
|           filePath, | ||||
|           queryParameters: {'original': true}, | ||||
|         ); | ||||
| @@ -356,7 +395,7 @@ class CloudFileZoomIn extends HookConsumerWidget { | ||||
|               imageProvider: CloudImageWidget.provider( | ||||
|                 fileId: item.id, | ||||
|                 serverUrl: serverUrl, | ||||
|                 original: true, | ||||
|                 original: showOriginal.value, | ||||
|               ), | ||||
|               // Apply rotation transformation | ||||
|               customSize: MediaQuery.of(context).size, | ||||
| @@ -390,6 +429,23 @@ class CloudFileZoomIn extends HookConsumerWidget { | ||||
|                         saveToGallery(); | ||||
|                       }, | ||||
|                     ), | ||||
|                     IconButton( | ||||
|                       onPressed: () { | ||||
|                         showOriginal.value = !showOriginal.value; | ||||
|                       }, | ||||
|                       icon: Icon( | ||||
|                         showOriginal.value ? Symbols.raw_on : Symbols.raw_off, | ||||
|                         color: Colors.white, | ||||
|                         size: 24, | ||||
|                         shadows: [ | ||||
|                           Shadow( | ||||
|                             color: Colors.black54, | ||||
|                             blurRadius: 5.0, | ||||
|                             offset: Offset(1.0, 1.0), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 IconButton( | ||||
| @@ -497,7 +553,6 @@ class _CloudFileListEntry extends StatelessWidget { | ||||
|   final bool isImage; | ||||
|   final bool disableZoomIn; | ||||
|   final VoidCallback? onTap; | ||||
|   final BoxFit fit; | ||||
|  | ||||
|   const _CloudFileListEntry({ | ||||
|     required this.file, | ||||
| @@ -505,7 +560,6 @@ class _CloudFileListEntry extends StatelessWidget { | ||||
|     required this.isImage, | ||||
|     required this.disableZoomIn, | ||||
|     this.onTap, | ||||
|     this.fit = BoxFit.contain, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -528,10 +582,10 @@ class _CloudFileListEntry extends StatelessWidget { | ||||
|             item: file, | ||||
|             heroTag: heroTag, | ||||
|             noBlurhash: true, | ||||
|             fit: fit, | ||||
|             fit: BoxFit.contain, | ||||
|           ) | ||||
|         else | ||||
|           CloudFileWidget(item: file, heroTag: heroTag, fit: fit), | ||||
|           CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain), | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @@ -45,7 +47,7 @@ class CloudFileWidget extends ConsumerWidget { | ||||
|       ), | ||||
|       "video" => AspectRatio( | ||||
|         aspectRatio: ratio, | ||||
|         child: UniversalVideo(uri: uri, aspectRatio: ratio), | ||||
|         child: CloudVideoWidget(item: item), | ||||
|       ), | ||||
|       _ => Text('Unable render for ${item.mimeType}'), | ||||
|     }; | ||||
| @@ -58,6 +60,119 @@ class CloudFileWidget extends ConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class CloudVideoWidget extends HookConsumerWidget { | ||||
|   final SnCloudFile item; | ||||
|   const CloudVideoWidget({super.key, required this.item}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final open = useState(false); | ||||
|  | ||||
|     final serverUrl = ref.watch(serverUrlProvider); | ||||
|     final uri = '$serverUrl/drive/files/${item.id}'; | ||||
|  | ||||
|     var ratio = | ||||
|         item.fileMeta?['ratio'] is num | ||||
|             ? item.fileMeta!['ratio'].toDouble() | ||||
|             : 1.0; | ||||
|     if (ratio == 0) ratio = 1.0; | ||||
|  | ||||
|     if (open.value) { | ||||
|       return UniversalVideo(uri: uri, aspectRatio: ratio, autoplay: true); | ||||
|     } | ||||
|  | ||||
|     return GestureDetector( | ||||
|       child: Stack( | ||||
|         children: [ | ||||
|           UniversalImage(uri: '$uri?thumbnail=true'), | ||||
|           Positioned.fill( | ||||
|             child: Center( | ||||
|               child: const Icon( | ||||
|                 Symbols.play_arrow, | ||||
|                 fill: 1, | ||||
|                 size: 32, | ||||
|                 shadows: [ | ||||
|                   BoxShadow( | ||||
|                     color: Colors.black54, | ||||
|                     offset: Offset(1, 1), | ||||
|                     spreadRadius: 8, | ||||
|                     blurRadius: 8, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           Positioned( | ||||
|             bottom: 0, | ||||
|             left: 0, | ||||
|             right: 0, | ||||
|             child: Column( | ||||
|               mainAxisAlignment: MainAxisAlignment.end, | ||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   spacing: 8, | ||||
|                   children: [ | ||||
|                     if (item.fileMeta?['duration'] != null) | ||||
|                       Text( | ||||
|                         Duration( | ||||
|                           milliseconds: | ||||
|                               ((item.fileMeta?['duration'] as num) * 1000) | ||||
|                                   .toInt(), | ||||
|                         ).formatDuration(), | ||||
|                         style: TextStyle( | ||||
|                           shadows: [ | ||||
|                             BoxShadow( | ||||
|                               color: Colors.black54, | ||||
|                               offset: Offset(1, 1), | ||||
|                               spreadRadius: 8, | ||||
|                               blurRadius: 8, | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                     if (item.fileMeta?['bit_rate'] != null) | ||||
|                       Text( | ||||
|                         '${int.parse(item.fileMeta?['bit_rate'] as String) ~/ 1000} Kbps', | ||||
|                         style: TextStyle( | ||||
|                           shadows: [ | ||||
|                             BoxShadow( | ||||
|                               color: Colors.black54, | ||||
|                               offset: Offset(1, 1), | ||||
|                               spreadRadius: 8, | ||||
|                               blurRadius: 8, | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 Text( | ||||
|                   item.name, | ||||
|                   style: TextStyle( | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                     shadows: [ | ||||
|                       BoxShadow( | ||||
|                         color: Colors.black54, | ||||
|                         offset: Offset(1, 1), | ||||
|                         spreadRadius: 8, | ||||
|                         blurRadius: 8, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ).padding(horizontal: 16, bottom: 12), | ||||
|         ], | ||||
|       ), | ||||
|       onTap: () { | ||||
|         open.value = true; | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class CloudImageWidget extends ConsumerWidget { | ||||
|   final String? fileId; | ||||
|   final SnCloudFile? file; | ||||
| @@ -92,7 +207,10 @@ class CloudImageWidget extends ConsumerWidget { | ||||
|     required String serverUrl, | ||||
|     bool original = false, | ||||
|   }) { | ||||
|     final uri = '$serverUrl/drive/files/$fileId?original=$original'; | ||||
|     final uri = | ||||
|         original | ||||
|             ? '$serverUrl/drive/files/$fileId?original=true' | ||||
|             : '$serverUrl/drive/files/$fileId'; | ||||
|     return CachedNetworkImageProvider(uri); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -81,7 +81,10 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|               if (url != null) { | ||||
|                 if (url.scheme == 'solian') { | ||||
|                   if (url.host == 'account') { | ||||
|                     context.pushNamed('accountProfile', pathParameters: {'name': url.pathSegments[0]}); | ||||
|                     context.pushNamed( | ||||
|                       'accountProfile', | ||||
|                       pathParameters: {'name': url.pathSegments[0]}, | ||||
|                     ); | ||||
|                   } | ||||
|                   return; | ||||
|                 } | ||||
| @@ -153,7 +156,7 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|                         ), | ||||
|                         child: UniversalImage( | ||||
|                           uri: | ||||
|                               '$baseUrl/stickers/lookup/${uri.pathSegments[0]}/open', | ||||
|                               '$baseUrl/sphere/stickers/lookup/${uri.pathSegments[0]}/open', | ||||
|                           width: size, | ||||
|                           height: size, | ||||
|                           fit: BoxFit.cover, | ||||
|   | ||||
| @@ -11,10 +11,12 @@ import 'package:media_kit_video/media_kit_video.dart'; | ||||
| class UniversalVideo extends ConsumerStatefulWidget { | ||||
|   final String uri; | ||||
|   final double aspectRatio; | ||||
|   final bool autoplay; | ||||
|   const UniversalVideo({ | ||||
|     super.key, | ||||
|     required this.uri, | ||||
|     this.aspectRatio = 16 / 9, | ||||
|     this.autoplay = false, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -47,7 +49,7 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> { | ||||
|       log('[MediaPlayer] Hit cache: $url'); | ||||
|     } | ||||
|  | ||||
|     _player!.open(Media(uri), play: false); | ||||
|     _player!.open(Media(uri), play: widget.autoplay); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -4,10 +4,12 @@ import 'package:flutter/material.dart'; | ||||
| class UniversalVideo extends StatelessWidget { | ||||
|   final String uri; | ||||
|   final double aspectRatio; | ||||
|   final bool autoplay; | ||||
|   const UniversalVideo({ | ||||
|     super.key, | ||||
|     required this.uri, | ||||
|     required this.aspectRatio, | ||||
|     this.autoplay = false, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| @@ -13,7 +14,10 @@ import 'package:island/pods/network.dart'; | ||||
| import 'package:island/services/file.dart'; | ||||
| import 'package:island/services/compose_storage_db.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:pasteboard/pasteboard.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| @@ -60,6 +64,9 @@ class ComposeState { | ||||
|     _autoSaveTimer?.cancel(); | ||||
|     _autoSaveTimer = null; | ||||
|   } | ||||
|  | ||||
|   bool get isEmpty => | ||||
|       attachments.value.isEmpty && contentController.text.isEmpty; | ||||
| } | ||||
|  | ||||
| class ComposeLogic { | ||||
| @@ -392,6 +399,95 @@ class ComposeLogic { | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   static Future<void> linkAttachment( | ||||
|     WidgetRef ref, | ||||
|     ComposeState state, | ||||
|     BuildContext context, | ||||
|   ) async { | ||||
|     final TextEditingController idController = TextEditingController(); | ||||
|     String? errorMessage; | ||||
|  | ||||
|     await showModalBottomSheet( | ||||
|       context: context, | ||||
|       builder: (BuildContext dialogContext) { | ||||
|         return StatefulBuilder( | ||||
|           builder: (context, setState) { | ||||
|             return SheetScaffold( | ||||
|               titleText: 'linkAttachment'.tr(), | ||||
|               child: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                 children: [ | ||||
|                   TextField( | ||||
|                     controller: idController, | ||||
|                     decoration: InputDecoration( | ||||
|                       labelText: 'fileId'.tr(), | ||||
|                       helperText: 'fileIdHint'.tr(), | ||||
|                       helperMaxLines: 3, | ||||
|                       errorText: errorMessage, | ||||
|                       border: OutlineInputBorder(), | ||||
|                     ), | ||||
|                   ), | ||||
|                   const Gap(16), | ||||
|                   Align( | ||||
|                     alignment: Alignment.centerRight, | ||||
|                     child: TextButton.icon( | ||||
|                       icon: const Icon(Symbols.add), | ||||
|                       label: Text('add'.tr()), | ||||
|                       onPressed: () async { | ||||
|                         final fileId = idController.text.trim(); | ||||
|                         if (fileId.isEmpty) { | ||||
|                           setState(() { | ||||
|                             errorMessage = 'fileIdCannotBeEmpty'.tr(); | ||||
|                           }); | ||||
|                           return; | ||||
|                         } | ||||
|  | ||||
|                         try { | ||||
|                           final client = ref.read(apiClientProvider); | ||||
|                           final response = await client.get( | ||||
|                             '/drive/files/$fileId/info', | ||||
|                           ); | ||||
|                           final SnCloudFile cloudFile = SnCloudFile.fromJson( | ||||
|                             response.data, | ||||
|                           ); | ||||
|  | ||||
|                           state.attachments.value = [ | ||||
|                             ...state.attachments.value, | ||||
|                             UniversalFile( | ||||
|                               data: cloudFile, | ||||
|                               type: switch (cloudFile.mimeType | ||||
|                                   ?.split('/') | ||||
|                                   .firstOrNull) { | ||||
|                                 'image' => UniversalFileType.image, | ||||
|                                 'video' => UniversalFileType.video, | ||||
|                                 'audio' => UniversalFileType.audio, | ||||
|                                 _ => UniversalFileType.file, | ||||
|                               }, | ||||
|                             ), | ||||
|                           ]; | ||||
|                           if (context.mounted) { | ||||
|                             Navigator.of(dialogContext).pop(); | ||||
|                           } | ||||
|                         } catch (e) { | ||||
|                           setState(() { | ||||
|                             errorMessage = 'failedToFetchFile'.tr( | ||||
|                               args: [e.toString()], | ||||
|                             ); | ||||
|                           }); | ||||
|                         } | ||||
|                       }, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ], | ||||
|               ).padding(horizontal: 24, vertical: 24), | ||||
|             ); | ||||
|           }, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static Future<void> uploadAttachment( | ||||
|     WidgetRef ref, | ||||
|     ComposeState state, | ||||
|   | ||||
							
								
								
									
										107
									
								
								lib/widgets/post/compose_toolbar.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								lib/widgets/post/compose_toolbar.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/services/compose_storage_db.dart'; | ||||
| import 'package:island/widgets/post/compose_shared.dart'; | ||||
| import 'package:island/widgets/post/draft_manager.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class ComposeToolbar extends HookConsumerWidget { | ||||
|   final ComposeState state; | ||||
|   final SnPost? originalPost; | ||||
|  | ||||
|   const ComposeToolbar({super.key, required this.state, this.originalPost}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     void pickPhotoMedia() { | ||||
|       ComposeLogic.pickPhotoMedia(ref, state); | ||||
|     } | ||||
|  | ||||
|     void pickVideoMedia() { | ||||
|       ComposeLogic.pickVideoMedia(ref, state); | ||||
|     } | ||||
|  | ||||
|     void linkAttachment() { | ||||
|       ComposeLogic.linkAttachment(ref, state, context); | ||||
|     } | ||||
|  | ||||
|     void saveDraft() { | ||||
|       ComposeLogic.saveDraft(ref, state); | ||||
|     } | ||||
|  | ||||
|     void showDraftManager() { | ||||
|       showModalBottomSheet( | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         builder: | ||||
|             (context) => DraftManagerSheet( | ||||
|               onDraftSelected: (draftId) { | ||||
|                 final draft = ref.read(composeStorageNotifierProvider)[draftId]; | ||||
|                 if (draft != null) { | ||||
|                   state.titleController.text = draft.title ?? ''; | ||||
|                   state.descriptionController.text = draft.description ?? ''; | ||||
|                   state.contentController.text = draft.content ?? ''; | ||||
|                   state.visibility.value = draft.visibility; | ||||
|                 } | ||||
|               }, | ||||
|             ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final colorScheme = Theme.of(context).colorScheme; | ||||
|  | ||||
|     return Material( | ||||
|       elevation: 4, | ||||
|       child: Center( | ||||
|         child: ConstrainedBox( | ||||
|           constraints: const BoxConstraints(maxWidth: 560), | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               IconButton( | ||||
|                 onPressed: pickPhotoMedia, | ||||
|                 tooltip: 'addPhoto'.tr(), | ||||
|                 icon: const Icon(Symbols.add_a_photo), | ||||
|                 color: colorScheme.primary, | ||||
|               ), | ||||
|               IconButton( | ||||
|                 onPressed: pickVideoMedia, | ||||
|                 tooltip: 'addVideo'.tr(), | ||||
|                 icon: const Icon(Symbols.videocam), | ||||
|                 color: colorScheme.primary, | ||||
|               ), | ||||
|               IconButton( | ||||
|                 onPressed: linkAttachment, | ||||
|                 icon: const Icon(Symbols.attach_file), | ||||
|                 tooltip: 'linkAttachment'.tr(), | ||||
|                 color: colorScheme.primary, | ||||
|               ), | ||||
|               const Spacer(), | ||||
|               if (originalPost == null && state.isEmpty) | ||||
|                 IconButton( | ||||
|                   icon: const Icon(Symbols.draft), | ||||
|                   color: colorScheme.primary, | ||||
|                   onPressed: showDraftManager, | ||||
|                   tooltip: 'drafts'.tr(), | ||||
|                 ) | ||||
|               else if (originalPost == null) | ||||
|                 IconButton( | ||||
|                   icon: const Icon(Symbols.save), | ||||
|                   color: colorScheme.primary, | ||||
|                   onPressed: saveDraft, | ||||
|                   onLongPress: showDraftManager, | ||||
|                   tooltip: 'saveDraft'.tr(), | ||||
|                 ), | ||||
|             ], | ||||
|           ).padding( | ||||
|             bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|             horizontal: 16, | ||||
|             top: 8, | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										152
									
								
								lib/widgets/post/post_item.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								lib/widgets/post/post_item.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'post_item.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$postFeaturedReplyHash() => r'3f0ac0d51ad21f8754a63dd94109eb8ac4812293'; | ||||
|  | ||||
| /// 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 [postFeaturedReply]. | ||||
| @ProviderFor(postFeaturedReply) | ||||
| const postFeaturedReplyProvider = PostFeaturedReplyFamily(); | ||||
|  | ||||
| /// See also [postFeaturedReply]. | ||||
| class PostFeaturedReplyFamily extends Family<AsyncValue<SnPost?>> { | ||||
|   /// See also [postFeaturedReply]. | ||||
|   const PostFeaturedReplyFamily(); | ||||
|  | ||||
|   /// See also [postFeaturedReply]. | ||||
|   PostFeaturedReplyProvider call(String id) { | ||||
|     return PostFeaturedReplyProvider(id); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   PostFeaturedReplyProvider getProviderOverride( | ||||
|     covariant PostFeaturedReplyProvider provider, | ||||
|   ) { | ||||
|     return call(provider.id); | ||||
|   } | ||||
|  | ||||
|   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'postFeaturedReplyProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [postFeaturedReply]. | ||||
| class PostFeaturedReplyProvider extends AutoDisposeFutureProvider<SnPost?> { | ||||
|   /// See also [postFeaturedReply]. | ||||
|   PostFeaturedReplyProvider(String id) | ||||
|     : this._internal( | ||||
|         (ref) => postFeaturedReply(ref as PostFeaturedReplyRef, id), | ||||
|         from: postFeaturedReplyProvider, | ||||
|         name: r'postFeaturedReplyProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$postFeaturedReplyHash, | ||||
|         dependencies: PostFeaturedReplyFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             PostFeaturedReplyFamily._allTransitiveDependencies, | ||||
|         id: id, | ||||
|       ); | ||||
|  | ||||
|   PostFeaturedReplyProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.id, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String id; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<SnPost?> Function(PostFeaturedReplyRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: PostFeaturedReplyProvider._internal( | ||||
|         (ref) => create(ref as PostFeaturedReplyRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         id: id, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<SnPost?> createElement() { | ||||
|     return _PostFeaturedReplyProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is PostFeaturedReplyProvider && other.id == id; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, id.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin PostFeaturedReplyRef on AutoDisposeFutureProviderRef<SnPost?> { | ||||
|   /// The parameter `id` of this provider. | ||||
|   String get id; | ||||
| } | ||||
|  | ||||
| class _PostFeaturedReplyProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<SnPost?> | ||||
|     with PostFeaturedReplyRef { | ||||
|   _PostFeaturedReplyProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get id => (origin as PostFeaturedReplyProvider).id; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| @@ -45,11 +45,13 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|               title: 'edit'.tr(), | ||||
|               image: MenuImage.icon(Symbols.edit), | ||||
|               callback: () { | ||||
|                 context.pushNamed('postEdit', pathParameters: {'id': item.id}).then((value) { | ||||
|                   if (value != null) { | ||||
|                     onRefresh?.call(); | ||||
|                   } | ||||
|                 }); | ||||
|                 context | ||||
|                     .pushNamed('postEdit', pathParameters: {'id': item.id}) | ||||
|                     .then((value) { | ||||
|                       if (value != null) { | ||||
|                         onRefresh?.call(); | ||||
|                       } | ||||
|                     }); | ||||
|               }, | ||||
|             ), | ||||
|             MenuAction( | ||||
| @@ -80,7 +82,10 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|               image: MenuImage.icon(Symbols.link), | ||||
|               callback: () { | ||||
|                 // Copy post link to clipboard | ||||
|                 context.pushNamed('postDetail', pathParameters: {'id': item.id}); | ||||
|                 context.pushNamed( | ||||
|                   'postDetail', | ||||
|                   pathParameters: {'id': item.id}, | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
| @@ -88,8 +93,6 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|       }, | ||||
|       child: Material( | ||||
|         color: backgroundColor ?? Theme.of(context).colorScheme.surface, | ||||
|         borderRadius: BorderRadius.circular(12), | ||||
|         elevation: 1, | ||||
|         child: InkWell( | ||||
|           borderRadius: BorderRadius.circular(12), | ||||
|           onTap: () { | ||||
| @@ -197,8 +200,8 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|           CloudFileList( | ||||
|             files: item.attachments, | ||||
|             maxWidth: MediaQuery.of(context).size.width * 0.85, | ||||
|             minWidth: MediaQuery.of(context).size.width * 0.9, | ||||
|           ).padding(top: 8), | ||||
|             padding: EdgeInsets.only(top: 8), | ||||
|           ), | ||||
|  | ||||
|         // Reference post indicator | ||||
|         if (item.repliedPost != null || item.forwardedPost != null) | ||||
| @@ -211,7 +214,7 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|                   size: 16, | ||||
|                   color: Theme.of(context).colorScheme.secondary, | ||||
|                 ), | ||||
|                 const SizedBox(width: 4), | ||||
|                 const Gap(4), | ||||
|                 Text( | ||||
|                   item.repliedPost != null | ||||
|                       ? 'repliedTo'.tr() | ||||
| @@ -364,6 +367,7 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|         PostReactionList( | ||||
|           parentId: item.id, | ||||
|           reactions: item.reactionsCount, | ||||
|           reactionsMade: item.reactionsMade, | ||||
|           padding: EdgeInsets.zero, | ||||
|         ), | ||||
|         const Gap(16), | ||||
|   | ||||
| @@ -94,9 +94,7 @@ class SliverPostList extends HookConsumerWidget { | ||||
|  | ||||
|               final post = data.items[index]; | ||||
|  | ||||
|               return Column( | ||||
|                 children: [_buildPostItem(post), const Divider(height: 1)], | ||||
|               ); | ||||
|               return _buildPostItem(post); | ||||
|             }, | ||||
|           ), | ||||
|     ); | ||||
| @@ -105,16 +103,24 @@ class SliverPostList extends HookConsumerWidget { | ||||
|   Widget _buildPostItem(SnPost post) { | ||||
|     switch (itemType) { | ||||
|       case PostItemType.creator: | ||||
|         return PostItemCreator( | ||||
|           item: post, | ||||
|           backgroundColor: backgroundColor, | ||||
|           padding: padding, | ||||
|           isOpenable: isOpenable, | ||||
|           onRefresh: onRefresh, | ||||
|           onUpdate: onUpdate, | ||||
|         return Column( | ||||
|           children: [ | ||||
|             PostItemCreator( | ||||
|               item: post, | ||||
|               backgroundColor: backgroundColor, | ||||
|               padding: padding, | ||||
|               isOpenable: isOpenable, | ||||
|               onRefresh: onRefresh, | ||||
|               onUpdate: onUpdate, | ||||
|             ), | ||||
|             const Divider(), | ||||
|           ], | ||||
|         ); | ||||
|       case PostItemType.regular: | ||||
|         return PostItem(item: post); | ||||
|         return Card( | ||||
|           margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|           child: PostActionableItem(item: post, borderRadius: 8), | ||||
|         ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
| @@ -56,17 +55,17 @@ class PostRepliesNotifier extends _$PostRepliesNotifier | ||||
|  | ||||
| class PostRepliesList extends HookConsumerWidget { | ||||
|   final String postId; | ||||
|   final Color? backgroundColor; | ||||
|   final double? maxWidth; | ||||
|   final VoidCallback? onOpen; | ||||
|   const PostRepliesList({ | ||||
|     super.key, | ||||
|     required this.postId, | ||||
|     this.backgroundColor, | ||||
|     this.maxWidth, | ||||
|     this.onOpen, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final isWide = isWideScreen(context); | ||||
|  | ||||
|     return PagingHelperSliverView( | ||||
|       provider: postRepliesNotifierProvider(postId), | ||||
|       futureRefreshable: postRepliesNotifierProvider(postId).future, | ||||
| @@ -93,16 +92,24 @@ class PostRepliesList extends HookConsumerWidget { | ||||
|               return endItemView; | ||||
|             } | ||||
|  | ||||
|             return Column( | ||||
|               children: [ | ||||
|                 PostItem( | ||||
|                   item: data.items[index], | ||||
|                   backgroundColor: | ||||
|                       backgroundColor ?? (isWide ? Colors.transparent : null), | ||||
|                   showReferencePost: false, | ||||
|                 ), | ||||
|                 const Divider(height: 1), | ||||
|               ], | ||||
|             final contentWidget = Card( | ||||
|               margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|               child: PostActionableItem( | ||||
|                 borderRadius: 8, | ||||
|                 item: data.items[index], | ||||
|                 isShowReference: false, | ||||
|                 isEmbedOpenable: true, | ||||
|                 onOpen: onOpen, | ||||
|               ), | ||||
|             ); | ||||
|  | ||||
|             if (maxWidth == null) return contentWidget; | ||||
|  | ||||
|             return Center( | ||||
|               child: ConstrainedBox( | ||||
|                 constraints: BoxConstraints(maxWidth: maxWidth!), | ||||
|                 child: contentWidget, | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
|         ); | ||||
|   | ||||
| @@ -27,7 +27,9 @@ class PostRepliesSheet extends HookConsumerWidget { | ||||
|               slivers: [ | ||||
|                 PostRepliesList( | ||||
|                   postId: post.id.toString(), | ||||
|                   backgroundColor: Colors.transparent, | ||||
|                   onOpen: () { | ||||
|                     Navigator.pop(context); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|   | ||||
| @@ -27,9 +27,13 @@ class PublisherCard extends ConsumerWidget { | ||||
|  | ||||
|     Widget card = Card( | ||||
|       clipBehavior: Clip.antiAlias, | ||||
|       margin: EdgeInsets.zero, | ||||
|       child: InkWell( | ||||
|         onTap: () { | ||||
|           context.pushNamed('publisherProfile', pathParameters: {'name': publisher.name}); | ||||
|           context.pushNamed( | ||||
|             'publisherProfile', | ||||
|             pathParameters: {'name': publisher.name}, | ||||
|           ); | ||||
|         }, | ||||
|         child: AspectRatio( | ||||
|           aspectRatio: 16 / 7, | ||||
|   | ||||
| @@ -29,9 +29,13 @@ class RealmCard extends ConsumerWidget { | ||||
|  | ||||
|     Widget card = Card( | ||||
|       clipBehavior: Clip.antiAlias, | ||||
|       margin: EdgeInsets.zero, | ||||
|       child: InkWell( | ||||
|         onTap: () { | ||||
|           context.pushNamed('realmDetail', pathParameters: {'slug': realm.slug}); | ||||
|           context.pushNamed( | ||||
|             'realmDetail', | ||||
|             pathParameters: {'slug': realm.slug}, | ||||
|           ); | ||||
|         }, | ||||
|         child: AspectRatio( | ||||
|           aspectRatio: 16 / 7, | ||||
|   | ||||
| @@ -28,6 +28,7 @@ class WebArticleCard extends StatelessWidget { | ||||
|     return ConstrainedBox( | ||||
|       constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), | ||||
|       child: Card( | ||||
|         margin: EdgeInsets.zero, | ||||
|         clipBehavior: Clip.antiAlias, | ||||
|         child: InkWell( | ||||
|           onTap: () => _onTap(context), | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
| #include <flutter_udid/flutter_udid_plugin.h> | ||||
| #include <flutter_webrtc/flutter_web_r_t_c_plugin.h> | ||||
| #include <irondash_engine_context/irondash_engine_context_plugin.h> | ||||
| #include <livekit_client/live_kit_plugin.h> | ||||
| #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> | ||||
| #include <media_kit_video/media_kit_video_plugin.h> | ||||
| #include <pasteboard/pasteboard_plugin.h> | ||||
| @@ -48,6 +49,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { | ||||
|   g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); | ||||
|   irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); | ||||
|   g_autoptr(FlPluginRegistrar) livekit_client_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "LiveKitPlugin"); | ||||
|   live_kit_plugin_register_with_registrar(livekit_client_registrar); | ||||
|   g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); | ||||
|   media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); | ||||
|   | ||||
| @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   flutter_udid | ||||
|   flutter_webrtc | ||||
|   irondash_engine_context | ||||
|   livekit_client | ||||
|   media_kit_libs_linux | ||||
|   media_kit_video | ||||
|   pasteboard | ||||
|   | ||||
| @@ -11,34 +11,34 @@ PODS: | ||||
|     - FlutterMacOS | ||||
|   - file_selector_macos (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - Firebase/CoreOnly (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|   - Firebase/Messaging (11.15.0): | ||||
|   - Firebase/CoreOnly (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|   - Firebase/Messaging (12.0.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.15.0) | ||||
|   - firebase_core (3.15.2): | ||||
|     - Firebase/CoreOnly (~> 11.15.0) | ||||
|     - FirebaseMessaging (~> 12.0.0) | ||||
|   - firebase_core (4.0.0): | ||||
|     - Firebase/CoreOnly (~> 12.0.0) | ||||
|     - FlutterMacOS | ||||
|   - firebase_messaging (15.2.10): | ||||
|     - Firebase/CoreOnly (~> 11.15.0) | ||||
|     - Firebase/Messaging (~> 11.15.0) | ||||
|   - firebase_messaging (16.0.0): | ||||
|     - Firebase/CoreOnly (~> 12.0.0) | ||||
|     - Firebase/Messaging (~> 12.0.0) | ||||
|     - firebase_core | ||||
|     - FlutterMacOS | ||||
|   - FirebaseCore (11.15.0): | ||||
|     - FirebaseCoreInternal (~> 11.15.0) | ||||
|   - FirebaseCore (12.0.0): | ||||
|     - FirebaseCoreInternal (~> 12.0.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Logger (~> 8.1) | ||||
|   - FirebaseCoreInternal (11.15.0): | ||||
|   - FirebaseCoreInternal (12.0.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|   - FirebaseInstallations (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|   - FirebaseInstallations (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (11.15.0): | ||||
|     - FirebaseCore (~> 11.15.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleDataTransport (~> 10.0) | ||||
|   - FirebaseMessaging (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|     - FirebaseInstallations (~> 12.0.0) | ||||
|     - GoogleDataTransport (~> 10.1) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Reachability (~> 8.1) | ||||
| @@ -56,9 +56,9 @@ PODS: | ||||
|   - flutter_udid (0.0.1): | ||||
|     - FlutterMacOS | ||||
|     - SAMKeychain | ||||
|   - flutter_webrtc (0.14.0): | ||||
|   - flutter_webrtc (1.0.0): | ||||
|     - FlutterMacOS | ||||
|     - WebRTC-SDK (= 125.6422.07) | ||||
|     - WebRTC-SDK (= 137.7151.02) | ||||
|   - FlutterMacOS (1.0.0) | ||||
|   - gal (1.0.0): | ||||
|     - Flutter | ||||
| @@ -92,10 +92,10 @@ PODS: | ||||
|     - GoogleUtilities/Privacy | ||||
|   - irondash_engine_context (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - livekit_client (2.4.9): | ||||
|   - livekit_client (2.5.0): | ||||
|     - flutter_webrtc | ||||
|     - FlutterMacOS | ||||
|     - WebRTC-SDK (= 125.6422.07) | ||||
|     - WebRTC-SDK (= 137.7151.02) | ||||
|   - local_auth_darwin (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
| @@ -143,6 +143,8 @@ PODS: | ||||
|     - sqlite3/common | ||||
|   - sqlite3/rtree (3.50.3): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/session (3.50.3): | ||||
|     - sqlite3/common | ||||
|   - sqlite3_flutter_libs (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
| @@ -152,6 +154,7 @@ PODS: | ||||
|     - sqlite3/math | ||||
|     - sqlite3/perf-threadsafe | ||||
|     - sqlite3/rtree | ||||
|     - sqlite3/session | ||||
|   - super_native_extensions (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - url_launcher_macos (0.0.1): | ||||
| @@ -160,7 +163,7 @@ PODS: | ||||
|     - FlutterMacOS | ||||
|   - wakelock_plus (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - WebRTC-SDK (125.6422.07) | ||||
|   - WebRTC-SDK (137.7151.02) | ||||
|  | ||||
| DEPENDENCIES: | ||||
|   - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) | ||||
| @@ -291,25 +294,25 @@ SPEC CHECKSUMS: | ||||
|   device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 | ||||
|   file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a | ||||
|   file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 | ||||
|   Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e | ||||
|   firebase_core: 7667f880631ae8ad10e3d6567ab7582fe0682326 | ||||
|   firebase_messaging: df39858bcbbcce792c9e4f1ca51b41123d6181fd | ||||
|   FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e | ||||
|   FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 | ||||
|   FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 | ||||
|   FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09 | ||||
|   Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 | ||||
|   firebase_core: eeea10f64026b68cd0bc3dee079ab4717e22909e | ||||
|   firebase_messaging: 5eefcd5bde556bfacdd9968e11c52f39032dfbe5 | ||||
|   FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a | ||||
|   FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 | ||||
|   FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 | ||||
|   FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde | ||||
|   flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d | ||||
|   flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284 | ||||
|   flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 | ||||
|   flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8 | ||||
|   flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495 | ||||
|   flutter_webrtc: a7eeb54859e672228c28f4b48b1fb61561976ea3 | ||||
|   flutter_webrtc: 0d70bd8782c19bde286dc52f766eebbea26de201 | ||||
|   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 | ||||
|   gal: baecd024ebfd13c441269ca7404792a7152fde89 | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||
|   irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba | ||||
|   livekit_client: c9d9f41996f5cf22b9ba0e8483e6af4ca5094059 | ||||
|   livekit_client: 0b0515e03858b86a7c14cc7fd6f772331f6ee84c | ||||
|   local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19 | ||||
|   media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 | ||||
|   media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 | ||||
| @@ -326,12 +329,12 @@ SPEC CHECKSUMS: | ||||
|   sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e | ||||
|   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 | ||||
|   sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 | ||||
|   sqlite3_flutter_libs: ce0522d143cee6ef5e16587acfce8f476316e005 | ||||
|   sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e | ||||
|   super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 | ||||
|   url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 | ||||
|   volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd | ||||
|   wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497 | ||||
|   WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e | ||||
|   WebRTC-SDK: d20de357dcbf7c9696b124b39f3ff62125107e4b | ||||
|  | ||||
| PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f | ||||
|  | ||||
|   | ||||
							
								
								
									
										84
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										84
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -13,10 +13,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: _flutterfire_internals | ||||
|       sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 | ||||
|       sha256: bb84ee51e527053dd8e25ecc9f97a6abfdc19130fb4d883e4e8585e23e7e6dd8 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.59" | ||||
|     version: "1.3.60" | ||||
|   analyzer: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -421,10 +421,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: dart_webrtc | ||||
|       sha256: "5b76fd85ac95d6f5dee3e7d7de8d4b51bfbec1dc73804647c6aebb52d6297116" | ||||
|       sha256: a2ae542cdadc21359022adedc26138fa3487cc3b3547c24ff4f556681869e28c | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.5.3+hotfix.2" | ||||
|     version: "1.5.3+hotfix.4" | ||||
|   dbus: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -477,10 +477,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: drift | ||||
|       sha256: dce2723fb0dd03563af21f305f8f96514c27f870efba934b4fe84d4fedb4eff7 | ||||
|       sha256: "6aaea757f53bb035e8a3baedf3d1d53a79d6549a6c13d84f7546509da9372c7c" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.28.0" | ||||
|     version: "2.28.1" | ||||
|   drift_dev: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -509,10 +509,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: easy_localization | ||||
|       sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12" | ||||
|       sha256: "2ccdf9db8fe4d9c5a75c122e6275674508fd0f0d49c827354967b8afcc56bbed" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.7+1" | ||||
|     version: "3.0.8" | ||||
|   easy_logger: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -573,10 +573,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: file_picker | ||||
|       sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a | ||||
|       sha256: "13ba4e627ef24503a465d1d61b32596ce10eb6b8903678d362a528f9939b4aa8" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.2.0" | ||||
|     version: "10.2.1" | ||||
|   file_selector_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -613,10 +613,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_core | ||||
|       sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" | ||||
|       sha256: "6b343e6f7b72a4f32d7ce8df8c9a28d8f54b4ac20d7c6500f3e8b3969afca457" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.15.2" | ||||
|     version: "4.0.0" | ||||
|   firebase_core_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -629,34 +629,34 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_core_web | ||||
|       sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" | ||||
|       sha256: "5d28b14dd32282fb7ce2b22b897362453755b6b8541d491127dc72b755bb7b16" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.24.1" | ||||
|     version: "3.0.0" | ||||
|   firebase_messaging: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_messaging | ||||
|       sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc" | ||||
|       sha256: "10272b553a49c13a6cedfd00121047157521f82a5d3f2a1706b9dd28342cc482" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "15.2.10" | ||||
|     version: "16.0.0" | ||||
|   firebase_messaging_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_platform_interface | ||||
|       sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754" | ||||
|       sha256: b846a305feb3f74ee3f0aace447f65a4696bc6550bc828ecf5a84a1b77473d16 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.6.10" | ||||
|     version: "4.7.0" | ||||
|   firebase_messaging_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_web | ||||
|       sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390" | ||||
|       sha256: "28714749880f7242c5fb3b1ee6c66b41f61453f02ae348b43c82957df80b87ae" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.10.10" | ||||
|     version: "4.0.0" | ||||
|   fixnum: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -830,6 +830,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0" | ||||
|   flutter_langdetect: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_langdetect | ||||
|       sha256: "93bd865c7d5723eac614744abb32234ee4f593505a293bc17ef097bd55fbdf38" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.0.2" | ||||
|   flutter_launcher_icons: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -1017,10 +1025,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_webrtc | ||||
|       sha256: "792aa1e5838a719f29ae52c0773dbb5dd781fc33b1bf87c321b274e55ab51ad1" | ||||
|       sha256: "69095ba39b83da3de48286dfc0769aa8e9f10491f70058dc8d8ecc960ef7a260" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.14.2" | ||||
|     version: "1.0.0" | ||||
|   font_awesome_flutter: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1089,10 +1097,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: google_fonts | ||||
|       sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 | ||||
|       sha256: df9763500dadba0155373e9cb44e202ce21bd9ed5de6bdbd05c5854e86839cb8 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.2.1" | ||||
|     version: "6.3.0" | ||||
|   graphs: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1337,10 +1345,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: livekit_client | ||||
|       sha256: "5d182f40cc9aafce60a9acf936bad8bc69010b5cbf0a949f6f27dc4390f2fcce" | ||||
|       sha256: b3db2d8afa8d1dbe4fd8dfc965fc9d661cb51a8d864ad199919575ce919a40fb | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.9" | ||||
|     version: "2.5.0+hotfix.1" | ||||
|   local_auth: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1381,6 +1389,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.11" | ||||
|   logger: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: logger | ||||
|       sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.4.0" | ||||
|   logging: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1433,10 +1449,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: material_symbols_icons | ||||
|       sha256: "7c50901b39d1ad645ee25d920aed008061e1fd541a897b4ebf2c01d966dbf16b" | ||||
|       sha256: ef20d86fb34c2b59eb7553c4d795bb8a7ec8c890c53ffd3148c64f7adc46ae50 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.2815.1" | ||||
|     version: "4.2858.1" | ||||
|   media_kit: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -2166,10 +2182,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite_common | ||||
|       sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" | ||||
|       sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.5.5" | ||||
|     version: "2.5.6" | ||||
|   sqflite_darwin: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2190,18 +2206,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqlite3 | ||||
|       sha256: "608b56d594e4c8498c972c8f1507209f9fd74939971b948ddbbfbfd1c9cb3c15" | ||||
|       sha256: dd806fff004a0aeb01e208b858dbc649bc72104670d425a81a6dd17698535f6e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.7.7" | ||||
|     version: "2.8.0" | ||||
|   sqlite3_flutter_libs: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqlite3_flutter_libs | ||||
|       sha256: "60464aa06f3f6f6fba9abd7564e315526c1fee6d6a77d6ee52a1f7f48a9107f6" | ||||
|       sha256: fd996da5515a73aacd0a04ae7063db5fe8df42670d974df4c3ee538c652eef2e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.37" | ||||
|     version: "0.5.38" | ||||
|   sqlparser: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
							
								
								
									
										55
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								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.1.0+115 | ||||
| version: 3.1.0+116 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.7.2 | ||||
| @@ -46,65 +46,65 @@ dependencies: | ||||
|   path_provider: ^2.1.5 | ||||
|   dio: ^5.8.0+1 | ||||
|   very_good_infinite_list: ^0.9.0 | ||||
|   freezed_annotation: ^3.0.0 | ||||
|   freezed_annotation: ^3.1.0 | ||||
|   json_annotation: ^4.9.0 | ||||
|   flutter_markdown_latex: ^0.3.4 | ||||
|   markdown: ^7.3.0 | ||||
|   flutter_highlight: ^0.7.0 | ||||
|   uuid: ^4.5.1 | ||||
|   url_launcher: ^6.3.1 | ||||
|   google_fonts: ^6.2.1 | ||||
|   url_launcher: ^6.3.2 | ||||
|   google_fonts: ^6.3.0 | ||||
|   gap: ^3.0.1 | ||||
|   cached_network_image: ^3.4.1 | ||||
|   web: ^1.1.1 | ||||
|   flutter_blurhash: ^0.9.0 | ||||
|   flutter_blurhash: ^0.9.1 | ||||
|   media_kit: ^1.2.0 | ||||
|   media_kit_video: ^1.3.0 | ||||
|   media_kit_libs_video: ^1.0.6 | ||||
|   flutter_cache_manager: ^3.4.1 | ||||
|   flutter_platform_alert: ^0.8.0 | ||||
|   email_validator: ^3.0.0 | ||||
|   easy_localization: ^3.0.7+1 | ||||
|   easy_localization: ^3.0.8 | ||||
|   flutter_inappwebview: ^6.1.5 | ||||
|   animations: ^2.0.11 | ||||
|   package_info_plus: ^8.3.0 | ||||
|   device_info_plus: ^11.4.0 | ||||
|   device_info_plus: ^11.5.0 | ||||
|   tus_client_dart: | ||||
|     git: https://github.com/LittleSheep2Code/tus_client.git | ||||
|   cross_file: ^0.3.4+2 | ||||
|   image_picker: ^1.1.2 | ||||
|   file_picker: ^10.1.7 | ||||
|   file_picker: ^10.2.1 | ||||
|   riverpod_annotation: ^2.6.1 | ||||
|   image_picker_platform_interface: ^2.10.1 | ||||
|   image_picker_android: ^0.8.12+23 | ||||
|   super_context_menu: ^0.9.0-dev.6 | ||||
|   image_picker_android: ^0.8.12+24 | ||||
|   super_context_menu: ^0.9.1 | ||||
|   modal_bottom_sheet: ^3.0.0 | ||||
|   firebase_messaging: ^15.2.5 | ||||
|   firebase_messaging: ^16.0.0 | ||||
|   flutter_udid: ^4.0.0 | ||||
|   firebase_core: ^3.13.0 | ||||
|   firebase_core: ^4.0.0 | ||||
|   web_socket_channel: ^3.0.3 | ||||
|   material_symbols_icons: ^4.2815.0 | ||||
|   drift: ^2.26.0 | ||||
|   drift_flutter: ^0.2.4 | ||||
|   material_symbols_icons: ^4.2858.1 | ||||
|   drift: ^2.28.1 | ||||
|   drift_flutter: ^0.2.5 | ||||
|   path: ^1.9.1 | ||||
|   collection: ^1.19.1 | ||||
|   markdown_editor_plus: ^0.2.15 | ||||
|   croppy: ^1.3.6 | ||||
|   table_calendar: ^3.1.3 | ||||
|   table_calendar: ^3.2.0 | ||||
|   relative_time: ^5.0.0 | ||||
|   dropdown_button2: ^2.3.9 | ||||
|   riverpod_paging_utils: ^0.8.0 | ||||
|   riverpod_paging_utils: ^0.8.1 | ||||
|   crypto: ^3.0.6 | ||||
|   avatar_stack: ^3.0.0 | ||||
|   markdown_widget: ^2.3.2+8 | ||||
|   visibility_detector: ^0.4.0+2 | ||||
|   flutter_native_splash: ^2.4.6 | ||||
|   photo_view: ^0.15.0 | ||||
|   gal: ^2.3.1 | ||||
|   gal: ^2.3.2 | ||||
|   dismissible_page: ^1.0.2 | ||||
|   super_sliver_list: ^0.4.1 | ||||
|   flutter_webrtc: ^0.14.1 | ||||
|   livekit_client: ^2.4.7 | ||||
|   flutter_webrtc: ^1.0.0 | ||||
|   livekit_client: ^2.5.0+hotfix.1 | ||||
|   pasteboard: ^0.4.0 | ||||
|   flutter_colorpicker: ^1.1.0 | ||||
|   record: ^6.0.0 | ||||
| @@ -116,7 +116,7 @@ dependencies: | ||||
|   flutter_timezone: ^4.1.1 | ||||
|   fl_chart: ^1.0.0 | ||||
|   sign_in_with_apple: ^7.0.1 | ||||
|   flutter_svg: ^2.1.0 | ||||
|   flutter_svg: ^2.2.0 | ||||
|   native_exif: ^0.6.2 | ||||
|   local_auth: ^2.3.0 | ||||
|   flutter_secure_storage: ^9.2.4 | ||||
| @@ -131,6 +131,7 @@ dependencies: | ||||
|   mime: ^2.0.0 | ||||
|   html2md: ^1.3.2 | ||||
|   flutter_typeahead: ^5.2.0 | ||||
|   flutter_langdetect: ^0.0.2 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
| @@ -142,15 +143,15 @@ dev_dependencies: | ||||
|   # package. See that file for information about deactivating specific lint | ||||
|   # rules and activating additional ones. | ||||
|   flutter_lints: ^6.0.0 | ||||
|   auto_route_generator: ^10.0.1 | ||||
|   build_runner: ^2.4.15 | ||||
|   freezed: ^3.0.6 | ||||
|   auto_route_generator: ^10.1.0 | ||||
|   build_runner: ^2.5.4 | ||||
|   freezed: ^3.1.0 | ||||
|   json_serializable: ^6.9.5 | ||||
|   riverpod_generator: ^2.6.5 | ||||
|   custom_lint: ^0.7.5 | ||||
|   custom_lint: ^0.7.6 | ||||
|   riverpod_lint: ^2.6.5 | ||||
|   drift_dev: ^2.26.0 | ||||
|   flutter_launcher_icons: ^0.14.3 | ||||
|   drift_dev: ^2.28.0 | ||||
|   flutter_launcher_icons: ^0.14.4 | ||||
|  | ||||
| # For information on the generic Dart part of this file, see the | ||||
| # following page: https://dart.dev/tools/pub/pubspec | ||||
|   | ||||
		Reference in New Issue
	
	Block a user