Compare commits
	
		
			90 Commits
		
	
	
		
			7c1f24b824
			...
			v3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 504322c2dd | |||
| a07ec3ca36 | |||
| d96691e920 | |||
| 6273b2d917 | |||
| ab90d244b5 | |||
| dc6af6d9e5 | |||
| 0ca801d963 | |||
| 3edcdd72af | |||
| 402bb3fe04 | |||
| 8ba55eb1be | |||
| 983ae2a1fc | |||
| 6fc94001b3 | |||
| 44dbcfdc94 | |||
| b57caf56db | |||
| dbcd1b6d36 | |||
| a8055de910 | |||
| 49b15e7674 | |||
| e2369c40db | |||
| 44c5d91620 | |||
| 7a5a2407b7 | |||
| 234434f102 | |||
| 9c3b228d02 | |||
| 82682cae9a | |||
| fcbd5fe680 | |||
| ad91b17af7 | |||
| 24fa637329 | |||
| 926ae5402f | |||
| 1a37d384e6 | |||
| d4cf598f69 | |||
| 0106c08891 | |||
| 9697def808 | |||
| 6572875229 | |||
| 66590b9079 | |||
| 08b9604b55 | |||
| 0602bbd277 | |||
| 76e7ba7898 | |||
| 6e6616b236 | |||
| 071d51b25e | |||
| a958362461 | |||
| 6749bb00fe | |||
| 11fb20c673 | |||
| a7990f83db | |||
| 5f4cdf7937 | |||
| 3330ca14dd | |||
| 1719b1c8fe | |||
| 3c2c51bfaf | |||
| 239d6750ff | |||
| 8b0c91977a | |||
| f74cca8464 | |||
| 08091d51bf | |||
| 481190811b | |||
| 4b32b65d1c | |||
| 50ac7109bb | |||
| 62da279c71 | |||
| fde6dbf891 | |||
| 613bf4fb42 | |||
| 00ae586016 | |||
| ea0d132dce | |||
| aa2df1e847 | |||
| 50672795f3 | |||
| 383de9568d | |||
| 01fa228e45 | |||
| 1e71ad33a6 | |||
| 92c0260ecd | |||
| 0a161ad255 | |||
| c003f27b9a | |||
| 19db8309c4 | |||
| aa72ce08e8 | |||
| 4639b00b86 | |||
| cc5460ea55 | |||
| eafac811e6 | |||
| e3be691596 | |||
| aa180a1358 | |||
| c2707b8af1 | |||
| 62fd0500f3 | |||
| eeae865cc8 | |||
| cdf1413fe0 | |||
| 327b4c04f1 | |||
| bd903ce29c | |||
| 1b8ecb15ce | |||
| d4e380a97a | |||
| 126048b4fa | |||
| 8bec18813d | |||
| 1ae81794b1 | |||
| 2a7d12de48 | |||
| 64c60ead48 | |||
| 001549b190 | |||
| 4595865ad3 | |||
|  | 1834643167 | ||
|  | 0e816eaa3e | 
| @@ -871,6 +871,7 @@ | |||||||
|     "pollShortTextAnswerPreview": "Short text answer (preview)", |     "pollShortTextAnswerPreview": "Short text answer (preview)", | ||||||
|     "award": "Award", |     "award": "Award", | ||||||
|     "awardPost": "Award Post", |     "awardPost": "Award Post", | ||||||
|  |     "awardPoints": "Awarded {} points", | ||||||
|     "awardMessage": "Message", |     "awardMessage": "Message", | ||||||
|     "awardMessageHint": "Enter your award message...", |     "awardMessageHint": "Enter your award message...", | ||||||
|     "awardAttitude": "Attitude", |     "awardAttitude": "Attitude", | ||||||
| @@ -1252,5 +1253,54 @@ | |||||||
|     "availableWithYourPlan": "Available with your plan", |     "availableWithYourPlan": "Available with your plan", | ||||||
|     "upgradeRequired": "Upgrade required", |     "upgradeRequired": "Upgrade required", | ||||||
|     "settingsDisableAnimation": "Disable Animation", |     "settingsDisableAnimation": "Disable Animation", | ||||||
|     "addTag": "Add Tag" |     "addTag": "Add Tag", | ||||||
|  |     "postFeaturedOn": "Post featured on {}", | ||||||
|  |     "messageSentAt": "Sent at {}", | ||||||
|  |     "myTickets": "My Tickets", | ||||||
|  |     "drawHistory": "Draw History", | ||||||
|  |     "lottery": "Lottery", | ||||||
|  |     "noLotteryTickets": "No lottery tickets yet", | ||||||
|  |     "buyYourFirstTicket": "Buy your first lottery ticket to get started!", | ||||||
|  |     "buyTicket": "Buy Ticket", | ||||||
|  |     "ticketNumbers": "Numbers: {}, Special: {}", | ||||||
|  |     "cost": "Cost", | ||||||
|  |     "multiplier": "Multiplier", | ||||||
|  |     "prizeWon": "Prize Won", | ||||||
|  |     "pending": "Pending", | ||||||
|  |     "drawn": "Drawn", | ||||||
|  |     "won": "Won", | ||||||
|  |     "lost": "Lost", | ||||||
|  |     "noDrawHistory": "No draw history yet", | ||||||
|  |     "buyLotteryTicket": "Buy Lottery Ticket", | ||||||
|  |     "selectNumbers": "Select Numbers", | ||||||
|  |     "select5UniqueNumbers": "Select 5 unique numbers", | ||||||
|  |     "selectSpecialNumber": "Select Special Number", | ||||||
|  |     "selectMultiplier": "Select Multiplier", | ||||||
|  |     "baseCost": "Base Cost", | ||||||
|  |     "totalCost": "Total Cost", | ||||||
|  |     "prizeStructure": "Prize Structure", | ||||||
|  |     "enterPinToConfirmPurchase": "Enter your PIN to confirm purchase", | ||||||
|  |     "ticketPurchasedSuccessfully": "Ticket purchased successfully!", | ||||||
|  |     "winningNumbers": "Winning Numbers", | ||||||
|  |     "specialNumber": "Special Number", | ||||||
|  |     "totalTickets": "Total Tickets", | ||||||
|  |     "totalWinners": "Total Winners", | ||||||
|  |     "prizePool": "Prize Pool", | ||||||
|  |     "enterPinToConfirmPayment": "Enter your PIN code to confirm payment", | ||||||
|  |     "purchase": "Purchase", | ||||||
|  |     "multiplierLabel": "Multiplier", | ||||||
|  |     "specialOnly": "Special Only", | ||||||
|  |     "matches": "Matches", | ||||||
|  |     "thoughtDefaultTopic": "Reflection", | ||||||
|  |     "thoughtAiName": "SN-chan", | ||||||
|  |     "thoughtUserName": "You", | ||||||
|  |     "thoughtStreamingHint": "Sn-chan is thinking...", | ||||||
|  |     "thoughtInputHint": "Ask sn-chan anything...", | ||||||
|  |     "thoughtNewConversation": "Start New Conversation", | ||||||
|  |     "thoughtParseError": "Failed to parse AI response", | ||||||
|  |     "thoughtFunctionCall": "Function Call", | ||||||
|  |     "aiThought": "AI Thought", | ||||||
|  |     "aiThoughtTitle": "Let sn-chan think", | ||||||
|  |     "postReferenceUnavailable": "Referenced post is unavailable", | ||||||
|  |     "fabLocation": "FAB Location" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1081,5 +1081,14 @@ | |||||||
|     "postPublish": "发布帖子", |     "postPublish": "发布帖子", | ||||||
|     "restoreDraftTitle": "恢复草稿", |     "restoreDraftTitle": "恢复草稿", | ||||||
|     "restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?", |     "restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?", | ||||||
|     "draft": "草稿" |     "draft": "草稿", | ||||||
|  |     "thoughtDefaultTopic": "寻思", | ||||||
|  |     "thoughtAiName": "SN 酱", | ||||||
|  |     "thoughtUserName": "您", | ||||||
|  |     "thoughtStreamingHint": "SN 酱正在思考...", | ||||||
|  |     "thoughtInputHint": "问 SN 酱任何问题...", | ||||||
|  |     "thoughtNewConversation": "开始新对话", | ||||||
|  |     "thoughtParseError": "解析 AI 响应失败", | ||||||
|  |     "aiThought": "寻思", | ||||||
|  |     "aiThoughtTitle": "让 SN 酱寻思寻思" | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								ios/Podfile
									
									
									
									
									
								
							
							
						
						| @@ -1,4 +1,3 @@ | |||||||
| # Uncomment this line to define a global platform for your project |  | ||||||
| platform :ios, '15.0' | platform :ios, '15.0' | ||||||
|  |  | ||||||
| # CocoaPods analytics sends network stats synchronously affecting flutter build latency. | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. | ||||||
| @@ -50,6 +49,16 @@ target 'Runner' do | |||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | target 'WatchRunner Watch App' do | ||||||
|  |   platform :watchos, '11.0' | ||||||
|  |  | ||||||
|  |   use_frameworks! | ||||||
|  |   use_modular_headers! | ||||||
|  |  | ||||||
|  |   pod 'Kingfisher', '~> 8.0' | ||||||
|  |   pod 'KingfisherWebP' | ||||||
|  | end | ||||||
|  |  | ||||||
| post_install do |installer| | post_install do |installer| | ||||||
|   installer.pods_project.targets.each do |target| |   installer.pods_project.targets.each do |target| | ||||||
|     flutter_additional_ios_build_settings(target) |     flutter_additional_ios_build_settings(target) | ||||||
|   | |||||||
| @@ -218,8 +218,23 @@ PODS: | |||||||
|     - Flutter |     - Flutter | ||||||
|   - irondash_engine_context (0.0.1): |   - irondash_engine_context (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - Kingfisher (8.6.0) |   - Kingfisher (8.6.1) | ||||||
|   - livekit_client (2.5.0): |   - KingfisherWebP (1.7.2): | ||||||
|  |     - Kingfisher (~> 8.0) | ||||||
|  |     - libwebp (>= 1.1.0) | ||||||
|  |   - libwebp (1.5.0): | ||||||
|  |     - libwebp/demux (= 1.5.0) | ||||||
|  |     - libwebp/mux (= 1.5.0) | ||||||
|  |     - libwebp/sharpyuv (= 1.5.0) | ||||||
|  |     - libwebp/webp (= 1.5.0) | ||||||
|  |   - libwebp/demux (1.5.0): | ||||||
|  |     - libwebp/webp | ||||||
|  |   - libwebp/mux (1.5.0): | ||||||
|  |     - libwebp/demux | ||||||
|  |   - libwebp/sharpyuv (1.5.0) | ||||||
|  |   - libwebp/webp (1.5.0): | ||||||
|  |     - libwebp/sharpyuv | ||||||
|  |   - livekit_client (2.5.3): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - flutter_webrtc |     - flutter_webrtc | ||||||
|     - WebRTC-SDK (= 137.7151.04) |     - WebRTC-SDK (= 137.7151.04) | ||||||
| @@ -333,6 +348,7 @@ DEPENDENCIES: | |||||||
|   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) |   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) | ||||||
|   - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) |   - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) | ||||||
|   - Kingfisher (~> 8.0) |   - Kingfisher (~> 8.0) | ||||||
|  |   - KingfisherWebP | ||||||
|   - livekit_client (from `.symlinks/plugins/livekit_client/ios`) |   - livekit_client (from `.symlinks/plugins/livekit_client/ios`) | ||||||
|   - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) |   - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) | ||||||
|   - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) |   - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) | ||||||
| @@ -375,6 +391,8 @@ SPEC REPOS: | |||||||
|     - GoogleDataTransport |     - GoogleDataTransport | ||||||
|     - GoogleUtilities |     - GoogleUtilities | ||||||
|     - Kingfisher |     - Kingfisher | ||||||
|  |     - KingfisherWebP | ||||||
|  |     - libwebp | ||||||
|     - nanopb |     - nanopb | ||||||
|     - OrderedSet |     - OrderedSet | ||||||
|     - PromisesObjC |     - PromisesObjC | ||||||
| @@ -517,10 +535,12 @@ SPEC CHECKSUMS: | |||||||
|   GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d |   GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d | ||||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 |   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 |   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||||
|   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a |   image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 | ||||||
|   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 |   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 | ||||||
|   Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0 |   Kingfisher: 7ac7a7288653787a54206b11a3c74f49ab650f1f | ||||||
|   livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4 |   KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402 | ||||||
|  |   libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 | ||||||
|  |   livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40 | ||||||
|   local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb |   local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb | ||||||
|   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 |   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 | ||||||
|   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 |   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 | ||||||
| @@ -529,8 +549,8 @@ SPEC CHECKSUMS: | |||||||
|   OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 |   OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 | ||||||
|   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 |   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 | ||||||
|   pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c |   pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c | ||||||
|   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 |   path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 | ||||||
|   pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed |   pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0 | ||||||
|   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 |   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 | ||||||
|   PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 |   PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 | ||||||
|   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 |   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 | ||||||
| @@ -538,7 +558,7 @@ SPEC CHECKSUMS: | |||||||
|   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c |   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c | ||||||
|   SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a |   SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a | ||||||
|   share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a |   share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a | ||||||
|   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 |   shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb | ||||||
|   sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 |   sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 | ||||||
|   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 |   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 | ||||||
|   sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b |   sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b | ||||||
| @@ -546,11 +566,11 @@ SPEC CHECKSUMS: | |||||||
|   super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 |   super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 | ||||||
|   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 |   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 | ||||||
|   syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14 |   syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14 | ||||||
|   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d |   url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b | ||||||
|   volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 |   volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 | ||||||
|   wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 |   wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 | ||||||
|   WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e |   WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e | ||||||
|  |  | ||||||
| PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f | PODFILE CHECKSUM: 9924dcd1590471adb798f3a0876bedd6a65ea145 | ||||||
|  |  | ||||||
| COCOAPODS: 1.16.2 | COCOAPODS: 1.16.2 | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ | |||||||
| 		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; | 		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; | ||||||
| 		331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; | 		331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; | ||||||
| 		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; | 		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; | ||||||
|  | 		7310A7DF2EB10963002C0FD3 /* WatchRunner Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||||||
| 		73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; }; | 		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, ); }; }; | 		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, ); }; }; | 		73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||||||
| @@ -20,6 +21,7 @@ | |||||||
| 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; | 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; | ||||||
| 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; | 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; | ||||||
| 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; | 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; | ||||||
|  | 		A1D34487886D362AC8B99B2E /* Pods_WatchRunner_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 802C1CFCA7F1E069AAEFB454 /* Pods_WatchRunner_Watch_App.framework */; }; | ||||||
| 		B87C0E607033790E71B54D73 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6D834CA86410B09796B312B /* Pods_Runner.framework */; }; | 		B87C0E607033790E71B54D73 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6D834CA86410B09796B312B /* Pods_Runner.framework */; }; | ||||||
| 		D174D53FF3E8EA943491A5CC /* Pods_SolianShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */; }; | 		D174D53FF3E8EA943491A5CC /* Pods_SolianShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */; }; | ||||||
| 		D1772CE196985AE8E8C9F2E5 /* Pods_SolianNotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */; }; | 		D1772CE196985AE8E8C9F2E5 /* Pods_SolianNotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */; }; | ||||||
| @@ -58,6 +60,17 @@ | |||||||
| /* End PBXContainerItemProxy section */ | /* End PBXContainerItemProxy section */ | ||||||
|  |  | ||||||
| /* Begin PBXCopyFilesBuildPhase section */ | /* Begin PBXCopyFilesBuildPhase section */ | ||||||
|  | 		7310A7DE2EB10963002C0FD3 /* Embed Watch Content */ = { | ||||||
|  | 			isa = PBXCopyFilesBuildPhase; | ||||||
|  | 			buildActionMask = 2147483647; | ||||||
|  | 			dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; | ||||||
|  | 			dstSubfolderSpec = 16; | ||||||
|  | 			files = ( | ||||||
|  | 				7310A7DF2EB10963002C0FD3 /* WatchRunner Watch App.app in Embed Watch Content */, | ||||||
|  | 			); | ||||||
|  | 			name = "Embed Watch Content"; | ||||||
|  | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
|  | 		}; | ||||||
| 		73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = { | 		73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = { | ||||||
| 			isa = PBXCopyFilesBuildPhase; | 			isa = PBXCopyFilesBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| @@ -84,6 +97,7 @@ | |||||||
| /* End PBXCopyFilesBuildPhase section */ | /* End PBXCopyFilesBuildPhase section */ | ||||||
|  |  | ||||||
| /* Begin PBXFileReference section */ | /* Begin PBXFileReference section */ | ||||||
|  | 		103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.profile.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.profile.xcconfig"; sourceTree = "<group>"; }; | ||||||
| 		14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; | 		14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; | ||||||
| 		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; | 		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; | ||||||
| 		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; | 		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; | ||||||
| @@ -100,6 +114,7 @@ | |||||||
| 		39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | 		39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||||
| 		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>"; }; | 		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>"; }; | 		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; | ||||||
|  | 		7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WatchRunner Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||||
| 		737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; 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; }; | 		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; }; | 		73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; }; | ||||||
| @@ -111,6 +126,8 @@ | |||||||
| 		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; | 		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; | ||||||
| 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; | 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; | ||||||
| 		7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | 		7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||||
|  | 		802C1CFCA7F1E069AAEFB454 /* Pods_WatchRunner_Watch_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WatchRunner_Watch_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||||
|  | 		86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.debug.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.debug.xcconfig"; sourceTree = "<group>"; }; | ||||||
| 		8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; }; | 		8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; }; | ||||||
| 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; | 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; | ||||||
| 		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; | 		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; | ||||||
| @@ -120,6 +137,7 @@ | |||||||
| 		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; | 		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; | ||||||
| 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; | 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; | ||||||
| 		9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; }; | 		9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; }; | ||||||
|  | 		A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.release.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.release.xcconfig"; sourceTree = "<group>"; }; | ||||||
| 		A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; | 		A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; | ||||||
| 		A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; }; | 		A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; }; | ||||||
| 		AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | 		AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||||
| @@ -162,6 +180,13 @@ | |||||||
| /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ | ||||||
|  |  | ||||||
| /* Begin PBXFileSystemSynchronizedRootGroup section */ | /* Begin PBXFileSystemSynchronizedRootGroup section */ | ||||||
|  | 		7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */ = { | ||||||
|  | 			isa = PBXFileSystemSynchronizedRootGroup; | ||||||
|  | 			exceptions = ( | ||||||
|  | 			); | ||||||
|  | 			path = "WatchRunner Watch App"; | ||||||
|  | 			sourceTree = "<group>"; | ||||||
|  | 		}; | ||||||
| 		73268D272DEB012A0076E970 /* Services */ = { | 		73268D272DEB012A0076E970 /* Services */ = { | ||||||
| 			isa = PBXFileSystemSynchronizedRootGroup; | 			isa = PBXFileSystemSynchronizedRootGroup; | ||||||
| 			exceptions = ( | 			exceptions = ( | ||||||
| @@ -205,6 +230,14 @@ | |||||||
| 			); | 			); | ||||||
| 			runOnlyForDeploymentPostprocessing = 0; | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
| 		}; | 		}; | ||||||
|  | 		7310A7D12EB10962002C0FD3 /* Frameworks */ = { | ||||||
|  | 			isa = PBXFrameworksBuildPhase; | ||||||
|  | 			buildActionMask = 2147483647; | ||||||
|  | 			files = ( | ||||||
|  | 				A1D34487886D362AC8B99B2E /* Pods_WatchRunner_Watch_App.framework in Frameworks */, | ||||||
|  | 			); | ||||||
|  | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
|  | 		}; | ||||||
| 		73ACDFA82E3D0E6100B63535 /* Frameworks */ = { | 		73ACDFA82E3D0E6100B63535 /* Frameworks */ = { | ||||||
| 			isa = PBXFrameworksBuildPhase; | 			isa = PBXFrameworksBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| @@ -258,6 +291,7 @@ | |||||||
| 				7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */, | 				7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */, | ||||||
| 				73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */, | 				73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */, | ||||||
| 				73ACDFB82E3D0E6100B63535 /* UIKit.framework */, | 				73ACDFB82E3D0E6100B63535 /* UIKit.framework */, | ||||||
|  | 				802C1CFCA7F1E069AAEFB454 /* Pods_WatchRunner_Watch_App.framework */, | ||||||
| 			); | 			); | ||||||
| 			name = Frameworks; | 			name = Frameworks; | ||||||
| 			sourceTree = "<group>"; | 			sourceTree = "<group>"; | ||||||
| @@ -280,6 +314,9 @@ | |||||||
| 				17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */, | 				17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */, | ||||||
| 				27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */, | 				27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */, | ||||||
| 				A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */, | 				A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */, | ||||||
|  | 				86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */, | ||||||
|  | 				A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */, | ||||||
|  | 				103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */, | ||||||
| 			); | 			); | ||||||
| 			path = Pods; | 			path = Pods; | ||||||
| 			sourceTree = "<group>"; | 			sourceTree = "<group>"; | ||||||
| @@ -303,6 +340,7 @@ | |||||||
| 				73CDD67B2DEC00480059D95D /* SolianNotificationService */, | 				73CDD67B2DEC00480059D95D /* SolianNotificationService */, | ||||||
| 				73C305CF2E0BE878009035B9 /* SolianShareExtension */, | 				73C305CF2E0BE878009035B9 /* SolianShareExtension */, | ||||||
| 				73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */, | 				73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */, | ||||||
|  | 				7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */, | ||||||
| 				97C146EF1CF9000F007C117D /* Products */, | 				97C146EF1CF9000F007C117D /* Products */, | ||||||
| 				331C8082294A63A400263BE5 /* RunnerTests */, | 				331C8082294A63A400263BE5 /* RunnerTests */, | ||||||
| 				91E124CE95BCB4DCD890160D /* Pods */, | 				91E124CE95BCB4DCD890160D /* Pods */, | ||||||
| @@ -319,6 +357,7 @@ | |||||||
| 				73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */, | 				73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */, | ||||||
| 				73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */, | 				73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */, | ||||||
| 				73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */, | 				73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */, | ||||||
|  | 				7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */, | ||||||
| 			); | 			); | ||||||
| 			name = Products; | 			name = Products; | ||||||
| 			sourceTree = "<group>"; | 			sourceTree = "<group>"; | ||||||
| @@ -363,6 +402,28 @@ | |||||||
| 			productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; | 			productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; | ||||||
| 			productType = "com.apple.product-type.bundle.unit-test"; | 			productType = "com.apple.product-type.bundle.unit-test"; | ||||||
| 		}; | 		}; | ||||||
|  | 		7310A7D32EB10962002C0FD3 /* WatchRunner Watch App */ = { | ||||||
|  | 			isa = PBXNativeTarget; | ||||||
|  | 			buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "WatchRunner Watch App" */; | ||||||
|  | 			buildPhases = ( | ||||||
|  | 				DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */, | ||||||
|  | 				7310A7D02EB10962002C0FD3 /* Sources */, | ||||||
|  | 				7310A7D12EB10962002C0FD3 /* Frameworks */, | ||||||
|  | 				7310A7D22EB10962002C0FD3 /* Resources */, | ||||||
|  | 				C74B07D6587D29C67A198025 /* [CP] Embed Pods Frameworks */, | ||||||
|  | 			); | ||||||
|  | 			buildRules = ( | ||||||
|  | 			); | ||||||
|  | 			dependencies = ( | ||||||
|  | 			); | ||||||
|  | 			fileSystemSynchronizedGroups = ( | ||||||
|  | 				7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */, | ||||||
|  | 			); | ||||||
|  | 			name = "WatchRunner Watch App"; | ||||||
|  | 			productName = "WatchRunner Watch App"; | ||||||
|  | 			productReference = 7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */; | ||||||
|  | 			productType = "com.apple.product-type.application"; | ||||||
|  | 		}; | ||||||
| 		73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = { | 		73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = { | ||||||
| 			isa = PBXNativeTarget; | 			isa = PBXNativeTarget; | ||||||
| 			buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */; | 			buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */; | ||||||
| @@ -434,6 +495,7 @@ | |||||||
| 				97C146EA1CF9000F007C117D /* Sources */, | 				97C146EA1CF9000F007C117D /* Sources */, | ||||||
| 				97C146EB1CF9000F007C117D /* Frameworks */, | 				97C146EB1CF9000F007C117D /* Frameworks */, | ||||||
| 				73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */, | 				73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */, | ||||||
|  | 				7310A7DE2EB10963002C0FD3 /* Embed Watch Content */, | ||||||
| 				97C146EC1CF9000F007C117D /* Resources */, | 				97C146EC1CF9000F007C117D /* Resources */, | ||||||
| 				9705A1C41CF9048500538489 /* Embed Frameworks */, | 				9705A1C41CF9048500538489 /* Embed Frameworks */, | ||||||
| 				3B06AD1E1E4923F5004D2608 /* Thin Binary */, | 				3B06AD1E1E4923F5004D2608 /* Thin Binary */, | ||||||
| @@ -463,7 +525,7 @@ | |||||||
| 			isa = PBXProject; | 			isa = PBXProject; | ||||||
| 			attributes = { | 			attributes = { | ||||||
| 				BuildIndependentTargetsInParallel = YES; | 				BuildIndependentTargetsInParallel = YES; | ||||||
| 				LastSwiftUpdateCheck = 1640; | 				LastSwiftUpdateCheck = 2600; | ||||||
| 				LastUpgradeCheck = 1510; | 				LastUpgradeCheck = 1510; | ||||||
| 				ORGANIZATIONNAME = ""; | 				ORGANIZATIONNAME = ""; | ||||||
| 				TargetAttributes = { | 				TargetAttributes = { | ||||||
| @@ -471,6 +533,9 @@ | |||||||
| 						CreatedOnToolsVersion = 14.0; | 						CreatedOnToolsVersion = 14.0; | ||||||
| 						TestTargetID = 97C146ED1CF9000F007C117D; | 						TestTargetID = 97C146ED1CF9000F007C117D; | ||||||
| 					}; | 					}; | ||||||
|  | 					7310A7D32EB10962002C0FD3 = { | ||||||
|  | 						CreatedOnToolsVersion = 26.0.1; | ||||||
|  | 					}; | ||||||
| 					73ACDFAA2E3D0E6100B63535 = { | 					73ACDFAA2E3D0E6100B63535 = { | ||||||
| 						CreatedOnToolsVersion = 16.4; | 						CreatedOnToolsVersion = 16.4; | ||||||
| 					}; | 					}; | ||||||
| @@ -504,6 +569,7 @@ | |||||||
| 				73CDD6792DEC00480059D95D /* SolianNotificationService */, | 				73CDD6792DEC00480059D95D /* SolianNotificationService */, | ||||||
| 				73C305CD2E0BE878009035B9 /* SolianShareExtension */, | 				73C305CD2E0BE878009035B9 /* SolianShareExtension */, | ||||||
| 				73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */, | 				73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */, | ||||||
|  | 				7310A7D32EB10962002C0FD3 /* WatchRunner Watch App */, | ||||||
| 			); | 			); | ||||||
| 		}; | 		}; | ||||||
| /* End PBXProject section */ | /* End PBXProject section */ | ||||||
| @@ -516,6 +582,13 @@ | |||||||
| 			); | 			); | ||||||
| 			runOnlyForDeploymentPostprocessing = 0; | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
| 		}; | 		}; | ||||||
|  | 		7310A7D22EB10962002C0FD3 /* Resources */ = { | ||||||
|  | 			isa = PBXResourcesBuildPhase; | ||||||
|  | 			buildActionMask = 2147483647; | ||||||
|  | 			files = ( | ||||||
|  | 			); | ||||||
|  | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
|  | 		}; | ||||||
| 		73ACDFA92E3D0E6100B63535 /* Resources */ = { | 		73ACDFA92E3D0E6100B63535 /* Resources */ = { | ||||||
| 			isa = PBXResourcesBuildPhase; | 			isa = PBXResourcesBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| @@ -683,6 +756,45 @@ | |||||||
| 			shellPath = /bin/sh; | 			shellPath = /bin/sh; | ||||||
| 			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; | 			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; | ||||||
| 		}; | 		}; | ||||||
|  | 		C74B07D6587D29C67A198025 /* [CP] Embed Pods Frameworks */ = { | ||||||
|  | 			isa = PBXShellScriptBuildPhase; | ||||||
|  | 			buildActionMask = 2147483647; | ||||||
|  | 			files = ( | ||||||
|  | 			); | ||||||
|  | 			inputFileListPaths = ( | ||||||
|  | 				"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist", | ||||||
|  | 			); | ||||||
|  | 			name = "[CP] Embed Pods Frameworks"; | ||||||
|  | 			outputFileListPaths = ( | ||||||
|  | 				"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist", | ||||||
|  | 			); | ||||||
|  | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
|  | 			shellPath = /bin/sh; | ||||||
|  | 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks.sh\"\n"; | ||||||
|  | 			showEnvVarsInLog = 0; | ||||||
|  | 		}; | ||||||
|  | 		DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */ = { | ||||||
|  | 			isa = PBXShellScriptBuildPhase; | ||||||
|  | 			buildActionMask = 2147483647; | ||||||
|  | 			files = ( | ||||||
|  | 			); | ||||||
|  | 			inputFileListPaths = ( | ||||||
|  | 			); | ||||||
|  | 			inputPaths = ( | ||||||
|  | 				"${PODS_PODFILE_DIR_PATH}/Podfile.lock", | ||||||
|  | 				"${PODS_ROOT}/Manifest.lock", | ||||||
|  | 			); | ||||||
|  | 			name = "[CP] Check Pods Manifest.lock"; | ||||||
|  | 			outputFileListPaths = ( | ||||||
|  | 			); | ||||||
|  | 			outputPaths = ( | ||||||
|  | 				"$(DERIVED_FILE_DIR)/Pods-WatchRunner Watch App-checkManifestLockResult.txt", | ||||||
|  | 			); | ||||||
|  | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
|  | 			shellPath = /bin/sh; | ||||||
|  | 			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; | ||||||
|  | 			showEnvVarsInLog = 0; | ||||||
|  | 		}; | ||||||
| 		E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { | 		E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { | ||||||
| 			isa = PBXShellScriptBuildPhase; | 			isa = PBXShellScriptBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| @@ -734,6 +846,13 @@ | |||||||
| 			); | 			); | ||||||
| 			runOnlyForDeploymentPostprocessing = 0; | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
| 		}; | 		}; | ||||||
|  | 		7310A7D02EB10962002C0FD3 /* Sources */ = { | ||||||
|  | 			isa = PBXSourcesBuildPhase; | ||||||
|  | 			buildActionMask = 2147483647; | ||||||
|  | 			files = ( | ||||||
|  | 			); | ||||||
|  | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
|  | 		}; | ||||||
| 		73ACDFA72E3D0E6100B63535 /* Sources */ = { | 		73ACDFA72E3D0E6100B63535 /* Sources */ = { | ||||||
| 			isa = PBXSourcesBuildPhase; | 			isa = PBXSourcesBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| @@ -943,6 +1062,147 @@ | |||||||
| 			}; | 			}; | ||||||
| 			name = Profile; | 			name = Profile; | ||||||
| 		}; | 		}; | ||||||
|  | 		7310A7E02EB10963002C0FD3 /* Debug */ = { | ||||||
|  | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = 86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */; | ||||||
|  | 			buildSettings = { | ||||||
|  | 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||||
|  | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
|  | 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||||||
|  | 				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_STYLE = Automatic; | ||||||
|  | 				CURRENT_PROJECT_VERSION = 1; | ||||||
|  | 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||||
|  | 				ENABLE_PREVIEWS = YES; | ||||||
|  | 				ENABLE_USER_SCRIPT_SANDBOXING = NO; | ||||||
|  | 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||||
|  | 				GENERATE_INFOPLIST_FILE = YES; | ||||||
|  | 				INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist"; | ||||||
|  | 				INFOPLIST_KEY_CFBundleDisplayName = WatchRunner; | ||||||
|  | 				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; | ||||||
|  | 				INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian; | ||||||
|  | 				LD_RUNPATH_SEARCH_PATHS = ( | ||||||
|  | 					"$(inherited)", | ||||||
|  | 					"@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.watchkitapp; | ||||||
|  | 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||||
|  | 				SDKROOT = watchos; | ||||||
|  | 				SKIP_INSTALL = YES; | ||||||
|  | 				STRING_CATALOG_GENERATE_SYMBOLS = YES; | ||||||
|  | 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; | ||||||
|  | 				SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||||||
|  | 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | ||||||
|  | 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||||
|  | 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||||
|  | 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||||||
|  | 				SWIFT_VERSION = 5.0; | ||||||
|  | 				TARGETED_DEVICE_FAMILY = 4; | ||||||
|  | 				WATCHOS_DEPLOYMENT_TARGET = 11.6; | ||||||
|  | 			}; | ||||||
|  | 			name = Debug; | ||||||
|  | 		}; | ||||||
|  | 		7310A7E12EB10963002C0FD3 /* Release */ = { | ||||||
|  | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */; | ||||||
|  | 			buildSettings = { | ||||||
|  | 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||||
|  | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
|  | 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||||||
|  | 				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_STYLE = Automatic; | ||||||
|  | 				CURRENT_PROJECT_VERSION = 1; | ||||||
|  | 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||||
|  | 				ENABLE_PREVIEWS = YES; | ||||||
|  | 				ENABLE_USER_SCRIPT_SANDBOXING = NO; | ||||||
|  | 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||||
|  | 				GENERATE_INFOPLIST_FILE = YES; | ||||||
|  | 				INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist"; | ||||||
|  | 				INFOPLIST_KEY_CFBundleDisplayName = WatchRunner; | ||||||
|  | 				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; | ||||||
|  | 				INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian; | ||||||
|  | 				LD_RUNPATH_SEARCH_PATHS = ( | ||||||
|  | 					"$(inherited)", | ||||||
|  | 					"@executable_path/Frameworks", | ||||||
|  | 				); | ||||||
|  | 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES; | ||||||
|  | 				MARKETING_VERSION = 1.0; | ||||||
|  | 				MTL_FAST_MATH = YES; | ||||||
|  | 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp; | ||||||
|  | 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||||
|  | 				SDKROOT = watchos; | ||||||
|  | 				SKIP_INSTALL = YES; | ||||||
|  | 				STRING_CATALOG_GENERATE_SYMBOLS = YES; | ||||||
|  | 				SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||||||
|  | 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | ||||||
|  | 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||||
|  | 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||||||
|  | 				SWIFT_VERSION = 5.0; | ||||||
|  | 				TARGETED_DEVICE_FAMILY = 4; | ||||||
|  | 				WATCHOS_DEPLOYMENT_TARGET = 11.6; | ||||||
|  | 			}; | ||||||
|  | 			name = Release; | ||||||
|  | 		}; | ||||||
|  | 		7310A7E22EB10963002C0FD3 /* Profile */ = { | ||||||
|  | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = 103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */; | ||||||
|  | 			buildSettings = { | ||||||
|  | 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||||
|  | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
|  | 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||||||
|  | 				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_STYLE = Automatic; | ||||||
|  | 				CURRENT_PROJECT_VERSION = 1; | ||||||
|  | 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||||
|  | 				ENABLE_PREVIEWS = YES; | ||||||
|  | 				ENABLE_USER_SCRIPT_SANDBOXING = NO; | ||||||
|  | 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||||
|  | 				GENERATE_INFOPLIST_FILE = YES; | ||||||
|  | 				INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist"; | ||||||
|  | 				INFOPLIST_KEY_CFBundleDisplayName = WatchRunner; | ||||||
|  | 				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; | ||||||
|  | 				INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian; | ||||||
|  | 				LD_RUNPATH_SEARCH_PATHS = ( | ||||||
|  | 					"$(inherited)", | ||||||
|  | 					"@executable_path/Frameworks", | ||||||
|  | 				); | ||||||
|  | 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES; | ||||||
|  | 				MARKETING_VERSION = 1.0; | ||||||
|  | 				MTL_FAST_MATH = YES; | ||||||
|  | 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp; | ||||||
|  | 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||||
|  | 				SDKROOT = watchos; | ||||||
|  | 				SKIP_INSTALL = YES; | ||||||
|  | 				STRING_CATALOG_GENERATE_SYMBOLS = YES; | ||||||
|  | 				SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||||||
|  | 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | ||||||
|  | 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||||
|  | 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||||||
|  | 				SWIFT_VERSION = 5.0; | ||||||
|  | 				TARGETED_DEVICE_FAMILY = 4; | ||||||
|  | 				WATCHOS_DEPLOYMENT_TARGET = 11.6; | ||||||
|  | 			}; | ||||||
|  | 			name = Profile; | ||||||
|  | 		}; | ||||||
| 		73ACDFC42E3D0E6100B63535 /* Debug */ = { | 		73ACDFC42E3D0E6100B63535 /* Debug */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| @@ -1487,6 +1747,16 @@ | |||||||
| 			defaultConfigurationIsVisible = 0; | 			defaultConfigurationIsVisible = 0; | ||||||
| 			defaultConfigurationName = Release; | 			defaultConfigurationName = Release; | ||||||
| 		}; | 		}; | ||||||
|  | 		7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "WatchRunner Watch App" */ = { | ||||||
|  | 			isa = XCConfigurationList; | ||||||
|  | 			buildConfigurations = ( | ||||||
|  | 				7310A7E02EB10963002C0FD3 /* Debug */, | ||||||
|  | 				7310A7E12EB10963002C0FD3 /* Release */, | ||||||
|  | 				7310A7E22EB10963002C0FD3 /* Profile */, | ||||||
|  | 			); | ||||||
|  | 			defaultConfigurationIsVisible = 0; | ||||||
|  | 			defaultConfigurationName = Release; | ||||||
|  | 		}; | ||||||
| 		73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = { | 		73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = { | ||||||
| 			isa = XCConfigurationList; | 			isa = XCConfigurationList; | ||||||
| 			buildConfigurations = ( | 			buildConfigurations = ( | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
| import Flutter | import Flutter | ||||||
| import UIKit | import UIKit | ||||||
|  | import WatchConnectivity | ||||||
|  |  | ||||||
| @main | @main | ||||||
| @objc class AppDelegate: FlutterAppDelegate { | @objc class AppDelegate: FlutterAppDelegate { | ||||||
|     let notifyDelegate = NotifyDelegate() |     let notifyDelegate = NotifyDelegate() | ||||||
|  |     private var watchConnectivityService: WatchConnectivityService? | ||||||
|      |      | ||||||
|     override func application( |     override func application( | ||||||
|         _ application: UIApplication, |         _ application: UIApplication, | ||||||
| @@ -28,6 +30,55 @@ import UIKit | |||||||
|          |          | ||||||
|         GeneratedPluginRegistrant.register(with: self) |         GeneratedPluginRegistrant.register(with: self) | ||||||
|          |          | ||||||
|  |         if WCSession.isSupported() { | ||||||
|  |             watchConnectivityService = WatchConnectivityService() | ||||||
|  |         } | ||||||
|  |          | ||||||
|         return super.application(application, didFinishLaunchingWithOptions: launchOptions) |         return super.application(application, didFinishLaunchingWithOptions: launchOptions) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class WatchConnectivityService: NSObject, WCSessionDelegate { | ||||||
|  |     private let session: WCSession | ||||||
|  |  | ||||||
|  |     override init() { | ||||||
|  |         self.session = .default | ||||||
|  |         super.init() | ||||||
|  |         print("[iOS] Activating WCSession") | ||||||
|  |         self.session.delegate = self | ||||||
|  |         self.session.activate() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { | ||||||
|  |         if let error = error { | ||||||
|  |             print("[iOS] WCSession activation failed with error: \(error.localizedDescription)") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         print("[iOS] WCSession activated with state: \(activationState.rawValue)") | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func sessionDidBecomeInactive(_ session: WCSession) {} | ||||||
|  |      | ||||||
|  |     func sessionDidDeactivate(_ session: WCSession) { | ||||||
|  |         session.activate() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { | ||||||
|  |         print("[iOS] Received message: \(message)") | ||||||
|  |         if let request = message["request"] as? String, request == "data" { | ||||||
|  |             let token = UserDefaults.standard.getFlutterToken() | ||||||
|  |             let serverUrl = UserDefaults.standard.getServerUrl() | ||||||
|  |              | ||||||
|  |             print("[iOS] Retrieved token: \(token ?? "nil")") | ||||||
|  |             print("[iOS] Retrieved serverUrl: \(serverUrl)") | ||||||
|  |              | ||||||
|  |             var data: [String: Any] = ["serverUrl": serverUrl] | ||||||
|  |             if let token = token { | ||||||
|  |                 data["token"] = token | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             print("[iOS] Replying with data: \(data)") | ||||||
|  |             replyHandler(data) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1 +1,334 @@ | |||||||
| {"images":[{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@2x.png","scale":"2x","platform":"ios"},{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@3x.png","scale":"3x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@2x.png","scale":"2x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@3x.png","scale":"3x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@2x.png","scale":"2x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@3x.png","scale":"3x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@2x.png","scale":"2x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@3x.png","scale":"3x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@2x.png","scale":"2x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@3x.png","scale":"3x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@2x.png","scale":"2x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@3x.png","scale":"3x","platform":"ios"},{"size":"68x68","idiom":"universal","filename":"Icon-App-68x68@2x.png","scale":"2x","platform":"ios"},{"size":"76x76","idiom":"universal","filename":"Icon-App-76x76@2x.png","scale":"2x","platform":"ios"},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x","platform":"ios"},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-1024x1024@1x.png","scale":"1x","platform":"ios"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"68x68","idiom":"universal","filename":"Icon-App-Dark-68x68@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"76x76","idiom":"universal","filename":"Icon-App-Dark-76x76@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-Dark-83.5x83.5@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-Dark-1024x1024@1x.png","scale":"1x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]}],"info":{"version":1,"author":"xcode"}} | { | ||||||
|  |   "images" : [ | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-20x20@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "20x20" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-20x20@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "20x20" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-29x29@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "29x29" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-29x29@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "29x29" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-38x38@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "38x38" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-38x38@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "38x38" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-40x40@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "40x40" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-40x40@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "40x40" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-60x60@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "60x60" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-60x60@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "60x60" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-64x64@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "64x64" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-64x64@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "64x64" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-68x68@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "68x68" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-76x76@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "76x76" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-83.5x83.5@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "83.5x83.5" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-1024x1024@1x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "1x", | ||||||
|  |       "size" : "1024x1024" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-20x20@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "20x20" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-20x20@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "20x20" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-29x29@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "29x29" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-29x29@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "29x29" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-38x38@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "38x38" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-38x38@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "38x38" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-40x40@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "40x40" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-40x40@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "40x40" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-60x60@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "60x60" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-60x60@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "60x60" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-64x64@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "64x64" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-64x64@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "64x64" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-68x68@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "68x68" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-76x76@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "76x76" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-83.5x83.5@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "83.5x83.5" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "appearances" : [ | ||||||
|  |         { | ||||||
|  |           "appearance" : "luminosity", | ||||||
|  |           "value" : "dark" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "filename" : "Icon-App-Dark-1024x1024@1x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "1x", | ||||||
|  |       "size" : "1024x1024" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "Icon-App-1024x1024@1x.png", | ||||||
|  |       "idiom" : "ios-marketing", | ||||||
|  |       "scale" : "1x", | ||||||
|  |       "size" : "1024x1024" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "info" : { | ||||||
|  |     "author" : "xcode", | ||||||
|  |     "version" : 1 | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| Before Width: | Height: | Size: 295 B | 
| Before Width: | Height: | Size: 282 B | 
| Before Width: | Height: | Size: 406 B | 
| Before Width: | Height: | Size: 762 B | 
| @@ -8,7 +8,7 @@ | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| func getAttachmentUrl(for identifier: String) -> String { | func getAttachmentUrl(for identifier: String) -> String { | ||||||
|     let serverBaseUrl = "https://api.solian.app" |     let serverBaseUrl = UserDefaults.standard.getServerUrl() | ||||||
|      |      | ||||||
|     return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)" |     return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -26,6 +26,6 @@ extension UserDefaults { | |||||||
|     } |     } | ||||||
|      |      | ||||||
|     func getServerUrl(forKey key: String = "app_server_url") -> String { |     func getServerUrl(forKey key: String = "app_server_url") -> String { | ||||||
|         return self.getFlutterValue(forKey: key) ?? "https://nt.solian.app" |         return self.getFlutterValue(forKey: key) ?? "https://api.solian.app" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -85,15 +85,8 @@ class NotificationService: UNNotificationServiceExtension { | |||||||
|                 customIdentifier: nil |                 customIdentifier: nil | ||||||
|             ) |             ) | ||||||
|              |              | ||||||
|             let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body) |  | ||||||
|             self.donateInteraction(for: intent) |  | ||||||
|             let updatedContent = try? request.content.updating(from: intent) |  | ||||||
|             content.categoryIdentifier = "CHAT_MESSAGE" |             content.categoryIdentifier = "CHAT_MESSAGE" | ||||||
|             if let updatedContent = updatedContent { |  | ||||||
|                 self.contentHandler?(updatedContent) |  | ||||||
|             } else { |  | ||||||
|             self.contentHandler?(content) |             self.contentHandler?(content) | ||||||
|             } |  | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|      |      | ||||||
|   | |||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | { | ||||||
|  |   "colors" : [ | ||||||
|  |     { | ||||||
|  |       "color" : { | ||||||
|  |         "platform" : "universal", | ||||||
|  |         "reference" : "systemIndigoColor" | ||||||
|  |       }, | ||||||
|  |       "idiom" : "universal" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "info" : { | ||||||
|  |     "author" : "xcode", | ||||||
|  |     "version" : 1 | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,318 @@ | |||||||
|  | { | ||||||
|  |   "images" : [ | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-20x20@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "20x20" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-20x20@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "20x20" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-29x29@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "29x29" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-29x29@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "29x29" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-38x38@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "38x38" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-38x38@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "38x38" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-40x40@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "40x40" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-40x40@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "40x40" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-60x60@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "60x60" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-60x60@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "60x60" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-64x64@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "64x64" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-64x64@3x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "3x", | ||||||
|  |       "size" : "64x64" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-68x68@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "68x68" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-76x76@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "76x76" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-83.5x83.5@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "83.5x83.5" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-ios-1024x1024.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "ios", | ||||||
|  |       "size" : "1024x1024" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-16x16.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "1x", | ||||||
|  |       "size" : "16x16" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-16x16@2x.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "16x16" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-32x32.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "1x", | ||||||
|  |       "size" : "32x32" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-32x32@2x.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "32x32" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-128x128.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "1x", | ||||||
|  |       "size" : "128x128" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-128x128@2x.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "128x128" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-256x256.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "1x", | ||||||
|  |       "size" : "256x256" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-256x256@2x.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "256x256" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-512x512.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "1x", | ||||||
|  |       "size" : "512x512" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-mac-512x512@2x.png", | ||||||
|  |       "idiom" : "mac", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "512x512" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-22x22@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "22x22" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-24x24@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "24x24" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-27.5x27.5@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "27.5x27.5" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-29x29@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "29x29" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-30x30@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "30x30" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-32x32@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "32x32" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-33x33@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "33x33" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-40x40@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "40x40" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-43.5x43.5@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "43.5x43.5" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-44x44@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "44x44" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-46x46@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "46x46" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-50x50@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "50x50" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-51x51@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "51x51" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-54x54@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "54x54" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-86x86@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "86x86" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-98x98@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "98x98" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-108x108@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "108x108" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-117x117@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "117x117" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-129x129@2x.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "scale" : "2x", | ||||||
|  |       "size" : "129x129" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "filename" : "icon-watchos-1024x1024.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "platform" : "watchos", | ||||||
|  |       "size" : "1024x1024" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "info" : { | ||||||
|  |     "author" : "xcode", | ||||||
|  |     "version" : 1 | ||||||
|  |   } | ||||||
|  | } | ||||||
| After Width: | Height: | Size: 45 KiB | 
| After Width: | Height: | Size: 1.5 KiB | 
| After Width: | Height: | Size: 2.5 KiB | 
| After Width: | Height: | Size: 2.4 KiB | 
| After Width: | Height: | Size: 4.1 KiB | 
| After Width: | Height: | Size: 3.4 KiB | 
| After Width: | Height: | Size: 5.7 KiB | 
| After Width: | Height: | Size: 3.6 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 9.6 KiB | 
| After Width: | Height: | Size: 6.6 KiB | 
| After Width: | Height: | Size: 10 KiB | 
| After Width: | Height: | Size: 7.0 KiB | 
| After Width: | Height: | Size: 7.8 KiB | 
| After Width: | Height: | Size: 8.8 KiB | 
| After Width: | Height: | Size: 6.6 KiB | 
| After Width: | Height: | Size: 14 KiB | 
| After Width: | Height: | Size: 473 B | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 14 KiB | 
| After Width: | Height: | Size: 30 KiB | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 2.7 KiB | 
| After Width: | Height: | Size: 30 KiB | 
| After Width: | Height: | Size: 45 KiB | 
| After Width: | Height: | Size: 45 KiB | 
| After Width: | Height: | Size: 12 KiB | 
| After Width: | Height: | Size: 13 KiB | 
| After Width: | Height: | Size: 14 KiB | 
| After Width: | Height: | Size: 1.7 KiB | 
| After Width: | Height: | Size: 1.9 KiB | 
| After Width: | Height: | Size: 2.3 KiB | 
| After Width: | Height: | Size: 2.4 KiB | 
| After Width: | Height: | Size: 2.5 KiB | 
| After Width: | Height: | Size: 2.7 KiB | 
| After Width: | Height: | Size: 2.9 KiB | 
| After Width: | Height: | Size: 3.6 KiB | 
| After Width: | Height: | Size: 4.1 KiB | 
| After Width: | Height: | Size: 4.2 KiB | 
| After Width: | Height: | Size: 4.4 KiB | 
| After Width: | Height: | Size: 4.9 KiB | 
| After Width: | Height: | Size: 5.0 KiB | 
| After Width: | Height: | Size: 5.3 KiB | 
| After Width: | Height: | Size: 9.1 KiB | 
| After Width: | Height: | Size: 10 KiB | 
							
								
								
									
										6
									
								
								ios/WatchRunner Watch App/Assets.xcassets/Contents.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | { | ||||||
|  |   "info" : { | ||||||
|  |     "author" : "xcode", | ||||||
|  |     "version" : 1 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								ios/WatchRunner Watch App/Assets.xcassets/Logo.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | { | ||||||
|  |   "images" : [ | ||||||
|  |     { | ||||||
|  |       "filename" : "icon.png", | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "scale" : "1x" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "scale" : "2x" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "idiom" : "universal", | ||||||
|  |       "scale" : "3x" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "info" : { | ||||||
|  |     "author" : "xcode", | ||||||
|  |     "version" : 1 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								ios/WatchRunner Watch App/Assets.xcassets/Logo.imageset/icon.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 70 KiB | 
							
								
								
									
										50
									
								
								ios/WatchRunner Watch App/ContentView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | |||||||
|  | // | ||||||
|  | //  ContentView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/28. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | // The root view of the app. | ||||||
|  | struct ContentView: View { | ||||||
|  |     @StateObject private var appState = AppState() | ||||||
|  |     @State private var selection: Panel? = .explore | ||||||
|  |      | ||||||
|  |     enum Panel: Hashable { | ||||||
|  |         case explore | ||||||
|  |         case chat | ||||||
|  |         case notifications | ||||||
|  |         case account | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         NavigationSplitView { | ||||||
|  |             List(selection: $selection) { | ||||||
|  |                 AppInfoHeaderView() | ||||||
|  |                     .listRowBackground(Color.clear) | ||||||
|  |                     .environmentObject(appState) | ||||||
|  |                  | ||||||
|  |                 Label("Explore", systemImage: "globe.fill").tag(Panel.explore) | ||||||
|  |                 Label("Chat", systemImage: "message.fill").tag(Panel.chat) | ||||||
|  |                 Label("Notifications", systemImage: "bell.fill").tag(Panel.notifications) | ||||||
|  |                 Label("Account", systemImage: "person.circle.fill").tag(Panel.account) | ||||||
|  |             } | ||||||
|  |             .listStyle(.automatic) | ||||||
|  |         } detail: { | ||||||
|  |             switch selection { | ||||||
|  |             case .explore: | ||||||
|  |                 ExploreView().environmentObject(appState) | ||||||
|  |             case .chat: | ||||||
|  |                 ChatView().environmentObject(appState) | ||||||
|  |             case .notifications: | ||||||
|  |                 NotificationView().environmentObject(appState) | ||||||
|  |             case .account: | ||||||
|  |                 AccountView().environmentObject(appState) | ||||||
|  |             case .none: | ||||||
|  |                 Text("Select a panel") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										88
									
								
								ios/WatchRunner Watch App/Layouts/FlowLayout.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,88 @@ | |||||||
|  | // | ||||||
|  | //  FlowLayout.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | // MARK: - Custom Layouts | ||||||
|  |  | ||||||
|  | struct FlowLayout: Layout { | ||||||
|  |     var alignment: HorizontalAlignment = .leading | ||||||
|  |     var spacing: CGFloat = 10 | ||||||
|  |  | ||||||
|  |     func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { | ||||||
|  |         let containerWidth = proposal.width ?? 0 | ||||||
|  |         let sizes = subviews.map { $0.sizeThatFits(.unspecified) } | ||||||
|  |  | ||||||
|  |         var currentX: CGFloat = 0 | ||||||
|  |         var currentY: CGFloat = 0 | ||||||
|  |         var lineHeight: CGFloat = 0 | ||||||
|  |         var totalHeight: CGFloat = 0 | ||||||
|  |  | ||||||
|  |         for size in sizes { | ||||||
|  |             if currentX + size.width > containerWidth { | ||||||
|  |                 // New line | ||||||
|  |                 currentX = 0 | ||||||
|  |                 currentY += lineHeight + spacing | ||||||
|  |                 totalHeight = currentY + size.height | ||||||
|  |                 lineHeight = 0 | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             currentX += size.width + spacing | ||||||
|  |             lineHeight = max(lineHeight, size.height) | ||||||
|  |         } | ||||||
|  |         totalHeight = currentY + lineHeight | ||||||
|  |  | ||||||
|  |         return CGSize(width: containerWidth, height: totalHeight) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { | ||||||
|  |         let containerWidth = bounds.width | ||||||
|  |         let sizes = subviews.map { $0.sizeThatFits(.unspecified) } | ||||||
|  |  | ||||||
|  |         var currentX: CGFloat = 0 | ||||||
|  |         var currentY: CGFloat = 0 | ||||||
|  |         var lineHeight: CGFloat = 0 | ||||||
|  |         var lineElements: [(offset: Int, size: CGSize)] = [] | ||||||
|  |  | ||||||
|  |         func placeLine() { | ||||||
|  |             let lineWidth = lineElements.map { $0.size.width }.reduce(0, +) + CGFloat(lineElements.count - 1) * spacing | ||||||
|  |             var startX: CGFloat = 0 | ||||||
|  |             switch alignment { | ||||||
|  |             case .leading: | ||||||
|  |                 startX = bounds.minX | ||||||
|  |             case .center: | ||||||
|  |                 startX = bounds.minX + (containerWidth - lineWidth) / 2 | ||||||
|  |             case .trailing: | ||||||
|  |                 startX = bounds.maxX - lineWidth | ||||||
|  |             default: | ||||||
|  |                 startX = bounds.minX | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var xOffset = startX | ||||||
|  |             for (offset, size) in lineElements { | ||||||
|  |                 subviews[offset].place(at: CGPoint(x: xOffset, y: bounds.minY + currentY), proposal: ProposedViewSize(size)) // Use bounds.minY + currentY | ||||||
|  |                 xOffset += size.width + spacing | ||||||
|  |             } | ||||||
|  |             lineElements.removeAll() // Clear elements for the next line | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (offset, size) in sizes.enumerated() { | ||||||
|  |             if currentX + size.width > containerWidth && !lineElements.isEmpty { | ||||||
|  |                 // New line | ||||||
|  |                 placeLine() | ||||||
|  |                 currentX = 0 | ||||||
|  |                 currentY += lineHeight + spacing | ||||||
|  |                 lineHeight = 0 | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             lineElements.append((offset, size)) | ||||||
|  |             currentX += size.width + spacing | ||||||
|  |             lineHeight = max(lineHeight, size.height) | ||||||
|  |         } | ||||||
|  |         placeLine() // Place the last line | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										365
									
								
								ios/WatchRunner Watch App/Models/Models.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,365 @@ | |||||||
|  | //  Models.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // MARK: - Models | ||||||
|  |  | ||||||
|  | struct AppToken: Codable { | ||||||
|  |     let token: String | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnActivity: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let type: String | ||||||
|  |     let data: ActivityData? | ||||||
|  |     let createdAt: Date | ||||||
|  | } | ||||||
|  |  | ||||||
|  | enum ActivityData: Codable { | ||||||
|  |     case post(SnPost) | ||||||
|  |     case discovery(DiscoveryData) | ||||||
|  |     case unknown | ||||||
|  |  | ||||||
|  |     init(from decoder: Decoder) throws { | ||||||
|  |         let container = try decoder.singleValueContainer() | ||||||
|  |         if let post = try? container.decode(SnPost.self) { | ||||||
|  |             self = .post(post) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         if let discoveryData = try? container.decode(DiscoveryData.self) { | ||||||
|  |             self = .discovery(discoveryData) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         self = .unknown | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func encode(to encoder: Encoder) throws { | ||||||
|  |         // Not needed for decoding | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnPost: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let title: String? | ||||||
|  |     let content: String? | ||||||
|  |     let publisher: SnPublisher | ||||||
|  |     let attachments: [SnCloudFile] | ||||||
|  |     let tags: [SnPostTag] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct DiscoveryData: Codable { | ||||||
|  |     let items: [DiscoveryItem] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct DiscoveryItem: Codable, Identifiable { | ||||||
|  |     var id = UUID() | ||||||
|  |     let type: String | ||||||
|  |     let data: DiscoveryItemData | ||||||
|  |  | ||||||
|  |     enum CodingKeys: String, CodingKey { | ||||||
|  |         case type, data | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | enum DiscoveryItemData: Codable { | ||||||
|  |     case realm(SnRealm) | ||||||
|  |     case publisher(SnPublisher) | ||||||
|  |     case article(SnWebArticle) | ||||||
|  |     case unknown | ||||||
|  |  | ||||||
|  |     init(from decoder: Decoder) throws { | ||||||
|  |         let container = try decoder.singleValueContainer() | ||||||
|  |         if let realm = try? container.decode(SnRealm.self) { | ||||||
|  |             self = .realm(realm) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         if let publisher = try? container.decode(SnPublisher.self) { | ||||||
|  |             self = .publisher(publisher) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         if let article = try? container.decode(SnWebArticle.self) { | ||||||
|  |             self = .article(article) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         self = .unknown | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func encode(to encoder: Encoder) throws { | ||||||
|  |         // Not needed for decoding | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnRealm: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let name: String | ||||||
|  |     let description: String? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnPublisher: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let name: String | ||||||
|  |     let nick: String? | ||||||
|  |     let description: String? | ||||||
|  |     let picture: SnCloudFile? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnCloudFile: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let mimeType: String? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnPostTag: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let slug: String | ||||||
|  |     let name: String? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnWebArticle: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let title: String | ||||||
|  |     let url: String | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnNotification: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let topic: String | ||||||
|  |     let title: String | ||||||
|  |     let subtitle: String | ||||||
|  |     let content: String | ||||||
|  |     let meta: [String: AnyCodable]? | ||||||
|  |     let priority: Int | ||||||
|  |     let viewedAt: Date? | ||||||
|  |     let accountId: String | ||||||
|  |     let createdAt: Date | ||||||
|  |     let updatedAt: Date | ||||||
|  |     let deletedAt: Date? | ||||||
|  |  | ||||||
|  |     enum CodingKeys: String, CodingKey { | ||||||
|  |         case id | ||||||
|  |         case topic | ||||||
|  |         case title | ||||||
|  |         case subtitle | ||||||
|  |         case content | ||||||
|  |         case meta | ||||||
|  |         case priority | ||||||
|  |         case viewedAt = "viewedAt" | ||||||
|  |         case accountId = "accountId" | ||||||
|  |         case createdAt = "createdAt" | ||||||
|  |         case updatedAt = "updatedAt" | ||||||
|  |         case deletedAt = "deletedAt" | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct AnyCodable: Codable { | ||||||
|  |     let value: Any | ||||||
|  |  | ||||||
|  |     init(_ value: Any) { | ||||||
|  |         self.value = value | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     init(from decoder: Decoder) throws { | ||||||
|  |         let container = try decoder.singleValueContainer() | ||||||
|  |         if let intValue = try? container.decode(Int.self) { | ||||||
|  |             value = intValue | ||||||
|  |         } else if let doubleValue = try? container.decode(Double.self) { | ||||||
|  |             value = doubleValue | ||||||
|  |         } else if let boolValue = try? container.decode(Bool.self) { | ||||||
|  |             value = boolValue | ||||||
|  |         } else if let stringValue = try? container.decode(String.self) { | ||||||
|  |             value = stringValue | ||||||
|  |         } else if let arrayValue = try? container.decode([AnyCodable].self) { | ||||||
|  |             value = arrayValue | ||||||
|  |         } else if let dictValue = try? container.decode([String: AnyCodable].self) { | ||||||
|  |             value = dictValue | ||||||
|  |         } else { | ||||||
|  |             value = NSNull() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func encode(to encoder: Encoder) throws { | ||||||
|  |         var container = encoder.singleValueContainer() | ||||||
|  |         switch value { | ||||||
|  |         case let intValue as Int: | ||||||
|  |             try container.encode(intValue) | ||||||
|  |         case let doubleValue as Double: | ||||||
|  |             try container.encode(doubleValue) | ||||||
|  |         case let boolValue as Bool: | ||||||
|  |             try container.encode(boolValue) | ||||||
|  |         case let stringValue as String: | ||||||
|  |             try container.encode(stringValue) | ||||||
|  |         case let arrayValue as [AnyCodable]: | ||||||
|  |             try container.encode(arrayValue) | ||||||
|  |         case let dictValue as [String: AnyCodable]: | ||||||
|  |             try container.encode(dictValue) | ||||||
|  |         default: | ||||||
|  |             try container.encodeNil() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct NotificationResponse { | ||||||
|  |     let notifications: [SnNotification] | ||||||
|  |     let total: Int | ||||||
|  |     let hasMore: Bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ActivityResponse { | ||||||
|  |     let activities: [SnActivity] | ||||||
|  |     let hasMore: Bool | ||||||
|  |     let nextCursor: String? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnAccount: Codable { | ||||||
|  |     let id: String | ||||||
|  |     let name: String | ||||||
|  |     let nick: String | ||||||
|  |     let profile: SnUserProfile | ||||||
|  |     let createdAt: Date | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnUserProfile: Codable { | ||||||
|  |     let bio: String? | ||||||
|  |     let picture: SnCloudFile? | ||||||
|  |     let background: SnCloudFile? | ||||||
|  |     let level: Int | ||||||
|  |     let experience: Int | ||||||
|  |     let levelingProgress: Double | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnAccountStatus: Codable { | ||||||
|  |     let id: String | ||||||
|  |     let attitude: Int | ||||||
|  |     let isOnline: Bool | ||||||
|  |     let isInvisible: Bool | ||||||
|  |     let isNotDisturb: Bool | ||||||
|  |     let isCustomized: Bool | ||||||
|  |     let label: String | ||||||
|  |     let meta: [String: AnyCodable]? | ||||||
|  |     let clearedAt: Date? | ||||||
|  |     let accountId: String | ||||||
|  |     let createdAt: Date | ||||||
|  |     let updatedAt: Date | ||||||
|  |     let deletedAt: Date? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: - Chat Models | ||||||
|  |  | ||||||
|  | struct SnChatRoom: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let name: String? | ||||||
|  |     let description: String? | ||||||
|  |     let type: Int | ||||||
|  |     let isPublic: Bool | ||||||
|  |     let isCommunity: Bool | ||||||
|  |     let picture: SnCloudFile? | ||||||
|  |     let background: SnCloudFile? | ||||||
|  |     let realmId: String? | ||||||
|  |     let realm: SnRealm? | ||||||
|  |     let createdAt: Date | ||||||
|  |     let updatedAt: Date | ||||||
|  |     let deletedAt: Date? | ||||||
|  |     let members: [SnChatMember]? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnChatMessage: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let type: String | ||||||
|  |     let content: String? | ||||||
|  |     let nonce: String? | ||||||
|  |     let meta: [String: AnyCodable] | ||||||
|  |     let membersMentioned: [String]? | ||||||
|  |     let editedAt: Date? | ||||||
|  |     let attachments: [SnCloudFile] | ||||||
|  |     let reactions: [SnChatReaction] | ||||||
|  |     let repliedMessageId: String? | ||||||
|  |     let forwardedMessageId: String? | ||||||
|  |     let senderId: String | ||||||
|  |     let sender: SnChatMember | ||||||
|  |     let chatRoomId: String | ||||||
|  |     let createdAt: Date | ||||||
|  |     let updatedAt: Date | ||||||
|  |     let deletedAt: Date? | ||||||
|  |  | ||||||
|  |     enum CodingKeys: String, CodingKey { | ||||||
|  |         case id, type, content, nonce, meta, membersMentioned, editedAt, attachments, reactions, repliedMessageId, forwardedMessageId, senderId, sender, chatRoomId, createdAt, updatedAt, deletedAt | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     init(from decoder: Decoder) throws { | ||||||
|  |         let container = try decoder.container(keyedBy: CodingKeys.self) | ||||||
|  |         id = try container.decode(String.self, forKey: .id) | ||||||
|  |         type = try container.decode(String.self, forKey: .type) | ||||||
|  |         content = try container.decodeIfPresent(String.self, forKey: .content) | ||||||
|  |         nonce = try container.decodeIfPresent(String.self, forKey: .nonce) | ||||||
|  |         meta = try container.decode([String: AnyCodable].self, forKey: .meta) | ||||||
|  |         membersMentioned = try container.decodeIfPresent([String].self, forKey: .membersMentioned) ?? [] | ||||||
|  |         editedAt = try container.decodeIfPresent(Date.self, forKey: .editedAt) | ||||||
|  |         attachments = try container.decode([SnCloudFile].self, forKey: .attachments) | ||||||
|  |         reactions = try container.decode([SnChatReaction].self, forKey: .reactions) | ||||||
|  |         repliedMessageId = try container.decodeIfPresent(String.self, forKey: .repliedMessageId) | ||||||
|  |         forwardedMessageId = try container.decodeIfPresent(String.self, forKey: .forwardedMessageId) | ||||||
|  |         senderId = try container.decode(String.self, forKey: .senderId) | ||||||
|  |         sender = try container.decode(SnChatMember.self, forKey: .sender) | ||||||
|  |         chatRoomId = try container.decode(String.self, forKey: .chatRoomId) | ||||||
|  |         createdAt = try container.decode(Date.self, forKey: .createdAt) | ||||||
|  |         updatedAt = try container.decode(Date.self, forKey: .updatedAt) | ||||||
|  |         deletedAt = try container.decodeIfPresent(Date.self, forKey: .deletedAt) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnChatReaction: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let messageId: String | ||||||
|  |     let senderId: String | ||||||
|  |     let sender: SnChatMember | ||||||
|  |     let symbol: String | ||||||
|  |     let attitude: Int | ||||||
|  |     let createdAt: Date | ||||||
|  |     let updatedAt: Date | ||||||
|  |     let deletedAt: Date? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnChatMember: Codable, Identifiable { | ||||||
|  |     let id: String | ||||||
|  |     let chatRoomId: String | ||||||
|  |     let chatRoom: SnChatRoom? | ||||||
|  |     let accountId: String | ||||||
|  |     let account: SnAccount | ||||||
|  |     let nick: String? | ||||||
|  |     let role: Int | ||||||
|  |     let notify: Int | ||||||
|  |     let joinedAt: Date? | ||||||
|  |     let breakUntil: Date? | ||||||
|  |     let timeoutUntil: Date? | ||||||
|  |     let isBot: Bool | ||||||
|  |     let status: SnAccountStatus? | ||||||
|  |     let createdAt: Date | ||||||
|  |     let updatedAt: Date | ||||||
|  |     let deletedAt: Date? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SnChatSummary: Codable { | ||||||
|  |     let unreadCount: Int | ||||||
|  |     let lastMessage: SnChatMessage? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ChatRoomsResponse { | ||||||
|  |     let rooms: [SnChatRoom] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ChatInvitesResponse { | ||||||
|  |     let invites: [SnChatMember] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct MessageSyncResponse: Codable { | ||||||
|  |     let messages: [SnChatMessage] | ||||||
|  |     let currentTimestamp: Date | ||||||
|  |  | ||||||
|  |     enum CodingKeys: String, CodingKey { | ||||||
|  |         case messages | ||||||
|  |         case currentTimestamp = "current_timestamp" | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								ios/WatchRunner Watch App/Previews/CustomPreviews.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | |||||||
|  | // | ||||||
|  | //  CustomPreviews.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     NavigationStack { | ||||||
|  |         ActivityListView(filter: "Preview", mockActivities: SnActivity.mock) | ||||||
|  |             .environmentObject(AppState()) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								ios/WatchRunner Watch App/Previews/MockData.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | |||||||
|  | // | ||||||
|  | //  MockData.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | #if DEBUG | ||||||
|  | extension SnActivity { | ||||||
|  |     static var mock: [SnActivity] { | ||||||
|  |         let mockPublisher = SnPublisher(id: "pub1", name: "Mock Publisher", nick: "mock_nick", description: "A publisher for testing", picture: SnCloudFile(id: "mock_avatar_id", mimeType: "image/png")) | ||||||
|  |         let mockTag1 = SnPostTag(id: "tag1", slug: "swiftui", name: "SwiftUI") | ||||||
|  |         let mockTag2 = SnPostTag(id: "tag2", slug: "watchos", name: "watchOS") | ||||||
|  |         let mockAttachment1 = SnCloudFile(id: "mock_image_id_1", mimeType: "image/jpeg") | ||||||
|  |         let mockAttachment2 = SnCloudFile(id: "mock_image_id_2", mimeType: "image/png") | ||||||
|  |  | ||||||
|  |         let post1 = SnPost(id: "1", title: "Hello from a Mock Post!", content: "This is a mock post content. It can be a bit longer to see how it wraps.", publisher: mockPublisher, attachments: [mockAttachment1, mockAttachment2], tags: [mockTag1, mockTag2]) | ||||||
|  |         let activity1 = SnActivity(id: "1", type: "posts.new", data: .post(post1), createdAt: Date()) | ||||||
|  |          | ||||||
|  |         let realm1 = SnRealm(id: "r1", name: "SwiftUI Previews", description: "A place for designing in previews.") | ||||||
|  |         let publisher1 = SnPublisher(id: "p1", name: "The Mock Times", nick: "mock_times", description: "All the news that's fit to mock.", picture: nil) | ||||||
|  |         let article1 = SnWebArticle(id: "a1", title: "The Art of Mocking Data", url: "https://example.com") | ||||||
|  |  | ||||||
|  |         let discoveryItem1 = DiscoveryItem(type: "realm", data: .realm(realm1)) | ||||||
|  |         let discoveryItem2 = DiscoveryItem(type: "publisher", data: .publisher(publisher1)) | ||||||
|  |         let discoveryItem3 = DiscoveryItem(type: "article", data: .article(article1)) | ||||||
|  |         let discoveryData = DiscoveryData(items: [discoveryItem1, discoveryItem2, discoveryItem3]) | ||||||
|  |         let activity2 = SnActivity(id: "2", type: "discovery", data: .discovery(discoveryData), createdAt: Date()) | ||||||
|  |          | ||||||
|  |         return [activity1, activity2] | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | #endif | ||||||
							
								
								
									
										95
									
								
								ios/WatchRunner Watch App/Services/ImageLoader.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,95 @@ | |||||||
|  | // | ||||||
|  | //  ImageLoader.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  | import Kingfisher | ||||||
|  | import KingfisherWebP | ||||||
|  | import Combine | ||||||
|  |  | ||||||
|  | // MARK: - Image Loader | ||||||
|  |  | ||||||
|  | @MainActor | ||||||
|  | class ImageLoader: ObservableObject { | ||||||
|  |     @Published var image: Image? | ||||||
|  |     @Published var errorMessage: String? | ||||||
|  |     @Published var isLoading = false | ||||||
|  |  | ||||||
|  |     private var currentTask: DownloadTask? | ||||||
|  |  | ||||||
|  |     init() {} | ||||||
|  |  | ||||||
|  |     deinit { | ||||||
|  |         currentTask?.cancel() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func loadImage(from initialUrl: URL, token: String) async { | ||||||
|  |         isLoading = true | ||||||
|  |         errorMessage = nil | ||||||
|  |         image = nil | ||||||
|  |  | ||||||
|  |         // Create request modifier for authorization | ||||||
|  |         let modifier = AnyModifier { request in | ||||||
|  |             var r = request | ||||||
|  |             r.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |             r.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |             return r | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Use WebP processor as default since the app seems to handle WebP images | ||||||
|  |         let processor = WebPProcessor.default | ||||||
|  |  | ||||||
|  |         // Use KingfisherManager to retrieve image with caching | ||||||
|  |         currentTask = KingfisherManager.shared.retrieveImage( | ||||||
|  |             with: initialUrl, | ||||||
|  |             options: [ | ||||||
|  |                 .requestModifier(modifier), | ||||||
|  |                 .processor(processor), | ||||||
|  |                 .cacheOriginalImage, // Cache the original image data | ||||||
|  |                 .loadDiskFileSynchronously // Load from disk cache synchronously if available | ||||||
|  |             ] | ||||||
|  |         ) { [weak self] result in | ||||||
|  |             guard let self = self else { return } | ||||||
|  |  | ||||||
|  |             Task { @MainActor in | ||||||
|  |                 switch result { | ||||||
|  |                 case .success(let value): | ||||||
|  |                     self.image = Image(uiImage: value.image) | ||||||
|  |                     self.isLoading = false | ||||||
|  |                 case .failure(_): | ||||||
|  |                     // If WebP processor fails (likely due to format), try with default processor | ||||||
|  |                     let defaultProcessor = DefaultImageProcessor.default | ||||||
|  |                     self.currentTask = KingfisherManager.shared.retrieveImage( | ||||||
|  |                         with: initialUrl, | ||||||
|  |                         options: [ | ||||||
|  |                             .requestModifier(modifier), | ||||||
|  |                             .processor(defaultProcessor), | ||||||
|  |                             .cacheOriginalImage, | ||||||
|  |                             .loadDiskFileSynchronously | ||||||
|  |                         ] | ||||||
|  |                     ) { [weak self] fallbackResult in | ||||||
|  |                         guard let self = self else { return } | ||||||
|  |  | ||||||
|  |                         Task { @MainActor in | ||||||
|  |                             switch fallbackResult { | ||||||
|  |                             case .success(let value): | ||||||
|  |                                 self.image = Image(uiImage: value.image) | ||||||
|  |                             case .failure(let fallbackError): | ||||||
|  |                                 self.errorMessage = fallbackError.localizedDescription | ||||||
|  |                                 print("[watchOS] Image loading failed: \(fallbackError.localizedDescription)") | ||||||
|  |                             } | ||||||
|  |                             self.isLoading = false | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func cancel() { | ||||||
|  |         currentTask?.cancel() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										637
									
								
								ios/WatchRunner Watch App/Services/NetworkService.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,637 @@ | |||||||
|  | // | ||||||
|  | //  NetworkService.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. // | ||||||
|  |  | ||||||
|  | import Combine | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // MARK: - WebSocket Data Structures | ||||||
|  |  | ||||||
|  | enum WebSocketState: Equatable { | ||||||
|  |     case connected | ||||||
|  |     case connecting | ||||||
|  |     case disconnected | ||||||
|  |     case serverDown | ||||||
|  |     case duplicateDevice | ||||||
|  |     case error(String) | ||||||
|  |      | ||||||
|  |     // Equatable conformance | ||||||
|  |     static func == (lhs: WebSocketState, rhs: WebSocketState) -> Bool { | ||||||
|  |         switch (lhs, rhs) { | ||||||
|  |         case (.connected, .connected), | ||||||
|  |             (.connecting, .connecting), | ||||||
|  |             (.disconnected, .disconnected), | ||||||
|  |             (.serverDown, .serverDown), | ||||||
|  |             (.duplicateDevice, .duplicateDevice): | ||||||
|  |             return true | ||||||
|  |         case let (.error(a), .error(b)): | ||||||
|  |             return a == b | ||||||
|  |         default: | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct WebSocketPacket { | ||||||
|  |     let type: String | ||||||
|  |     let data: [String: Any]? | ||||||
|  |     let endpoint: String? | ||||||
|  |     let errorMessage: String? | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: - Network Service | ||||||
|  |  | ||||||
|  | class NetworkService { | ||||||
|  |     private let session = URLSession.shared | ||||||
|  |      | ||||||
|  |     // Add a serial queue for WebSocket operations | ||||||
|  |     private let webSocketQueue = DispatchQueue(label: "com.solian.websocketQueue") | ||||||
|  |      | ||||||
|  |     func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> ActivityResponse { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         var components = URLComponents(url: baseURL.appendingPathComponent("/sphere/activities"), resolvingAgainstBaseURL: false)! | ||||||
|  |         var queryItems = [URLQueryItem(name: "take", value: "20")] | ||||||
|  |         if filter.lowercased() != "explore" { | ||||||
|  |             queryItems.append(URLQueryItem(name: "filter", value: filter.lowercased())) | ||||||
|  |         } | ||||||
|  |         if let cursor = cursor { | ||||||
|  |             queryItems.append(URLQueryItem(name: "cursor", value: cursor)) | ||||||
|  |         } | ||||||
|  |         components.queryItems = queryItems | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: components.url!) | ||||||
|  |         request.httpMethod = "GET" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |          | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, _) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |          | ||||||
|  |         let activities = try decoder.decode([SnActivity].self, from: data) | ||||||
|  |          | ||||||
|  |         let hasMore = (activities.first?.type ?? "empty") != "empty" | ||||||
|  |         let nextCursor = activities.isEmpty ? nil : activities.map { $0.createdAt }.min()?.ISO8601Format() | ||||||
|  |          | ||||||
|  |         return ActivityResponse(activities: activities, hasMore: hasMore, nextCursor: nextCursor) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func createPost(title: String, content: String, token: String, serverUrl: String) async throws { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/sphere/posts") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = "POST" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let body: [String: Any] = ["title": title, "content": content] | ||||||
|  |         request.httpBody = try JSONSerialization.data(withJSONObject: body) | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 { | ||||||
|  |             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||||
|  |             print("[watchOS] createPost failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||||
|  |             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func fetchNotifications(offset: Int = 0, take: Int = 20, token: String, serverUrl: String) async throws -> NotificationResponse { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         var components = URLComponents(url: baseURL.appendingPathComponent("/ring/notifications"), resolvingAgainstBaseURL: false)! | ||||||
|  |         let queryItems = [URLQueryItem(name: "offset", value: String(offset)), URLQueryItem(name: "take", value: String(take))] | ||||||
|  |         components.queryItems = queryItems | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: components.url!) | ||||||
|  |         request.httpMethod = "GET" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |          | ||||||
|  |         let notifications = try decoder.decode([SnNotification].self, from: data) | ||||||
|  |          | ||||||
|  |         let httpResponse = response as? HTTPURLResponse | ||||||
|  |         let total = Int(httpResponse?.value(forHTTPHeaderField: "X-Total") ?? "0") ?? 0 | ||||||
|  |         let hasMore = offset + notifications.count < total | ||||||
|  |          | ||||||
|  |         return NotificationResponse(notifications: notifications, total: total, hasMore: hasMore) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func fetchUserProfile(token: String, serverUrl: String) async throws -> SnAccount { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/pass/accounts/me") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = "GET" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, _) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |          | ||||||
|  |         return try decoder.decode(SnAccount.self, from: data) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func fetchAccountStatus(token: String, serverUrl: String) async throws -> SnAccountStatus? { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = "GET" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 { | ||||||
|  |             return nil | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |          | ||||||
|  |         return try decoder.decode(SnAccountStatus.self, from: data) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func createOrUpdateStatus(attitude: Int, isInvisible: Bool, isNotDisturb: Bool, label: String?, token: String, serverUrl: String) async throws -> SnAccountStatus { | ||||||
|  |         // Check if there\'s already a customized status | ||||||
|  |         let existingStatus = try? await fetchAccountStatus(token: token, serverUrl: serverUrl) | ||||||
|  |         let method = (existingStatus?.isCustomized == true) ? "PATCH" : "POST" | ||||||
|  |          | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = method | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         var body: [String: Any] = [ | ||||||
|  |             "attitude": attitude, | ||||||
|  |             "is_invisible": isInvisible, | ||||||
|  |             "is_not_disturb": isNotDisturb, | ||||||
|  |         ] | ||||||
|  |          | ||||||
|  |         if let label = label, !label.isEmpty { | ||||||
|  |             body["label"] = label | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         request.httpBody = try JSONSerialization.data(withJSONObject: body) | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 && httpResponse.statusCode != 200 { | ||||||
|  |             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||||
|  |             print("[watchOS] createOrUpdateStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||||
|  |             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |          | ||||||
|  |         return try decoder.decode(SnAccountStatus.self, from: data) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func clearStatus(token: String, serverUrl: String) async throws { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = "DELETE" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 { | ||||||
|  |             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||||
|  |             print("[watchOS] clearStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||||
|  |             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // MARK: - Chat API Methods | ||||||
|  |      | ||||||
|  |     func fetchChatRooms(token: String, serverUrl: String) async throws -> ChatRoomsResponse { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/sphere/chat") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = "GET" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, _) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |          | ||||||
|  |         let rooms = try decoder.decode([SnChatRoom].self, from: data) | ||||||
|  |         return ChatRoomsResponse(rooms: rooms) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func fetchChatRoom(identifier: String, token: String, serverUrl: String) async throws -> SnChatRoom { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/sphere/chat/\(identifier)") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = "GET" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 { | ||||||
|  |             throw URLError(.resourceUnavailable) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |          | ||||||
|  |         return try decoder.decode(SnChatRoom.self, from: data) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func fetchChatInvites(token: String, serverUrl: String) async throws -> ChatInvitesResponse { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/sphere/chat/invites") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = "GET" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, _) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |          | ||||||
|  |         let invites = try decoder.decode([SnChatMember].self, from: data) | ||||||
|  |         return ChatInvitesResponse(invites: invites) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func acceptChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/accept") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = "POST" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { | ||||||
|  |             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||||
|  |             print("[watchOS] acceptChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||||
|  |             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func declineChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |         let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/decline") | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: url) | ||||||
|  |         request.httpMethod = "POST" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { | ||||||
|  |             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||||
|  |             print("[watchOS] declineChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||||
|  |             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // MARK: - Message API Methods | ||||||
|  |      | ||||||
|  |     func fetchChatMessages(chatRoomId: String, token: String, serverUrl: String, before: Date? = nil, take: Int = 50) async throws -> [SnChatMessage] { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Try a different pattern: /sphere/chat/messages with roomId as query param | ||||||
|  |         var components = URLComponents( | ||||||
|  |             url: baseURL.appendingPathComponent("/sphere/chat/\(chatRoomId)/messages"), | ||||||
|  |             resolvingAgainstBaseURL: false | ||||||
|  |         )! | ||||||
|  |         var queryItems = [ | ||||||
|  |             URLQueryItem(name: "take", value: String(take)), | ||||||
|  |         ] | ||||||
|  |         if let before = before { | ||||||
|  |             queryItems.append(URLQueryItem(name: "before", value: ISO8601DateFormatter().string(from: before))) | ||||||
|  |         } | ||||||
|  |         components.queryItems = queryItems | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: components.url!) | ||||||
|  |         request.httpMethod = "GET" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |          | ||||||
|  |         if let httpResponse = response as? HTTPURLResponse { | ||||||
|  |             _ = String(data: data, encoding: .utf8) ?? "Unable to decode response body" | ||||||
|  |              | ||||||
|  |             if httpResponse.statusCode != 200 { | ||||||
|  |                 print("[watchOS] fetchChatMessages failed with status \(httpResponse.statusCode)") | ||||||
|  |                 throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Check if data is empty | ||||||
|  |         if data.isEmpty { | ||||||
|  |             print("[watchOS] fetchChatMessages received empty response data") | ||||||
|  |             return [] | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |          | ||||||
|  |         do { | ||||||
|  |             let messages = try decoder.decode([SnChatMessage].self, from: data) | ||||||
|  |             print("[watchOS] fetchChatMessages successfully decoded \(messages.count) messages") | ||||||
|  |             return messages | ||||||
|  |         } catch { | ||||||
|  |             print("error: ", error) | ||||||
|  |             throw error | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // MARK: - WebSocket | ||||||
|  |  | ||||||
|  |     private var webSocketTask: URLSessionWebSocketTask? | ||||||
|  |     private var heartbeatTimer: Timer? | ||||||
|  |     private var reconnectTimer: Timer? | ||||||
|  |     private var isDisconnectingManually = false | ||||||
|  |  | ||||||
|  |     private var lastToken: String? | ||||||
|  |     private var lastServerUrl: String? | ||||||
|  |  | ||||||
|  |     private var heartbeatAt: Date? | ||||||
|  |     var heartbeatDelay: TimeInterval? | ||||||
|  |  | ||||||
|  |     private let connectLock = NSLock() | ||||||
|  |      | ||||||
|  |     private let packetSubject = PassthroughSubject<WebSocketPacket, Error>() | ||||||
|  |     private let stateSubject = CurrentValueSubject<WebSocketState, Never>(.disconnected) // Changed to CurrentValueSubject | ||||||
|  |      | ||||||
|  |     private var currentConnectionState: WebSocketState = .disconnected { // New property | ||||||
|  |         didSet { | ||||||
|  |             // Only send updates if the state has actually changed | ||||||
|  |             if oldValue != currentConnectionState { | ||||||
|  |                 stateSubject.send(currentConnectionState) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     var packetStream: AnyPublisher<WebSocketPacket, Error> { | ||||||
|  |         packetSubject.eraseToAnyPublisher() | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     var stateStream: AnyPublisher<WebSocketState, Never> { | ||||||
|  |         stateSubject.eraseToAnyPublisher() | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func connectWebSocket(token: String, serverUrl: String) { | ||||||
|  |         webSocketQueue.async { [weak self] in | ||||||
|  |             guard let self = self else { return } | ||||||
|  |  | ||||||
|  |             self.connectLock.lock() | ||||||
|  |             defer { self.connectLock.unlock() } | ||||||
|  |              | ||||||
|  |             // Prevent redundant connection attempts | ||||||
|  |             if self.currentConnectionState == .connecting || self.currentConnectionState == .connected { | ||||||
|  |                 print("[WebSocket] Already connecting or connected, ignoring new connect request.") | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             self.currentConnectionState = .connecting | ||||||
|  |  | ||||||
|  |             // Ensure any existing task is cancelled before starting a new one | ||||||
|  |             self.webSocketTask?.cancel(with: .goingAway, reason: nil) | ||||||
|  |             self.webSocketTask = nil | ||||||
|  |  | ||||||
|  |             self.isDisconnectingManually = false // Reset this flag for a new connection attempt | ||||||
|  |  | ||||||
|  |             self.lastToken = token | ||||||
|  |             self.lastServerUrl = serverUrl | ||||||
|  |  | ||||||
|  |             guard var urlComponents = URLComponents(string: serverUrl) else { | ||||||
|  |                 self.currentConnectionState = .error("Invalid server URL") | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             urlComponents.scheme = urlComponents.scheme?.replacingOccurrences(of: "http", with: "ws") | ||||||
|  |             urlComponents.path = "/ws" | ||||||
|  |             urlComponents.queryItems = [URLQueryItem(name: "deviceAlt", value: "watch")] | ||||||
|  |  | ||||||
|  |             guard let url = urlComponents.url else { | ||||||
|  |                 self.currentConnectionState = .error("Invalid WebSocket URL") | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var request = URLRequest(url: url) | ||||||
|  |             request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |             request.addValue("application/json", forHTTPHeaderField: "Content-Type") | ||||||
|  |              | ||||||
|  |             print("[WebSocket] Trying connecting to \(url)") | ||||||
|  |              | ||||||
|  |             self.webSocketTask = self.session.webSocketTask(with: request) | ||||||
|  |             self.webSocketTask?.resume() | ||||||
|  |  | ||||||
|  |             self.listenForWebSocketMessages() | ||||||
|  |             self.scheduleHeartbeat() | ||||||
|  |             self.currentConnectionState = .connected | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func listenForWebSocketMessages() { | ||||||
|  |         // Ensure webSocketTask is still valid before attempting to receive | ||||||
|  |         guard let task = webSocketTask else { | ||||||
|  |             print("[WebSocket] listenForWebSocketMessages: webSocketTask is nil, stopping listen.") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         task.receive { [weak self] result in | ||||||
|  |             guard let self = self else { return } | ||||||
|  |              | ||||||
|  |             switch result { | ||||||
|  |             case .failure(let error): | ||||||
|  |                 print("[WebSocket] Error in receiving message: \(error)") | ||||||
|  |                 // Only attempt to reconnect if not manually disconnecting | ||||||
|  |                 if !self.isDisconnectingManually { | ||||||
|  |                     self.currentConnectionState = .error(error.localizedDescription) | ||||||
|  |                     self.scheduleReconnect() | ||||||
|  |                 } else { | ||||||
|  |                     // If manually disconnecting, just ensure state is disconnected | ||||||
|  |                     self.currentConnectionState = .disconnected | ||||||
|  |                 } | ||||||
|  |             case .success(let message): | ||||||
|  |                 switch message { | ||||||
|  |                 case .string(let text): | ||||||
|  |                     self.handleWebSocketMessage(text: text) | ||||||
|  |                 case .data(let data): | ||||||
|  |                     if let text = String(data: data, encoding: .utf8) { | ||||||
|  |                         self.handleWebSocketMessage(text: text) | ||||||
|  |                     } | ||||||
|  |                 @unknown default: | ||||||
|  |                     break | ||||||
|  |                 } | ||||||
|  |                 // Continue listening for next message only if task is still valid | ||||||
|  |                 if self.webSocketTask === task { // Check if it's the same task | ||||||
|  |                     self.listenForWebSocketMessages() | ||||||
|  |                 } else { | ||||||
|  |                     print("[WebSocket] listenForWebSocketMessages: Task changed, stopping listen for old task.") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private func handleWebSocketMessage(text: String) { | ||||||
|  |         guard let data = text.data(using: .utf8) else { | ||||||
|  |             print("[WebSocket] Could not convert message to data") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         do { | ||||||
|  |             if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], | ||||||
|  |                let type = json["type"] as? String | ||||||
|  |             { | ||||||
|  |                 let packet = WebSocketPacket( | ||||||
|  |                     type: type, | ||||||
|  |                     data: json["data"] as? [String: Any], | ||||||
|  |                     endpoint: json["endpoint"] as? String, | ||||||
|  |                     errorMessage: json["errorMessage"] as? String | ||||||
|  |                 ) | ||||||
|  |                  | ||||||
|  |                 print("[WebSocket] Received packet: \(packet.type) \(packet.errorMessage ?? "")") | ||||||
|  |                  | ||||||
|  |                 if packet.type == "error.dupe" { | ||||||
|  |                     self.currentConnectionState = .duplicateDevice | ||||||
|  |                     self.disconnectWebSocket() | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 if packet.type == "pong" { | ||||||
|  |                     if let beatAt = self.heartbeatAt { | ||||||
|  |                         let now = Date() | ||||||
|  |                         self.heartbeatDelay = now.timeIntervalSince(beatAt) | ||||||
|  |                         print("[WebSocket] Server respond last heartbeat for \((self.heartbeatDelay ?? 0) * 1000) ms") | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 self.packetSubject.send(packet) | ||||||
|  |             } | ||||||
|  |         } catch { | ||||||
|  |             print("[WebSocket] Could not parse message json: \(error.localizedDescription)") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private func scheduleReconnect() { | ||||||
|  |         reconnectTimer?.invalidate() | ||||||
|  |         reconnectTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in | ||||||
|  |             guard let self = self, let token = self.lastToken, let serverUrl = self.lastServerUrl else { return } | ||||||
|  |             print("[WebSocket] Attempting to reconnect...") | ||||||
|  |              | ||||||
|  |             // No need to call disconnectWebSocket here, connectWebSocket will handle cancelling old task | ||||||
|  |             self.isDisconnectingManually = false // Reset for the new connection attempt | ||||||
|  |              | ||||||
|  |             self.connectWebSocket(token: token, serverUrl: serverUrl) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private func scheduleHeartbeat() { | ||||||
|  |         heartbeatTimer?.invalidate() | ||||||
|  |         heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in | ||||||
|  |             self?.beatTheHeart() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private func beatTheHeart() { | ||||||
|  |         heartbeatAt = Date() | ||||||
|  |         print("[WebSocket] We\'re beating the heart! \(String(describing: self.heartbeatAt))") | ||||||
|  |         sendWebSocketMessage(message: "{\"type\":\"ping\"}") | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func sendWebSocketMessage(message: String) { | ||||||
|  |         webSocketTask?.send(.string(message)) { error in | ||||||
|  |             if let error = error { | ||||||
|  |                 print("[WebSocket] Error sending message: \(error.localizedDescription)") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func disconnectWebSocket() { | ||||||
|  |         isDisconnectingManually = true | ||||||
|  |         reconnectTimer?.invalidate() | ||||||
|  |         heartbeatTimer?.invalidate() | ||||||
|  |          | ||||||
|  |         // Cancel the task and then nil it out | ||||||
|  |         webSocketTask?.cancel(with: .goingAway, reason: nil) | ||||||
|  |         webSocketTask = nil // Set to nil immediately after cancelling | ||||||
|  |          | ||||||
|  |         self.currentConnectionState = .disconnected | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										56
									
								
								ios/WatchRunner Watch App/State/AppState.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,56 @@ | |||||||
|  | // | ||||||
|  | //  AppState.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  | import Combine | ||||||
|  |  | ||||||
|  | // MARK: - App State | ||||||
|  |  | ||||||
|  | @MainActor | ||||||
|  | class AppState: ObservableObject { | ||||||
|  |     @Published var token: String? = nil | ||||||
|  |     @Published var serverUrl: String? = nil | ||||||
|  |     @Published var isReady = false | ||||||
|  |  | ||||||
|  |     let networkService = NetworkService() | ||||||
|  |     private var wcService = WatchConnectivityService() | ||||||
|  |     private var cancellables = Set<AnyCancellable>() | ||||||
|  |     private var hasAttemptedConnection = false | ||||||
|  |  | ||||||
|  |     init() { | ||||||
|  |         wcService.$token.combineLatest(wcService.$serverUrl, wcService.$isFetched) | ||||||
|  |             .receive(on: DispatchQueue.main) | ||||||
|  |             .sink { [weak self] (token: String?, serverUrl: String?, isFetched: Bool?) in | ||||||
|  |                 guard let self = self else { return } | ||||||
|  |                  | ||||||
|  |                 self.token = token | ||||||
|  |                 self.serverUrl = serverUrl | ||||||
|  |  | ||||||
|  |                 if let token = token, let serverUrl = serverUrl, !token.isEmpty, !serverUrl.isEmpty { | ||||||
|  |                     self.isReady = true | ||||||
|  |                     // Only connect once when we have valid credentials and tried fetch from phone | ||||||
|  |                     if !self.hasAttemptedConnection && isFetched == true { | ||||||
|  |                         self.hasAttemptedConnection = true | ||||||
|  |                         print("[AppState] Connecting WebSocket to server: \(serverUrl)") | ||||||
|  |                         self.networkService.connectWebSocket(token: token, serverUrl: serverUrl) | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     self.isReady = false | ||||||
|  |                     if self.hasAttemptedConnection { | ||||||
|  |                         self.hasAttemptedConnection = false | ||||||
|  |                         // Disconnect WebSocket if token or serverUrl become invalid | ||||||
|  |                         self.networkService.disconnectWebSocket() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .store(in: &cancellables) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func requestData() { | ||||||
|  |         wcService.requestDataFromPhone() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,93 @@ | |||||||
|  | // | ||||||
|  | //  WatchConnectivityService.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  | import WatchConnectivity | ||||||
|  | import Combine | ||||||
|  |  | ||||||
|  | // MARK: - Watch Connectivity | ||||||
|  |  | ||||||
|  | class WatchConnectivityService: NSObject, WCSessionDelegate, ObservableObject { | ||||||
|  |     @Published var token: String? | ||||||
|  |     @Published var serverUrl: String? | ||||||
|  |     @Published var isFetched: Bool? | ||||||
|  |  | ||||||
|  |     private let session: WCSession | ||||||
|  |     private let userDefaults = UserDefaults.standard | ||||||
|  |     private let tokenKey = "token" | ||||||
|  |     private let serverUrlKey = "serverUrl" | ||||||
|  |  | ||||||
|  |     override init() { | ||||||
|  |         self.session = .default | ||||||
|  |         super.init() | ||||||
|  |         print("[watchOS] Activating WCSession") | ||||||
|  |         self.session.delegate = self | ||||||
|  |         self.session.activate() | ||||||
|  |          | ||||||
|  |         // Load cached data | ||||||
|  |         self.token = userDefaults.string(forKey: tokenKey) | ||||||
|  |         self.serverUrl = userDefaults.string(forKey: serverUrlKey) | ||||||
|  |         self.isFetched = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { | ||||||
|  |         if let error = error { | ||||||
|  |             print("[watchOS] WCSession activation failed with error: \(error.localizedDescription)") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         print("[watchOS] WCSession activated with state: \(activationState.rawValue)") | ||||||
|  |         if activationState == .activated { | ||||||
|  |             requestDataFromPhone() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { | ||||||
|  |         print("[watchOS] Received message: \(message)") | ||||||
|  |         DispatchQueue.main.async { | ||||||
|  |             if let token = message["token"] as? String { | ||||||
|  |                 self.token = token | ||||||
|  |                 self.userDefaults.set(token, forKey: self.tokenKey) | ||||||
|  |             } | ||||||
|  |             if let serverUrl = message["serverUrl"] as? String { | ||||||
|  |                 self.serverUrl = serverUrl | ||||||
|  |                 self.userDefaults.set(serverUrl, forKey: self.serverUrlKey) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func requestDataFromPhone() { | ||||||
|  |         if self.isFetched == true { | ||||||
|  |             print("[watchOS] Skipped fetch from phone due to tried.") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         guard session.isReachable else { | ||||||
|  |             self.isFetched = true | ||||||
|  |             print("[watchOS] Phone is not reachable") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         print("[watchOS] Requesting data from phone") | ||||||
|  |         session.sendMessage(["request": "data"]) { [weak self] response in | ||||||
|  |             guard let self = self else { return } | ||||||
|  |             print("[watchOS] Received reply: \(response)") | ||||||
|  |             DispatchQueue.main.async { | ||||||
|  |                 self.isFetched = true | ||||||
|  |                 if let token = response["token"] as? String { | ||||||
|  |                     self.token = token | ||||||
|  |                     self.userDefaults.set(token, forKey: self.tokenKey) | ||||||
|  |                 } | ||||||
|  |                 if let serverUrl = response["serverUrl"] as? String { | ||||||
|  |                     self.serverUrl = serverUrl | ||||||
|  |                     self.userDefaults.set(serverUrl, forKey: self.serverUrlKey) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } errorHandler: { error in | ||||||
|  |             print("[watchOS] sendMessage failed with error: \(error.localizedDescription)") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								ios/WatchRunner Watch App/Utils/AttachmentUtils.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | |||||||
|  | // | ||||||
|  | //  AttachmentUtils.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | // MARK: - Helper Functions | ||||||
|  |  | ||||||
|  | func getAttachmentUrl(for fileId: String, serverUrl: String) -> URL? { | ||||||
|  |     let urlString: String | ||||||
|  |     if fileId.starts(with: "http") { | ||||||
|  |         urlString = fileId | ||||||
|  |     } else { | ||||||
|  |         urlString = "\(serverUrl)/drive/files/\(fileId)" | ||||||
|  |     } | ||||||
|  |     return URL(string: urlString) | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								ios/WatchRunner Watch App/ViewModels/ActivityViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,73 @@ | |||||||
|  | // | ||||||
|  | //  ActivityViewModel.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  | import Combine | ||||||
|  |  | ||||||
|  | // MARK: - View Models | ||||||
|  |  | ||||||
|  | @MainActor | ||||||
|  | class ActivityViewModel: ObservableObject { | ||||||
|  |     @Published var activities: [SnActivity] = [] | ||||||
|  |     @Published var isLoading = false | ||||||
|  |     @Published var isLoadingMore = false | ||||||
|  |     @Published var errorMessage: String? | ||||||
|  |     @Published var hasMore = false | ||||||
|  |  | ||||||
|  |     private let networkService = NetworkService() | ||||||
|  |     let filter: String | ||||||
|  |     private var isMock = false | ||||||
|  |     private var hasFetched = false | ||||||
|  |     private var nextCursor: String? | ||||||
|  |  | ||||||
|  |     init(filter: String, mockActivities: [SnActivity]? = nil) { | ||||||
|  |         self.filter = filter | ||||||
|  |         if let mockActivities = mockActivities { | ||||||
|  |             self.activities = mockActivities | ||||||
|  |             self.isMock = true | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func fetchActivities(token: String, serverUrl: String) async { | ||||||
|  |         if isMock || hasFetched { return } | ||||||
|  |         guard !isLoading else { return } | ||||||
|  |         isLoading = true | ||||||
|  |         errorMessage = nil | ||||||
|  |         hasFetched = true | ||||||
|  |         nextCursor = nil | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let response = try await networkService.fetchActivities(filter: filter, cursor: nil, token: token, serverUrl: serverUrl) | ||||||
|  |             self.activities = response.activities | ||||||
|  |             self.hasMore = response.hasMore | ||||||
|  |             self.nextCursor = response.nextCursor | ||||||
|  |         } catch { | ||||||
|  |             self.errorMessage = error.localizedDescription | ||||||
|  |             print("[watchOS] fetchActivities failed with error: \(error)") | ||||||
|  |             hasFetched = false | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoading = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func loadMoreActivities(token: String, serverUrl: String) async { | ||||||
|  |         guard !isLoadingMore && hasMore && nextCursor != nil else { return } | ||||||
|  |         isLoadingMore = true | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let response = try await networkService.fetchActivities(filter: filter, cursor: nextCursor, token: token, serverUrl: serverUrl) | ||||||
|  |             self.activities.append(contentsOf: response.activities) | ||||||
|  |             self.hasMore = response.hasMore | ||||||
|  |             self.nextCursor = response.nextCursor | ||||||
|  |         } catch { | ||||||
|  |             self.errorMessage = error.localizedDescription | ||||||
|  |             print("[watchOS] loadMoreActivities failed with error: \(error)") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoadingMore = false | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | // | ||||||
|  | //  ComposePostViewModel.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  | import Combine | ||||||
|  |  | ||||||
|  | @MainActor | ||||||
|  | class ComposePostViewModel: ObservableObject { | ||||||
|  |     @Published var title = "" | ||||||
|  |     @Published var content = "" | ||||||
|  |     @Published var isPosting = false | ||||||
|  |     @Published var errorMessage: String? | ||||||
|  |     @Published var didPost = false | ||||||
|  |  | ||||||
|  |     private let networkService = NetworkService() | ||||||
|  |  | ||||||
|  |     func createPost(token: String, serverUrl: String) async { | ||||||
|  |         guard !isPosting else { return } | ||||||
|  |         isPosting = true | ||||||
|  |         errorMessage = nil | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             try await networkService.createPost(title: title, content: content, token: token, serverUrl: serverUrl) | ||||||
|  |             didPost = true | ||||||
|  |         } catch { | ||||||
|  |             errorMessage = error.localizedDescription | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isPosting = false | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										284
									
								
								ios/WatchRunner Watch App/Views/AccountView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,284 @@ | |||||||
|  | // | ||||||
|  | //  AccountView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/30. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct AccountView: View { | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @State private var user: SnAccount? | ||||||
|  |     @State private var status: SnAccountStatus? | ||||||
|  |     @State private var isLoading = false | ||||||
|  |     @State private var error: Error? | ||||||
|  |     @State private var showingClearConfirmation = false | ||||||
|  |  | ||||||
|  |     @StateObject private var profileImageLoader = ImageLoader() | ||||||
|  |     @StateObject private var bannerImageLoader = ImageLoader() | ||||||
|  |  | ||||||
|  |     private let networkService = NetworkService() | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         ScrollView { | ||||||
|  |             if isLoading { | ||||||
|  |                 ProgressView() | ||||||
|  |                     .padding() | ||||||
|  |             } else if let error = error { | ||||||
|  |                 VStack { | ||||||
|  |                     Text("Failed to load account") | ||||||
|  |                         .foregroundColor(.red) | ||||||
|  |                     Text(error.localizedDescription) | ||||||
|  |                         .font(.caption) | ||||||
|  |                         .foregroundColor(.secondary) | ||||||
|  |                 } | ||||||
|  |                 .padding() | ||||||
|  |             } else if let user = user { | ||||||
|  |                 VStack(spacing: 16) { | ||||||
|  |                     // Banner | ||||||
|  |                     if user.profile.background != nil { | ||||||
|  |                         if bannerImageLoader.isLoading { | ||||||
|  |                             ProgressView() | ||||||
|  |                                 .frame(height: 80) | ||||||
|  |                         } else if let bannerImage = bannerImageLoader.image { | ||||||
|  |                             bannerImage | ||||||
|  |                                 .resizable() | ||||||
|  |                                 .aspectRatio(contentMode: .fill) | ||||||
|  |                                 .frame(height: 80) | ||||||
|  |                                 .clipped() | ||||||
|  |                                 .cornerRadius(8) | ||||||
|  |                         } else if bannerImageLoader.errorMessage != nil { | ||||||
|  |                             Rectangle() | ||||||
|  |                                 .fill(Color.gray.opacity(0.3)) | ||||||
|  |                                 .frame(height: 80) | ||||||
|  |                                 .cornerRadius(8) | ||||||
|  |                         } else { | ||||||
|  |                             Rectangle() | ||||||
|  |                                 .fill(Color.gray.opacity(0.3)) | ||||||
|  |                                 .frame(height: 80) | ||||||
|  |                                 .cornerRadius(8) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     // Profile Picture | ||||||
|  |                     HStack(spacing: 16) | ||||||
|  |                     { | ||||||
|  |                         if profileImageLoader.isLoading { | ||||||
|  |                             ProgressView() | ||||||
|  |                                 .frame(width: 60, height: 60) | ||||||
|  |                         } else if let profileImage = profileImageLoader.image { | ||||||
|  |                             profileImage | ||||||
|  |                                 .resizable() | ||||||
|  |                                 .frame(width: 60, height: 60) | ||||||
|  |                                 .clipShape(Circle()) | ||||||
|  |                         } else if profileImageLoader.errorMessage != nil { | ||||||
|  |                             Circle() | ||||||
|  |                                 .fill(Color.red.opacity(0.3)) | ||||||
|  |                                 .frame(width: 60, height: 60) | ||||||
|  |                                 .overlay( | ||||||
|  |                                     Image(systemName: "exclamationmark.triangle") | ||||||
|  |                                         .resizable() | ||||||
|  |                                         .scaledToFit() | ||||||
|  |                                         .foregroundColor(.red) | ||||||
|  |                                 ) | ||||||
|  |                         } else { | ||||||
|  |                             Circle() | ||||||
|  |                                 .fill(Color.gray.opacity(0.3)) | ||||||
|  |                                 .frame(width: 60, height: 60) | ||||||
|  |                                 .overlay( | ||||||
|  |                                     Image(systemName: "person.circle.fill") | ||||||
|  |                                         .resizable() | ||||||
|  |                                         .scaledToFit() | ||||||
|  |                                         .foregroundColor(.gray) | ||||||
|  |                                 ) | ||||||
|  |                         } | ||||||
|  |                          | ||||||
|  |                         // Username and Handle | ||||||
|  |                         VStack(alignment: .leading) { | ||||||
|  |                             Text(user.nick) | ||||||
|  |                                 .font(.headline) | ||||||
|  |                             Text("@\(user.name)") | ||||||
|  |                                 .font(.caption) | ||||||
|  |                                 .foregroundColor(.secondary) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     // Status | ||||||
|  |                     VStack(alignment: .leading, spacing: 8) { | ||||||
|  |                         HStack { | ||||||
|  |                             Text("Status") | ||||||
|  |                                 .font(.subheadline) | ||||||
|  |                                 .foregroundColor(.secondary) | ||||||
|  |                             Spacer() | ||||||
|  |                             if status?.isCustomized == true { | ||||||
|  |                                 Button(action: { | ||||||
|  |                                     showingClearConfirmation = true | ||||||
|  |                                 }) { | ||||||
|  |                                     ZStack { | ||||||
|  |                                         Circle() | ||||||
|  |                                             .fill(Color.red.opacity(0.1)) | ||||||
|  |                                             .frame(width: 28, height: 28) | ||||||
|  |                                         Image(systemName: "trash") | ||||||
|  |                                             .foregroundColor(.red) | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                                 .buttonStyle(.plain) | ||||||
|  |                                 .frame(width: 28, height: 28) | ||||||
|  |                             } | ||||||
|  |                             NavigationLink( | ||||||
|  |                                 destination: StatusCreationView(initialStatus: status?.isCustomized == true ? status : nil) | ||||||
|  |                                     .environmentObject(appState) | ||||||
|  |                             ) { | ||||||
|  |                                 ZStack { | ||||||
|  |                                     Circle() | ||||||
|  |                                         .fill(Color.blue.opacity(0.1)) | ||||||
|  |                                         .frame(width: 28, height: 28) | ||||||
|  |                                     Image(systemName: "pencil") | ||||||
|  |                                         .foregroundColor(.blue) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             .buttonStyle(.plain) | ||||||
|  |                             .frame(width: 28, height: 28) | ||||||
|  |                         } | ||||||
|  |                          | ||||||
|  |                         if let status = status { | ||||||
|  |                             VStack(alignment: .leading, spacing: 4) { | ||||||
|  |                                 HStack { | ||||||
|  |                                     Circle() | ||||||
|  |                                         .fill(status.isOnline ? Color.green : Color.gray) | ||||||
|  |                                         .frame(width: 8, height: 8) | ||||||
|  |                                     Text(status.label.isEmpty ? "No status" : status.label) | ||||||
|  |                                         .font(.body) | ||||||
|  |                                 } | ||||||
|  |                                  | ||||||
|  |                                 if status.isInvisible { | ||||||
|  |                                     Text("Invisible") | ||||||
|  |                                         .font(.caption) | ||||||
|  |                                         .foregroundColor(.secondary) | ||||||
|  |                                 } | ||||||
|  |                                 if status.isNotDisturb { | ||||||
|  |                                     Text("Do Not Disturb") | ||||||
|  |                                         .font(.caption) | ||||||
|  |                                         .foregroundColor(.secondary) | ||||||
|  |                                 } | ||||||
|  |                                 if let clearedAt = status.clearedAt { | ||||||
|  |                                     Text("Clears: \(clearedAt.formatted(date: .abbreviated, time: .shortened))") | ||||||
|  |                                         .font(.caption) | ||||||
|  |                                         .foregroundColor(.secondary) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } else { | ||||||
|  |                             Text("No status set") | ||||||
|  |                                 .font(.body) | ||||||
|  |                                 .foregroundColor(.secondary) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     // Level and Progress | ||||||
|  |                     VStack(alignment: .leading, spacing: 8) { | ||||||
|  |                         Text("Level \(user.profile.level)") | ||||||
|  |                             .font(.title3) | ||||||
|  |                             .bold() | ||||||
|  |                         ProgressView(value: user.profile.levelingProgress) | ||||||
|  |                             .progressViewStyle(LinearProgressViewStyle()) | ||||||
|  |                             .frame(height: 8) | ||||||
|  |                         Text("Experience: \(user.profile.experience)") | ||||||
|  |                             .font(.caption) | ||||||
|  |                             .foregroundColor(.secondary) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     // Bio | ||||||
|  |                     if let bio = user.profile.bio, !bio.isEmpty { | ||||||
|  |                         Text(bio) | ||||||
|  |                             .font(.body) | ||||||
|  |                             .multilineTextAlignment(.center) | ||||||
|  |                             .foregroundColor(.secondary) | ||||||
|  |                             .frame(alignment: .leading) | ||||||
|  |                     } else { | ||||||
|  |                         Text("No bio available") | ||||||
|  |                             .font(.body) | ||||||
|  |                             .foregroundColor(.secondary) | ||||||
|  |                             .frame(alignment: .leading) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     // Member since | ||||||
|  |                     Text("Joined at \(user.createdAt.formatted(.dateTime.month(.abbreviated).year()))") | ||||||
|  |                         .font(.caption) | ||||||
|  |                         .foregroundColor(.secondary) | ||||||
|  |                         .frame(alignment: .leading) | ||||||
|  |                 } | ||||||
|  |                 .padding() | ||||||
|  |                 // Load images when user data is available | ||||||
|  |                 .task(id: user.profile.picture?.id) { | ||||||
|  |                     if let serverUrl = appState.serverUrl, let pictureId = user.profile.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token { | ||||||
|  |                         await profileImageLoader.loadImage(from: imageUrl, token: token) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .task(id: user.profile.background?.id) { | ||||||
|  |                     if let serverUrl = appState.serverUrl, let backgroundId = user.profile.background?.id, let imageUrl = getAttachmentUrl(for: backgroundId, serverUrl: serverUrl), let token = appState.token { | ||||||
|  |                         await bannerImageLoader.loadImage(from: imageUrl, token: token) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 Text("No account data") | ||||||
|  |                     .padding() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Account") | ||||||
|  |         .confirmationDialog("Clear Status", isPresented: $showingClearConfirmation) { | ||||||
|  |             Button("Clear Status", role: .destructive) { | ||||||
|  |                 Task { | ||||||
|  |                     await clearStatus() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Button("Cancel", role: .cancel) {} | ||||||
|  |         } message: { | ||||||
|  |             Text("Are you sure you want to clear your status? This action cannot be undone.") | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             Task.detached { | ||||||
|  |                 await loadUserProfile() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private func loadUserProfile() async { | ||||||
|  |         guard let token = appState.token, let serverUrl = appState.serverUrl else { | ||||||
|  |             error = NSError(domain: "AccountView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"]) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoading = true | ||||||
|  |         error = nil | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             user = try await networkService.fetchUserProfile(token: token, serverUrl: serverUrl) | ||||||
|  |             status = try await networkService.fetchAccountStatus(token: token, serverUrl: serverUrl) | ||||||
|  |         } catch { | ||||||
|  |             self.error = error | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoading = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func clearStatus() async { | ||||||
|  |         guard let token = appState.token, let serverUrl = appState.serverUrl else { | ||||||
|  |             error = NSError(domain: "AccountView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"]) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             try await networkService.clearStatus(token: token, serverUrl: serverUrl) | ||||||
|  |             // Refresh status after clearing | ||||||
|  |             status = try await networkService.fetchAccountStatus(token: token, serverUrl: serverUrl) | ||||||
|  |         } catch { | ||||||
|  |             self.error = error | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     AccountView() | ||||||
|  |         .environmentObject(AppState()) | ||||||
|  | } | ||||||
							
								
								
									
										86
									
								
								ios/WatchRunner Watch App/Views/ActivityListView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,86 @@ | |||||||
|  | // | ||||||
|  | //  ActivityListView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | // MARK: - Views | ||||||
|  |  | ||||||
|  | struct ActivityListView: View { | ||||||
|  |     @StateObject private var viewModel: ActivityViewModel | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |  | ||||||
|  |     init(filter: String, mockActivities: [SnActivity]? = nil) { | ||||||
|  |         _viewModel = StateObject(wrappedValue: ActivityViewModel(filter: filter, mockActivities: mockActivities)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         Group { | ||||||
|  |             if viewModel.isLoading { | ||||||
|  |                 ProgressView() | ||||||
|  |             } else if let errorMessage = viewModel.errorMessage { | ||||||
|  |                 VStack { | ||||||
|  |                     Text("Error fetching data") | ||||||
|  |                         .font(.headline) | ||||||
|  |                     Text(errorMessage) | ||||||
|  |                         .font(.caption) | ||||||
|  |                         .lineLimit(nil) | ||||||
|  |                 } | ||||||
|  |                 .padding() | ||||||
|  |             } else if viewModel.activities.isEmpty { | ||||||
|  |                 Text("No activities found.") | ||||||
|  |             } else { | ||||||
|  |                 List { | ||||||
|  |                     ForEach(viewModel.activities) { activity in | ||||||
|  |                         switch activity.type { | ||||||
|  |                         case "posts.new", "posts.new.replies": | ||||||
|  |                             if case .post(let post) = activity.data { | ||||||
|  |                                 NavigationLink( | ||||||
|  |                                     destination: PostDetailView(post: post).environmentObject(appState) | ||||||
|  |                                 ) { | ||||||
|  |                                     PostRowView(post: post) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         case "discovery": | ||||||
|  |                             if case .discovery(let discoveryData) = activity.data { | ||||||
|  |                                 DiscoveryView(discoveryData: discoveryData) | ||||||
|  |                             } | ||||||
|  |                         default: | ||||||
|  |                             Text("Unknown activity type: \(activity.type)") | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     if viewModel.hasMore { | ||||||
|  |                         if viewModel.isLoadingMore { | ||||||
|  |                             HStack { | ||||||
|  |                                 Spacer() | ||||||
|  |                                 ProgressView() | ||||||
|  |                                 Spacer() | ||||||
|  |                             } | ||||||
|  |                         } else { | ||||||
|  |                             Button("Load More") { | ||||||
|  |                                 Task { | ||||||
|  |                                     if let token = appState.token, let serverUrl = appState.serverUrl { | ||||||
|  |                                         await viewModel.loadMoreActivities(token: token, serverUrl: serverUrl) | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             .frame(maxWidth: .infinity) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             if appState.isReady, let token = appState.token, let serverUrl = appState.serverUrl { | ||||||
|  |                 Task.detached { | ||||||
|  |                     await viewModel.fetchActivities(token: token, serverUrl: serverUrl) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .navigationTitle(viewModel.filter) | ||||||
|  |         .navigationBarTitleDisplayMode(.inline) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								ios/WatchRunner Watch App/Views/AppInfoHeaderView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,62 @@ | |||||||
|  | // | ||||||
|  | //  AppInfoHeader.swift | ||||||
|  | //  Runner | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/30. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Combine | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct AppInfoHeaderView : View { | ||||||
|  |     @EnvironmentObject var appState: AppState // Access AppState | ||||||
|  |     @State private var webSocketConnectionState: WebSocketState = .disconnected // New state for WebSocket status | ||||||
|  |     @State private var cancellables = Set<AnyCancellable>() // For managing subscriptions | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(alignment: .leading) { | ||||||
|  |             HStack(spacing: 12) { | ||||||
|  |                 Image("Logo") | ||||||
|  |                     .resizable() | ||||||
|  |                     .frame(width: 40, height: 40) | ||||||
|  |                  | ||||||
|  |                 VStack(alignment: .leading) { | ||||||
|  |                     Text("Solian").font(.headline) | ||||||
|  |                     Text("for Apple Watch").font(.system(size: 11)) | ||||||
|  |                      | ||||||
|  |                     // Display WebSocket connection status | ||||||
|  |                     Text(webSocketStatusMessage) | ||||||
|  |                         .font(.system(size: 10)) | ||||||
|  |                         .foregroundColor(.secondary) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             setupWebSocketListeners() | ||||||
|  |         } | ||||||
|  |         .onDisappear { | ||||||
|  |             cancellables.forEach { $0.cancel() } | ||||||
|  |             cancellables.removeAll() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private var webSocketStatusMessage: String { | ||||||
|  |         switch webSocketConnectionState { | ||||||
|  |         case .connected: return "Connected" | ||||||
|  |         case .connecting: return "Connecting..." | ||||||
|  |         case .disconnected: return "Disconnected" | ||||||
|  |         case .serverDown: return "Server Down" | ||||||
|  |         case .duplicateDevice: return "Duplicate Device" | ||||||
|  |         case .error(let msg): return "Error: \(msg)" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func setupWebSocketListeners() { | ||||||
|  |         appState.networkService.stateStream | ||||||
|  |             .receive(on: DispatchQueue.main) | ||||||
|  |             .sink { state in | ||||||
|  |                 webSocketConnectionState = state | ||||||
|  |             } | ||||||
|  |             .store(in: &cancellables) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										109
									
								
								ios/WatchRunner Watch App/Views/AttachmentView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,109 @@ | |||||||
|  | // | ||||||
|  | //  AttachmentImageView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  | import AVKit | ||||||
|  | import AVFoundation | ||||||
|  |  | ||||||
|  | struct AttachmentView: View { | ||||||
|  |     let attachment: SnCloudFile | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @StateObject private var imageLoader = ImageLoader() | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         Group { | ||||||
|  |             if let mimeType = attachment.mimeType { | ||||||
|  |                 if mimeType.starts(with: "image") { | ||||||
|  |                     if let serverUrl = appState.serverUrl, let imageUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) { | ||||||
|  |                         NavigationLink( | ||||||
|  |                             destination: ImageViewer(imageUrl: imageUrl).environmentObject(appState) | ||||||
|  |                         ) { | ||||||
|  |                             if imageLoader.isLoading { | ||||||
|  |                                 ProgressView() | ||||||
|  |                             } else if let image = imageLoader.image { | ||||||
|  |                                 image | ||||||
|  |                                     .resizable() | ||||||
|  |                                     .aspectRatio(contentMode: .fit) | ||||||
|  |                                     .frame(maxWidth: .infinity) | ||||||
|  |                                     .cornerRadius(8) | ||||||
|  |                             } else if let errorMessage = imageLoader.errorMessage { | ||||||
|  |                                 Text("Failed to load attachment: \(errorMessage)") | ||||||
|  |                                     .font(.caption) | ||||||
|  |                                     .foregroundColor(.red) | ||||||
|  |                                     .cornerRadius(8) | ||||||
|  |                             } else { | ||||||
|  |                                 Text("File: \(attachment.id)") | ||||||
|  |                                     .cornerRadius(8) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         .buttonStyle(PlainButtonStyle()) | ||||||
|  |                     } else { | ||||||
|  |                         Text("Image URL not available.") | ||||||
|  |                     } | ||||||
|  |                 } else if mimeType.starts(with: "video") { | ||||||
|  |                     if let serverUrl = appState.serverUrl, let videoUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) { | ||||||
|  |                         NavigationLink(destination: VideoPlayerView(videoUrl: videoUrl)) { | ||||||
|  |                             if imageLoader.isLoading { | ||||||
|  |                                 ProgressView() | ||||||
|  |                             } else if let image = imageLoader.image { | ||||||
|  |                                 ZStack { | ||||||
|  |                                     image | ||||||
|  |                                         .resizable() | ||||||
|  |                                         .aspectRatio(contentMode: .fit) | ||||||
|  |                                         .frame(maxWidth: .infinity) | ||||||
|  |                                         .cornerRadius(8) | ||||||
|  |  | ||||||
|  |                                     Image(systemName: "play.circle.fill") | ||||||
|  |                                         .resizable() | ||||||
|  |                                         .scaledToFit() | ||||||
|  |                                         .frame(width: 36, height: 36) | ||||||
|  |                                         .foregroundColor(.white) | ||||||
|  |                                         .shadow(color: .black.opacity(0.6), radius: 4, x: 0, y: 2) | ||||||
|  |                                 } | ||||||
|  |                             } else if imageLoader.errorMessage != nil { | ||||||
|  |                                 Image(systemName: "play.rectangle.fill") | ||||||
|  |                                     .resizable() | ||||||
|  |                                     .aspectRatio(contentMode: .fit) | ||||||
|  |                                     .frame(maxWidth: .infinity) | ||||||
|  |                                     .foregroundColor(.gray) | ||||||
|  |                                     .cornerRadius(8) | ||||||
|  |                             } else { | ||||||
|  |                                 ProgressView() | ||||||
|  |                                     .cornerRadius(8) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         .buttonStyle(PlainButtonStyle()) | ||||||
|  |                     } else { | ||||||
|  |                         Text("Video URL not available.") | ||||||
|  |                     } | ||||||
|  |                 } else if mimeType.starts(with: "audio") { | ||||||
|  |                     if let serverUrl = appState.serverUrl, let audioUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) { | ||||||
|  |                         AudioPlayerView(audioUrl: audioUrl) | ||||||
|  |                     } else { | ||||||
|  |                         Text("Cannot play audio: URL not available.") | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Text("Unsupported media type: \(mimeType)") | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 Text("File: \(attachment.id) (No MIME type)") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .task(id: attachment.id) { | ||||||
|  |             if let serverUrl = appState.serverUrl, let attachmentUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl), let token = appState.token { | ||||||
|  |                 if attachment.mimeType?.starts(with: "image") == true { | ||||||
|  |                     await imageLoader.loadImage(from: attachmentUrl, token: token) | ||||||
|  |                 } | ||||||
|  |                 if attachment.mimeType?.starts(with: "video") == true { | ||||||
|  |                     let thumbnailUrl = attachmentUrl | ||||||
|  |                         .appending(queryItems: [URLQueryItem(name: "thumbnail", value: "true")]) // Construct thumbnail URL | ||||||
|  |                     await imageLoader.loadImage(from: thumbnailUrl, token: token) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								ios/WatchRunner Watch App/Views/AudioPlayerView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | |||||||
|  |  | ||||||
|  | // | ||||||
|  | //  AudioPlayerView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  | import AVFoundation | ||||||
|  |  | ||||||
|  | struct AudioPlayerView: View { | ||||||
|  |     let audioUrl: URL | ||||||
|  |     @State private var player: AVPlayer? | ||||||
|  |     @State private var isPlaying: Bool = false | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         VStack { | ||||||
|  |             if player != nil { | ||||||
|  |                 Button(action: togglePlayPause) { | ||||||
|  |                     Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill") | ||||||
|  |                         .font(.largeTitle) | ||||||
|  |                 } | ||||||
|  |                 .buttonStyle(.plain) | ||||||
|  |             } else { | ||||||
|  |                 Text("Loading audio...") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             player = AVPlayer(url: audioUrl) | ||||||
|  |         } | ||||||
|  |         .onDisappear { | ||||||
|  |             player?.pause() | ||||||
|  |             player = nil | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func togglePlayPause() { | ||||||
|  |         guard let player = player else { return } | ||||||
|  |         if isPlaying { | ||||||
|  |             player.pause() | ||||||
|  |         } else { | ||||||
|  |             player.play() | ||||||
|  |         } | ||||||
|  |         isPlaying.toggle() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										785
									
								
								ios/WatchRunner Watch App/Views/ChatViews.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,785 @@ | |||||||
|  | // | ||||||
|  | //  ChatView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/30. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ChatView: View { | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @State private var selectedTab = 0 | ||||||
|  |     @State private var chatRooms: [SnChatRoom] = [] | ||||||
|  |     @State private var chatInvites: [SnChatMember] = [] | ||||||
|  |     @State private var isLoading = false | ||||||
|  |     @State private var error: Error? | ||||||
|  |     @State private var showingInvites = false | ||||||
|  |  | ||||||
|  |     private let tabs = ["All", "Direct", "Group"] | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         TabView(selection: $selectedTab) { | ||||||
|  |             ForEach(0..<tabs.count, id: \.self) { index in | ||||||
|  |                 VStack { | ||||||
|  |                     if isLoading { | ||||||
|  |                         ProgressView() | ||||||
|  |                     } else if error != nil { | ||||||
|  |                         VStack { | ||||||
|  |                             Text("Error loading chats") | ||||||
|  |                                 .font(.caption) | ||||||
|  |                             Button("Retry") { | ||||||
|  |                                 Task { | ||||||
|  |                                     await loadChatRooms() | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             .font(.caption2) | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         ChatRoomListView( | ||||||
|  |                             chatRooms: filteredChatRooms(for: index), | ||||||
|  |                             selectedTab: index | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .tabItem { | ||||||
|  |                     Text(tabs[index]) | ||||||
|  |                 } | ||||||
|  |                 .tag(index) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .tabViewStyle(.page) | ||||||
|  |         .navigationTitle("Chat") | ||||||
|  |         .toolbar { | ||||||
|  |             ToolbarItem(placement: .topBarTrailing) { | ||||||
|  |                 Button { | ||||||
|  |                     showingInvites = true | ||||||
|  |                 } label: { | ||||||
|  |                     ZStack { | ||||||
|  |                         Image(systemName: "envelope") | ||||||
|  |                         if !chatInvites.isEmpty { | ||||||
|  |                             Circle() | ||||||
|  |                                 .fill(Color.red) | ||||||
|  |                                 .frame(width: 8, height: 8) | ||||||
|  |                                 .offset(x: 8, y: -8) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .sheet(isPresented: $showingInvites) { | ||||||
|  |             ChatInvitesView(invites: $chatInvites, appState: appState) | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             Task.detached { | ||||||
|  |                 await loadChatRooms() | ||||||
|  |                 await loadChatInvites() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func filteredChatRooms(for tabIndex: Int) -> [SnChatRoom] { | ||||||
|  |         switch tabIndex { | ||||||
|  |         case 0: // All | ||||||
|  |             return chatRooms | ||||||
|  |         case 1: // Direct | ||||||
|  |             return chatRooms.filter { $0.type == 1 } | ||||||
|  |         case 2: // Group | ||||||
|  |             return chatRooms.filter { $0.type != 1 } | ||||||
|  |         default: | ||||||
|  |             return chatRooms | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func loadChatRooms() async { | ||||||
|  |         guard let token = appState.token, let serverUrl = appState.serverUrl else { return } | ||||||
|  |  | ||||||
|  |         isLoading = true | ||||||
|  |         error = nil | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let response = try await appState.networkService.fetchChatRooms(token: token, serverUrl: serverUrl) | ||||||
|  |             chatRooms = response.rooms | ||||||
|  |         } catch { | ||||||
|  |             self.error = error | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoading = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func loadChatInvites() async { | ||||||
|  |         guard let token = appState.token, let serverUrl = appState.serverUrl else { return } | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let response = try await appState.networkService.fetchChatInvites(token: token, serverUrl: serverUrl) | ||||||
|  |             chatInvites = response.invites | ||||||
|  |         } catch { | ||||||
|  |             // Handle error silently for invites | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ChatRoomListView: View { | ||||||
|  |     let chatRooms: [SnChatRoom] | ||||||
|  |     let selectedTab: Int | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         if chatRooms.isEmpty { | ||||||
|  |             VStack { | ||||||
|  |                 Image(systemName: "message") | ||||||
|  |                     .font(.largeTitle) | ||||||
|  |                     .foregroundColor(.secondary) | ||||||
|  |                 Text("No chats yet") | ||||||
|  |                     .font(.caption) | ||||||
|  |                     .foregroundColor(.secondary) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             List(chatRooms) { room in | ||||||
|  |                 ChatRoomListItem(room: room) | ||||||
|  |             } | ||||||
|  |             .listStyle(.plain) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ChatRoomListItem: View { | ||||||
|  |     let room: SnChatRoom | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @StateObject private var avatarLoader = ImageLoader() | ||||||
|  |  | ||||||
|  |     private var displayName: String { | ||||||
|  |         if room.type == 1, let members = room.members, !members.isEmpty { | ||||||
|  |             // For direct messages, show the other member's name | ||||||
|  |             return members[0].account.nick | ||||||
|  |         } else { | ||||||
|  |             // For group chats, show room name or fallback | ||||||
|  |             return room.name ?? "Group Chat" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private var subtitle: String { | ||||||
|  |         if room.type == 1, let members = room.members, members.count > 1 { | ||||||
|  |             // For direct messages, show member usernames | ||||||
|  |             return members.map { "@\($0.account.name)" }.joined(separator: ", ") | ||||||
|  |         } else if let description = room.description { | ||||||
|  |             // For group chats with description | ||||||
|  |             return description | ||||||
|  |         } else { | ||||||
|  |             // Fallback | ||||||
|  |             return "" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private var avatarPictureId: String? { | ||||||
|  |         if room.type == 1, let members = room.members, !members.isEmpty { | ||||||
|  |             // For direct messages, use the other member's avatar | ||||||
|  |             return members[0].account.profile.picture?.id | ||||||
|  |         } else { | ||||||
|  |             // For group chats, use room picture | ||||||
|  |             return room.picture?.id | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         NavigationLink( | ||||||
|  |             destination: ChatRoomView(room: room) | ||||||
|  |                 .environmentObject(appState) | ||||||
|  |         ) { | ||||||
|  |             HStack { | ||||||
|  |                 // Avatar using ImageLoader pattern | ||||||
|  |                 Group { | ||||||
|  |                     if avatarLoader.isLoading { | ||||||
|  |                         ProgressView() | ||||||
|  |                             .frame(width: 32, height: 32) | ||||||
|  |                     } else if let image = avatarLoader.image { | ||||||
|  |                         image | ||||||
|  |                             .resizable() | ||||||
|  |                             .frame(width: 32, height: 32) | ||||||
|  |                             .clipShape(Circle()) | ||||||
|  |                     } else if avatarLoader.errorMessage != nil { | ||||||
|  |                         // Error state - show fallback | ||||||
|  |                         Circle() | ||||||
|  |                             .fill(Color.gray.opacity(0.3)) | ||||||
|  |                             .frame(width: 32, height: 32) | ||||||
|  |                             .overlay( | ||||||
|  |                                 Text(displayName.prefix(1).uppercased()) | ||||||
|  |                                     .font(.system(size: 12, weight: .medium)) | ||||||
|  |                                     .foregroundColor(.primary) | ||||||
|  |                             ) | ||||||
|  |                     } else { | ||||||
|  |                         // No image available - show initial | ||||||
|  |                         Circle() | ||||||
|  |                             .fill(Color.gray.opacity(0.3)) | ||||||
|  |                             .frame(width: 32, height: 32) | ||||||
|  |                             .overlay( | ||||||
|  |                                 Text(displayName.prefix(1).uppercased()) | ||||||
|  |                                     .font(.system(size: 12, weight: .medium)) | ||||||
|  |                                     .foregroundColor(.primary) | ||||||
|  |                             ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .task(id: avatarPictureId) { | ||||||
|  |                     if let serverUrl = appState.serverUrl, | ||||||
|  |                        let pictureId = avatarPictureId, | ||||||
|  |                        let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), | ||||||
|  |                        let token = appState.token { | ||||||
|  |                         await avatarLoader.loadImage(from: imageUrl, token: token) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 VStack(alignment: .leading, spacing: 2) { | ||||||
|  |                     Text(displayName) | ||||||
|  |                         .font(.system(size: 14, weight: .medium)) | ||||||
|  |                         .lineLimit(1) | ||||||
|  |  | ||||||
|  |                     if !subtitle.isEmpty { | ||||||
|  |                         Text(subtitle) | ||||||
|  |                             .font(.system(size: 12)) | ||||||
|  |                             .foregroundColor(.secondary) | ||||||
|  |                             .lineLimit(1) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 Spacer() | ||||||
|  |  | ||||||
|  |                 // Unread count badge placeholder | ||||||
|  |                 // In a full implementation, this would show unread count | ||||||
|  |             } | ||||||
|  |             .padding(.vertical, 4) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | import Combine | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ChatRoomView: View { | ||||||
|  |     let room: SnChatRoom | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @State private var messages: [SnChatMessage] = [] | ||||||
|  |     @State private var isLoading = false | ||||||
|  |     @State private var error: Error? | ||||||
|  |     @State private var wsState: WebSocketState = .disconnected // New state for WebSocket status | ||||||
|  |     @State private var hasLoadedMessages = false // Track if messages have been loaded | ||||||
|  |     @State private var messageText = "" // Text input for sending messages | ||||||
|  |     @State private var isSending = false // Track sending state | ||||||
|  |     @State private var isInputHidden = false // Track if input should be hidden during scrolling | ||||||
|  |     @State private var scrollTimer: Timer? // Timer to show input after scrolling stops | ||||||
|  |  | ||||||
|  |     @State private var cancellables = Set<AnyCancellable>() // For managing subscriptions | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         VStack { | ||||||
|  |             // Display WebSocket connection status | ||||||
|  |             if (wsState != .connected) | ||||||
|  |             { | ||||||
|  |                 Text(webSocketStatusMessage) | ||||||
|  |                     .font(.caption2) | ||||||
|  |                     .foregroundColor(.secondary) | ||||||
|  |                     .padding(.vertical, 2) | ||||||
|  |                     .animation(.easeInOut, value: wsState) // Animate status changes | ||||||
|  |                     .transition(.opacity) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if isLoading { | ||||||
|  |                 ProgressView() | ||||||
|  |             } else if error != nil { | ||||||
|  |                 VStack { | ||||||
|  |                     Text("Error loading messages") | ||||||
|  |                         .font(.caption) | ||||||
|  |                     Button("Retry") { | ||||||
|  |                         Task { | ||||||
|  |                             await loadMessages() | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     .font(.caption2) | ||||||
|  |                 } | ||||||
|  |             } else if messages.isEmpty { | ||||||
|  |                 VStack { | ||||||
|  |                     Image(systemName: "bubble.left") | ||||||
|  |                         .font(.largeTitle) | ||||||
|  |                         .foregroundColor(.secondary) | ||||||
|  |                     Text("No messages yet") | ||||||
|  |                         .font(.caption) | ||||||
|  |                         .foregroundColor(.secondary) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 ScrollViewReader { scrollView in | ||||||
|  |                     ScrollView { | ||||||
|  |                         LazyVStack(alignment: .leading, spacing: 8) { | ||||||
|  |                             ForEach(messages) { message in | ||||||
|  |                                 ChatMessageItem(message: message) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         .padding(.horizontal) | ||||||
|  |                         .padding(.vertical, 8) | ||||||
|  |                         .padding(.bottom, 8) | ||||||
|  |                     } | ||||||
|  |                     .onAppear { | ||||||
|  |                         // Scroll to bottom when messages load | ||||||
|  |                         if let lastMessage = messages.last { | ||||||
|  |                             scrollView.scrollTo(lastMessage.id, anchor: .bottom) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     .onChange(of: messages.count) { _, _ in | ||||||
|  |                         // Scroll to bottom when new messages arrive | ||||||
|  |                         if let lastMessage = messages.last { | ||||||
|  |                             withAnimation { | ||||||
|  |                                 scrollView.scrollTo(lastMessage.id, anchor: .bottom) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     .onScrollPhaseChange { _, phase  in | ||||||
|  |                         switch phase { | ||||||
|  |                         case .interacting: | ||||||
|  |                             if !isInputHidden { | ||||||
|  |                                 withAnimation(.easeOut(duration: 0.2)) { | ||||||
|  |                                     isInputHidden = true | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         case .idle: | ||||||
|  |                             withAnimation(.easeIn(duration: 0.3)) { | ||||||
|  |                                 isInputHidden = false | ||||||
|  |                             } | ||||||
|  |                         default: break | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Message input area | ||||||
|  |             if !isInputHidden { | ||||||
|  |                 HStack(spacing: 8) { | ||||||
|  |                     TextField("Send message...", text: $messageText) | ||||||
|  |                         .font(.system(size: 14)) | ||||||
|  |                         .disabled(isSending) | ||||||
|  |                         .frame(height: 40) | ||||||
|  |  | ||||||
|  |                     Button { | ||||||
|  |                         Task { | ||||||
|  |                             await sendMessage() | ||||||
|  |                         } | ||||||
|  |                     } label: { | ||||||
|  |                         if isSending { | ||||||
|  |                             ProgressView() | ||||||
|  |                                 .frame(width: 20, height: 20) | ||||||
|  |                         } else { | ||||||
|  |                             Image(systemName: "arrow.up.circle.fill") | ||||||
|  |                                 .resizable() | ||||||
|  |                                 .frame(width: 20, height: 20) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     .labelStyle(.iconOnly) | ||||||
|  |                     .buttonStyle(.glass) | ||||||
|  |                     .disabled(messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSending) | ||||||
|  |                     .frame(width: 40, height: 40) | ||||||
|  |                 } | ||||||
|  |                 .padding(.horizontal) | ||||||
|  |                 .padding(.top, 8) | ||||||
|  |                 .transition(.move(edge: .bottom).combined(with: .opacity)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .navigationTitle(room.name ?? "Chat") | ||||||
|  |         .task { | ||||||
|  |             await loadMessages() | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             setupWebSocketListeners() | ||||||
|  |         } | ||||||
|  |         .onDisappear { | ||||||
|  |             cancellables.forEach { $0.cancel() } | ||||||
|  |             cancellables.removeAll() | ||||||
|  |             scrollTimer?.invalidate() | ||||||
|  |             scrollTimer = nil | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private var webSocketStatusMessage: String { | ||||||
|  |         switch wsState { | ||||||
|  |         case .connected: return "Connected" | ||||||
|  |         case .connecting: return "Connecting..." | ||||||
|  |         case .disconnected: return "Disconnected" | ||||||
|  |         case .serverDown: return "Server Down" | ||||||
|  |         case .duplicateDevice: return "Duplicate Device" | ||||||
|  |         case .error(let msg): return "Error: \(msg)" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func loadMessages() async { | ||||||
|  |         // Prevent reloading if already loaded | ||||||
|  |         guard !hasLoadedMessages else { return } | ||||||
|  |  | ||||||
|  |         guard let token = appState.token, let serverUrl = appState.serverUrl else { | ||||||
|  |             isLoading = false | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoading = true | ||||||
|  |         error = nil | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let messages = try await appState.networkService.fetchChatMessages( | ||||||
|  |                 chatRoomId: room.id, | ||||||
|  |                 token: token, | ||||||
|  |                 serverUrl: serverUrl | ||||||
|  |             ) | ||||||
|  |             // Sort with newest messages first (for flipped list, newest will appear at bottom) | ||||||
|  |             self.messages = messages.sorted { $0.createdAt < $1.createdAt } | ||||||
|  |             hasLoadedMessages = true | ||||||
|  |         } catch { | ||||||
|  |             print("[watchOS] Error loading messages: \(error.localizedDescription)") | ||||||
|  |             self.error = error | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoading = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func sendMessage() async { | ||||||
|  |         let content = messageText.trimmingCharacters(in: .whitespacesAndNewlines) | ||||||
|  |         guard !content.isEmpty, | ||||||
|  |               let token = appState.token, | ||||||
|  |               let serverUrl = appState.serverUrl else { return } | ||||||
|  |  | ||||||
|  |         isSending = true | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             // Generate a nonce for the message | ||||||
|  |             let nonce = UUID().uuidString | ||||||
|  |  | ||||||
|  |             // Prepare the request data | ||||||
|  |             let messageData: [String: Any] = [ | ||||||
|  |                 "content": content, | ||||||
|  |                 "attachments_id": [], // Empty for now, can be extended for attachments | ||||||
|  |                 "meta": [:], | ||||||
|  |                 "nonce": nonce | ||||||
|  |             ] | ||||||
|  |  | ||||||
|  |             // Create the URL | ||||||
|  |             guard let url = URL(string: "\(serverUrl)/sphere/chat/\(room.id)/messages") else { | ||||||
|  |                 throw URLError(.badURL) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Create the request | ||||||
|  |             var request = URLRequest(url: url) | ||||||
|  |             request.httpMethod = "POST" | ||||||
|  |             request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |             request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||||||
|  |             request.httpBody = try JSONSerialization.data(withJSONObject: messageData, options: []) | ||||||
|  |  | ||||||
|  |             // Send the request | ||||||
|  |             let (data, response) = try await URLSession.shared.data(for: request) | ||||||
|  |  | ||||||
|  |             guard let httpResponse = response as? HTTPURLResponse, | ||||||
|  |                   (200...299).contains(httpResponse.statusCode) else { | ||||||
|  |                 throw URLError(.badServerResponse) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Parse the response to get the sent message | ||||||
|  |             let decoder = JSONDecoder() | ||||||
|  |             decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |             decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |             let sentMessage = try decoder.decode(SnChatMessage.self, from: data) | ||||||
|  |  | ||||||
|  |             // Add the message to the local list | ||||||
|  |             messages.append(sentMessage) | ||||||
|  |  | ||||||
|  |             // Clear the input | ||||||
|  |             messageText = "" | ||||||
|  |  | ||||||
|  |         } catch { | ||||||
|  |             print("[watchOS] Error sending message: \(error.localizedDescription)") | ||||||
|  |             // Could show an error alert here | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isSending = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func sendReadReceipt() { | ||||||
|  |         let data: [String: Any] = ["chat_room_id": room.id] | ||||||
|  |         let packet: [String: Any] = ["type": "messages.read", "data": data, "endpoint": "sphere"] | ||||||
|  |         if let jsonData = try? JSONSerialization.data(withJSONObject: packet, options: []), | ||||||
|  |            let jsonString = String(data: jsonData, encoding: .utf8) { | ||||||
|  |             appState.networkService.sendWebSocketMessage(message: jsonString) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func setupWebSocketListeners() { | ||||||
|  |         // Listen for WebSocket packets (new messages) | ||||||
|  |         appState.networkService.packetStream | ||||||
|  |             .receive(on: DispatchQueue.main) // Ensure UI updates on main thread | ||||||
|  |             .sink(receiveCompletion: { completion in | ||||||
|  |                 if case .failure(let err) = completion { | ||||||
|  |                     print("[ChatRoomView] WebSocket packet stream error: \(err.localizedDescription)") | ||||||
|  |                 } | ||||||
|  |             }, receiveValue: { packet in | ||||||
|  |                 if ["messages.new", "messages.update", "messages.delete"].contains(packet.type), | ||||||
|  |                    let messageData = packet.data { | ||||||
|  |                     do { | ||||||
|  |                         let jsonData = try JSONSerialization.data(withJSONObject: messageData, options: []) | ||||||
|  |                         let decoder = JSONDecoder() | ||||||
|  |                         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |                         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |                         let message = try decoder.decode(SnChatMessage.self, from: jsonData) | ||||||
|  |  | ||||||
|  |                         if message.chatRoomId == room.id { | ||||||
|  |                             switch packet.type { | ||||||
|  |                             case "messages.new": | ||||||
|  |                                 if message.type.hasPrefix("call") { | ||||||
|  |                                     // TODO: Handle ongoing call | ||||||
|  |                                 } | ||||||
|  |                                 if !messages.contains(where: { $0.id == message.id }) { | ||||||
|  |                                     messages.append(message) | ||||||
|  |                                 } | ||||||
|  |                                 sendReadReceipt() | ||||||
|  |                             case "messages.update": | ||||||
|  |                                 if let index = messages.firstIndex(where: { $0.id == message.id }) { | ||||||
|  |                                     messages[index] = message | ||||||
|  |                                 } | ||||||
|  |                             case "messages.delete": | ||||||
|  |                                 messages.removeAll(where: { $0.id == message.id }) | ||||||
|  |                             default: | ||||||
|  |                                 break | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } catch { | ||||||
|  |                         print("[ChatRoomView] Error decoding message from websocket: \(error.localizedDescription)") | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             .store(in: &cancellables) | ||||||
|  |  | ||||||
|  |         // Listen for WebSocket connection state changes | ||||||
|  |         appState.networkService.stateStream | ||||||
|  |             .receive(on: DispatchQueue.main) // Ensure UI updates on main thread | ||||||
|  |             .sink { state in | ||||||
|  |                 wsState = state | ||||||
|  |             } | ||||||
|  |             .store(in: &cancellables) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ChatMessageItem: View { | ||||||
|  |     let message: SnChatMessage | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @StateObject private var avatarLoader = ImageLoader() | ||||||
|  |  | ||||||
|  |     private var avatarPictureId: String? { | ||||||
|  |         message.sender.account.profile.picture?.id | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         HStack(alignment: .top, spacing: 8) { | ||||||
|  |             // Avatar | ||||||
|  |             Group { | ||||||
|  |                 if avatarLoader.isLoading { | ||||||
|  |                     ProgressView() | ||||||
|  |                         .frame(width: 24, height: 24) | ||||||
|  |                 } else if let image = avatarLoader.image { | ||||||
|  |                     image | ||||||
|  |                         .resizable() | ||||||
|  |                         .frame(width: 24, height: 24) | ||||||
|  |                         .clipShape(Circle()) | ||||||
|  |                 } else { | ||||||
|  |                     Circle() | ||||||
|  |                         .fill(Color.gray.opacity(0.3)) | ||||||
|  |                         .frame(width: 24, height: 24) | ||||||
|  |                         .overlay( | ||||||
|  |                             Text(message.sender.account.nick.prefix(1).uppercased()) | ||||||
|  |                                 .font(.system(size: 10, weight: .medium)) | ||||||
|  |                                 .foregroundColor(.primary) | ||||||
|  |                         ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .task(id: avatarPictureId) { | ||||||
|  |                 if let serverUrl = appState.serverUrl, | ||||||
|  |                    let pictureId = avatarPictureId, | ||||||
|  |                    let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), | ||||||
|  |                    let token = appState.token { | ||||||
|  |                     await avatarLoader.loadImage(from: imageUrl, token: token) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             VStack(alignment: .leading, spacing: 4) { | ||||||
|  |                 HStack { | ||||||
|  |                     Text(message.sender.account.nick) | ||||||
|  |                         .font(.system(size: 12, weight: .medium)) | ||||||
|  |                     Spacer() | ||||||
|  |                     Text(message.createdAt, style: .time) | ||||||
|  |                         .font(.system(size: 10)) | ||||||
|  |                         .foregroundColor(.secondary) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if let content = message.content, !content.isEmpty { | ||||||
|  |                     Text(content) | ||||||
|  |                         .font(.system(size: 14)) | ||||||
|  |                         .lineLimit(nil) | ||||||
|  |                         .fixedSize(horizontal: false, vertical: true) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if !message.attachments.isEmpty { | ||||||
|  |                     AttachmentView(attachment: message.attachments[0]) | ||||||
|  |                     if message.attachments.count > 1 { | ||||||
|  |                         HStack(spacing: 8) { | ||||||
|  |                             Image(systemName: "paperclip.circle.fill") | ||||||
|  |                                 .frame(width: 12, height: 12) | ||||||
|  |                                 .foregroundStyle(.gray) | ||||||
|  |                             Text("\(message.attachments.count - 1)+ attachments") | ||||||
|  |                                 .font(.footnote) | ||||||
|  |                                 .foregroundStyle(.gray) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .padding(.vertical, 4) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ChatInvitesView: View { | ||||||
|  |     @Binding var invites: [SnChatMember] | ||||||
|  |     let appState: AppState | ||||||
|  |     @Environment(\.dismiss) private var dismiss | ||||||
|  |     @State private var isLoading = false | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         NavigationView { | ||||||
|  |             VStack { | ||||||
|  |                 if invites.isEmpty { | ||||||
|  |                     VStack { | ||||||
|  |                         Image(systemName: "envelope.open") | ||||||
|  |                             .font(.largeTitle) | ||||||
|  |                             .foregroundColor(.secondary) | ||||||
|  |                         Text("No invites") | ||||||
|  |                             .font(.caption) | ||||||
|  |                             .foregroundColor(.secondary) | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     List(invites) { invite in | ||||||
|  |                         ChatInviteItem(invite: invite, appState: appState, invites: $invites) | ||||||
|  |                     } | ||||||
|  |                     .listStyle(.plain) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .navigationTitle("Invites") | ||||||
|  |             .navigationBarTitleDisplayMode(.inline) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ChatInviteItem: View { | ||||||
|  |     let invite: SnChatMember | ||||||
|  |     let appState: AppState | ||||||
|  |     @Binding var invites: [SnChatMember] | ||||||
|  |     @State private var isAccepting = false | ||||||
|  |     @State private var isDeclining = false | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(alignment: .leading, spacing: 8) { | ||||||
|  |             HStack { | ||||||
|  |                 Circle() | ||||||
|  |                     .fill(Color.gray.opacity(0.3)) | ||||||
|  |                     .frame(width: 24, height: 24) | ||||||
|  |                     .overlay( | ||||||
|  |                         Text((invite.chatRoom?.name ?? "C").prefix(1).uppercased()) | ||||||
|  |                             .font(.system(size: 10, weight: .medium)) | ||||||
|  |                             .foregroundColor(.primary) | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                 VStack(alignment: .leading, spacing: 2) { | ||||||
|  |                     Text(invite.chatRoom?.name ?? "Unknown Chat") | ||||||
|  |                         .font(.system(size: 14, weight: .medium)) | ||||||
|  |                         .lineLimit(1) | ||||||
|  |  | ||||||
|  |                     HStack(spacing: 4) { | ||||||
|  |                         Text(invite.role == 100 ? "Owner" : invite.role >= 50 ? "Moderator" : "Member") | ||||||
|  |                             .font(.system(size: 12)) | ||||||
|  |                             .foregroundColor(.secondary) | ||||||
|  |  | ||||||
|  |                         if invite.chatRoom?.type == 1 { | ||||||
|  |                             Text("Direct") | ||||||
|  |                                 .font(.system(size: 12)) | ||||||
|  |                                 .foregroundColor(.blue) | ||||||
|  |                                 .padding(.horizontal, 4) | ||||||
|  |                                 .padding(.vertical, 2) | ||||||
|  |                                 .background(Color.blue.opacity(0.1)) | ||||||
|  |                                 .cornerRadius(4) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 Spacer() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             HStack(spacing: 8) { | ||||||
|  |                 Button { | ||||||
|  |                     Task { | ||||||
|  |                         await acceptInvite() | ||||||
|  |                     } | ||||||
|  |                 } label: { | ||||||
|  |                     if isAccepting { | ||||||
|  |                         ProgressView() | ||||||
|  |                             .frame(width: 20, height: 20) | ||||||
|  |                     } else { | ||||||
|  |                         Image(systemName: "checkmark") | ||||||
|  |                             .frame(width: 20, height: 20) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .disabled(isAccepting || isDeclining) | ||||||
|  |  | ||||||
|  |                 Button { | ||||||
|  |                     Task { | ||||||
|  |                         await declineInvite() | ||||||
|  |                     } | ||||||
|  |                 } label: { | ||||||
|  |                     if isDeclining { | ||||||
|  |                         ProgressView() | ||||||
|  |                             .frame(width: 20, height: 20) | ||||||
|  |                     } else { | ||||||
|  |                         Image(systemName: "xmark") | ||||||
|  |                             .frame(width: 20, height: 20) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .disabled(isAccepting || isDeclining) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .padding(.vertical, 8) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func acceptInvite() async { | ||||||
|  |         guard let token = appState.token, | ||||||
|  |               let serverUrl = appState.serverUrl, | ||||||
|  |               let chatRoomId = invite.chatRoom?.id else { return } | ||||||
|  |  | ||||||
|  |         isAccepting = true | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             try await appState.networkService.acceptChatInvite(chatRoomId: chatRoomId, token: token, serverUrl: serverUrl) | ||||||
|  |             // Remove from invites list | ||||||
|  |             invites.removeAll { $0.id == invite.id } | ||||||
|  |         } catch { | ||||||
|  |             // Handle error - could show alert | ||||||
|  |             print("Failed to accept invite: \(error)") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isAccepting = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func declineInvite() async { | ||||||
|  |         guard let token = appState.token, | ||||||
|  |               let serverUrl = appState.serverUrl, | ||||||
|  |               let chatRoomId = invite.chatRoom?.id else { return } | ||||||
|  |  | ||||||
|  |         isDeclining = true | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             try await appState.networkService.declineChatInvite(chatRoomId: chatRoomId, token: token, serverUrl: serverUrl) | ||||||
|  |             // Remove from invites list | ||||||
|  |             invites.removeAll { $0.id == invite.id } | ||||||
|  |         } catch { | ||||||
|  |             // Handle error - could show alert | ||||||
|  |             print("Failed to decline invite: \(error)") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isDeclining = false | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								ios/WatchRunner Watch App/Views/ComposePostView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,53 @@ | |||||||
|  | // | ||||||
|  | //  ComposePostView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ComposePostView: View { | ||||||
|  |     @StateObject private var viewModel = ComposePostViewModel() | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @Environment(\.dismiss) private var dismiss | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         NavigationStack { | ||||||
|  |             Form { | ||||||
|  |                 TextField("Title", text: $viewModel.title) | ||||||
|  |                 TextField("Content", text: $viewModel.content) | ||||||
|  |             } | ||||||
|  |             .navigationTitle("New Post") | ||||||
|  |             .toolbar { | ||||||
|  |                 ToolbarItem(placement: .cancellationAction) { | ||||||
|  |                     Button("Cancel", systemImage: "xmark") { | ||||||
|  |                         dismiss() | ||||||
|  |                     } | ||||||
|  |                     .labelStyle(.iconOnly) | ||||||
|  |                 } | ||||||
|  |                 ToolbarItem(placement: .confirmationAction) { | ||||||
|  |                     Button("Post", systemImage: "square.and.arrow.up") { | ||||||
|  |                         Task { | ||||||
|  |                             if let token = appState.token, let serverUrl = appState.serverUrl { | ||||||
|  |                                 await viewModel.createPost(token: token, serverUrl: serverUrl) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     .labelStyle(.iconOnly) | ||||||
|  |                     .disabled(viewModel.isPosting) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .onChange(of: viewModel.didPost) { | ||||||
|  |                 if viewModel.didPost { | ||||||
|  |                     dismiss() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .alert("Error", isPresented: .constant(viewModel.errorMessage != nil), actions: { | ||||||
|  |                 Button("OK") { viewModel.errorMessage = nil } | ||||||
|  |             }, message: { | ||||||
|  |                 Text(viewModel.errorMessage ?? "") | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										110
									
								
								ios/WatchRunner Watch App/Views/DiscoveryViews.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,110 @@ | |||||||
|  | // | ||||||
|  | //  DiscoveryViews.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct DiscoveryView: View { | ||||||
|  |     let discoveryData: DiscoveryData | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         NavigationLink(destination: DiscoveryDetailView(discoveryData: discoveryData)) { | ||||||
|  |             VStack(alignment: .leading) { | ||||||
|  |                 Text("Discovery") | ||||||
|  |                     .font(.headline) | ||||||
|  |                 Text("\(discoveryData.items.count) new items to discover") | ||||||
|  |                     .font(.subheadline) | ||||||
|  |                     .foregroundColor(.secondary) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct DiscoveryDetailView: View { | ||||||
|  |     let discoveryData: DiscoveryData | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         List(discoveryData.items) { item in | ||||||
|  |             NavigationLink(destination: destinationView(for: item)) { | ||||||
|  |                 itemView(for: item) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Discovery") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @ViewBuilder | ||||||
|  |     private func itemView(for item: DiscoveryItem) -> some View { | ||||||
|  |         VStack(alignment: .leading) { | ||||||
|  |             switch item.data { | ||||||
|  |             case .realm(let realm): | ||||||
|  |                 Text("Realm").font(.headline) | ||||||
|  |                 Text(realm.name).foregroundColor(.secondary) | ||||||
|  |             case .publisher(let publisher): | ||||||
|  |                 Text("Publisher").font(.headline) | ||||||
|  |                 Text(publisher.name).foregroundColor(.secondary) | ||||||
|  |             case .article(let article): | ||||||
|  |                 Text("Article").font(.headline) | ||||||
|  |                 Text(article.title).foregroundColor(.secondary) | ||||||
|  |             case .unknown: | ||||||
|  |                 Text("Unknown item") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     @ViewBuilder | ||||||
|  |     private func destinationView(for item: DiscoveryItem) -> some View { | ||||||
|  |         switch item.data { | ||||||
|  |         case .realm(let realm): | ||||||
|  |             RealmDetailView(realm: realm) | ||||||
|  |         case .publisher(let publisher): | ||||||
|  |             PublisherDetailView(publisher: publisher) | ||||||
|  |         case .article(let article): | ||||||
|  |             ArticleDetailView(article: article) | ||||||
|  |         case .unknown: | ||||||
|  |             Text("Detail view not available") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct RealmDetailView: View { | ||||||
|  |     let realm: SnRealm | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(alignment: .leading, spacing: 8) { | ||||||
|  |             Text(realm.name).font(.headline) | ||||||
|  |             if let description = realm.description { | ||||||
|  |                 Text(description).font(.body) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Realm") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct PublisherDetailView: View { | ||||||
|  |     let publisher: SnPublisher | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(alignment: .leading, spacing: 8) { | ||||||
|  |             Text(publisher.name).font(.headline) | ||||||
|  |             if let description = publisher.description { | ||||||
|  |                 Text(description).font(.body) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Publisher") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ArticleDetailView: View { | ||||||
|  |     let article: SnWebArticle | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(alignment: .leading, spacing: 8) { | ||||||
|  |             Text(article.title).font(.headline) | ||||||
|  |             Text(article.url).font(.caption).foregroundColor(.secondary) | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Article") | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								ios/WatchRunner Watch App/Views/ExploreView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,59 @@ | |||||||
|  | // | ||||||
|  | //  ExploreView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | // The main view with the TabView for filtering. | ||||||
|  | struct ExploreView: View { | ||||||
|  |     @StateObject private var appState = AppState() | ||||||
|  |     @State private var isComposing = false | ||||||
|  |     @State private var selectedTab: String = "Explore" | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         NavigationStack { | ||||||
|  |             if appState.isReady { | ||||||
|  |                 TabView(selection: $selectedTab) { | ||||||
|  |                     ActivityListView(filter: "Explore") | ||||||
|  |                         .tag("Explore") | ||||||
|  |                         .tabItem { | ||||||
|  |                             Label("Explore", systemImage: "safari") | ||||||
|  |                         } | ||||||
|  |                         .labelStyle(.titleOnly) | ||||||
|  |  | ||||||
|  |                     ActivityListView(filter: "Subscriptions") | ||||||
|  |                         .tag("Subscriptions") | ||||||
|  |                         .tabItem { | ||||||
|  |                             Label("Subscriptions", systemImage: "star") | ||||||
|  |                         } | ||||||
|  |                         .labelStyle(.titleOnly) | ||||||
|  |  | ||||||
|  |                     ActivityListView(filter: "Friends") | ||||||
|  |                         .tag("Friends") | ||||||
|  |                         .tabItem { | ||||||
|  |                             Label("Friends", systemImage: "person.2") | ||||||
|  |                         } | ||||||
|  |                         .labelStyle(.titleOnly) | ||||||
|  |                 } | ||||||
|  |                 .navigationTitle(selectedTab) | ||||||
|  |                 .toolbar { | ||||||
|  |                     ToolbarItem(placement: .primaryAction) { | ||||||
|  |                         Button(action: { isComposing = true }) { | ||||||
|  |                             Label("Compose", systemImage: "plus") | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .environmentObject(appState) | ||||||
|  |             } else { | ||||||
|  |                 ProgressView { Text("Connecting to phone...") } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .sheet(isPresented: $isComposing) { | ||||||
|  |             ComposePostView() | ||||||
|  |                 .environmentObject(appState) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								ios/WatchRunner Watch App/Views/ImageViewer.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ImageViewer: View { | ||||||
|  |     let imageUrl: URL | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @StateObject private var imageLoader = ImageLoader() | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         Group { | ||||||
|  |             if imageLoader.isLoading { | ||||||
|  |                 ProgressView() | ||||||
|  |             } else if let image = imageLoader.image { | ||||||
|  |                 image | ||||||
|  |                     .resizable() | ||||||
|  |                     .aspectRatio(contentMode: .fit) | ||||||
|  |                     .frame(maxWidth: .infinity, maxHeight: .infinity) | ||||||
|  |                     .scaledToFit() | ||||||
|  |             } else if let errorMessage = imageLoader.errorMessage { | ||||||
|  |                 Text("Failed to load image: \(errorMessage)") | ||||||
|  |                     .font(.caption) | ||||||
|  |                     .foregroundColor(.red) | ||||||
|  |             } else { | ||||||
|  |                 Text("Failed to load image.") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .task(id: imageUrl) { | ||||||
|  |             if let token = appState.token { | ||||||
|  |                 await imageLoader.loadImage(from: imageUrl, token: token) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Image") | ||||||
|  |         .navigationBarTitleDisplayMode(.inline) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										198
									
								
								ios/WatchRunner Watch App/Views/NotificationView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,198 @@ | |||||||
|  |  | ||||||
|  | // | ||||||
|  | //  NotificationView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  | import Combine | ||||||
|  |  | ||||||
|  | @MainActor | ||||||
|  | class NotificationViewModel: ObservableObject { | ||||||
|  |     @Published var notifications = [SnNotification]() | ||||||
|  |     @Published var isLoading = false | ||||||
|  |     @Published var isLoadingMore = false | ||||||
|  |     @Published var errorMessage: String? | ||||||
|  |     @Published var hasMore = false | ||||||
|  |  | ||||||
|  |     private let networkService = NetworkService() | ||||||
|  |     private var hasFetched = false | ||||||
|  |     private var offset = 0 | ||||||
|  |     private let pageSize = 20 | ||||||
|  |  | ||||||
|  |     func fetchNotifications(token: String, serverUrl: String) async { | ||||||
|  |         if hasFetched { return } | ||||||
|  |         guard !isLoading else { return } | ||||||
|  |         isLoading = true | ||||||
|  |         errorMessage = nil | ||||||
|  |         hasFetched = true | ||||||
|  |         offset = 0 | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let response = try await networkService.fetchNotifications(offset: offset, take: pageSize, token: token, serverUrl: serverUrl) | ||||||
|  |             self.notifications = response.notifications | ||||||
|  |             self.hasMore = response.hasMore | ||||||
|  |             offset += response.notifications.count | ||||||
|  |         } catch { | ||||||
|  |             self.errorMessage = error.localizedDescription | ||||||
|  |             print("[watchOS] fetchNotifications failed with error: \(error)") | ||||||
|  |             hasFetched = false | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoading = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func loadMoreNotifications(token: String, serverUrl: String) async { | ||||||
|  |         guard !isLoadingMore && hasMore else { return } | ||||||
|  |         isLoadingMore = true | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let response = try await networkService.fetchNotifications(offset: offset, take: pageSize, token: token, serverUrl: serverUrl) | ||||||
|  |             self.notifications.append(contentsOf: response.notifications) | ||||||
|  |             self.hasMore = response.hasMore | ||||||
|  |             offset += response.notifications.count | ||||||
|  |         } catch { | ||||||
|  |             self.errorMessage = error.localizedDescription | ||||||
|  |             print("[watchOS] loadMoreNotifications failed with error: \(error)") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoadingMore = false | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct NotificationView: View { | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @StateObject private var viewModel = NotificationViewModel() | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         Group { | ||||||
|  |             if viewModel.isLoading { | ||||||
|  |                 ProgressView() | ||||||
|  |             } else if let errorMessage = viewModel.errorMessage { | ||||||
|  |                 VStack { | ||||||
|  |                     Text("Error") | ||||||
|  |                         .font(.headline) | ||||||
|  |                     Text(errorMessage) | ||||||
|  |                         .font(.caption) | ||||||
|  |                     Button("Retry") { | ||||||
|  |                         Task { | ||||||
|  |                             if let token = appState.token, let serverUrl = appState.serverUrl { | ||||||
|  |                                 await viewModel.fetchNotifications(token: token, serverUrl: serverUrl) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .padding() | ||||||
|  |             } else if viewModel.notifications.isEmpty { | ||||||
|  |                 Text("No notifications") | ||||||
|  |             } else { | ||||||
|  |                 List { | ||||||
|  |                     ForEach(viewModel.notifications) { notification in | ||||||
|  |                         NavigationLink(destination: NotificationDetailView(notification: notification)) { | ||||||
|  |                             VStack(alignment: .leading, spacing: 4) { | ||||||
|  |                                 HStack { | ||||||
|  |                                     Text(notification.title) | ||||||
|  |                                         .font(.headline) | ||||||
|  |                                     Spacer() | ||||||
|  |                                     if notification.viewedAt == nil { | ||||||
|  |                                         Circle() | ||||||
|  |                                             .fill(Color.blue) | ||||||
|  |                                             .frame(width: 8, height: 8) | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                                 if !notification.subtitle.isEmpty { | ||||||
|  |                                     Text(notification.subtitle) | ||||||
|  |                                         .font(.subheadline) | ||||||
|  |                                         .foregroundColor(.secondary) | ||||||
|  |                                 } | ||||||
|  |                                 if notification.content.count > 100 { | ||||||
|  |                                     Text(notification.content.prefix(100) + "...") | ||||||
|  |                                         .font(.caption) | ||||||
|  |                                         .foregroundColor(.gray) | ||||||
|  |                                         .lineLimit(2) | ||||||
|  |                                 } else { | ||||||
|  |                                     Text(notification.content) | ||||||
|  |                                         .font(.caption) | ||||||
|  |                                         .foregroundColor(.gray) | ||||||
|  |                                         .lineLimit(2) | ||||||
|  |                                 } | ||||||
|  |                                 Text(notification.createdAt, style: .relative) | ||||||
|  |                                     .font(.caption2) | ||||||
|  |                                     .foregroundColor(.gray) | ||||||
|  |                             } | ||||||
|  |                             .padding(.vertical, 8) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     if viewModel.hasMore { | ||||||
|  |                         if viewModel.isLoadingMore { | ||||||
|  |                             HStack { | ||||||
|  |                                 Spacer() | ||||||
|  |                                 ProgressView() | ||||||
|  |                                 Spacer() | ||||||
|  |                             } | ||||||
|  |                         } else { | ||||||
|  |                             Button("Load More") { | ||||||
|  |                                 Task { | ||||||
|  |                                     if let token = appState.token, let serverUrl = appState.serverUrl { | ||||||
|  |                                         await viewModel.loadMoreNotifications(token: token, serverUrl: serverUrl) | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             .frame(maxWidth: .infinity) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             if appState.isReady, let token = appState.token, let serverUrl = appState.serverUrl { | ||||||
|  |                 Task.detached { | ||||||
|  |                     await viewModel.fetchNotifications(token: token, serverUrl: serverUrl) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Notifications") | ||||||
|  |         .navigationBarTitleDisplayMode(.inline) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct NotificationDetailView: View { | ||||||
|  |     let notification: SnNotification | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         ScrollView { | ||||||
|  |             VStack(alignment: .leading, spacing: 16) { | ||||||
|  |                 Text(notification.title) | ||||||
|  |                     .font(.headline) | ||||||
|  |                  | ||||||
|  |                 if !notification.subtitle.isEmpty { | ||||||
|  |                     Text(notification.subtitle) | ||||||
|  |                         .font(.subheadline) | ||||||
|  |                         .foregroundColor(.secondary) | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 Text(notification.content) | ||||||
|  |                     .font(.body) | ||||||
|  |                  | ||||||
|  |                 HStack { | ||||||
|  |                     Text(notification.createdAt, style: .date) | ||||||
|  |                     Text("·") | ||||||
|  |                     Text(notification.createdAt, style: .time) | ||||||
|  |                 } | ||||||
|  |                 .font(.caption) | ||||||
|  |                 .foregroundColor(.gray) | ||||||
|  |                  | ||||||
|  |                 if notification.viewedAt == nil { | ||||||
|  |                     Text("Unread") | ||||||
|  |                         .font(.caption) | ||||||
|  |                         .foregroundColor(.blue) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .padding() | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Notification") | ||||||
|  |         .navigationBarTitleDisplayMode(.inline) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										151
									
								
								ios/WatchRunner Watch App/Views/PostViews.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,151 @@ | |||||||
|  | // | ||||||
|  | //  PostViews.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/29. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct PostRowView: View { | ||||||
|  |     let post: SnPost | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @StateObject private var imageLoader = ImageLoader() // Instantiate ImageLoader | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(alignment: .leading, spacing: 4) { | ||||||
|  |             HStack { | ||||||
|  |                 if imageLoader.isLoading { | ||||||
|  |                     ProgressView() | ||||||
|  |                         .frame(width: 24, height: 24) | ||||||
|  |                 } else if let image = imageLoader.image { | ||||||
|  |                     image | ||||||
|  |                         .resizable() | ||||||
|  |                         .frame(width: 24, height: 24) | ||||||
|  |                         .clipShape(Circle()) | ||||||
|  |                 } else if let errorMessage = imageLoader.errorMessage { | ||||||
|  |                     Text("Failed: \(errorMessage)") | ||||||
|  |                         .font(.caption) | ||||||
|  |                         .foregroundColor(.red) | ||||||
|  |                         .frame(width: 24, height: 24) | ||||||
|  |                 } else { | ||||||
|  |                     // Placeholder if no image and not loading | ||||||
|  |                     Image(systemName: "person.circle.fill") | ||||||
|  |                         .resizable() | ||||||
|  |                         .frame(width: 24, height: 24) | ||||||
|  |                         .clipShape(Circle()) | ||||||
|  |                         .foregroundColor(.gray) | ||||||
|  |                 } | ||||||
|  |                 Text(post.publisher.nick ?? post.publisher.name) | ||||||
|  |                     .font(.subheadline) | ||||||
|  |                     .bold() | ||||||
|  |             } | ||||||
|  |             .task(id: post.publisher.picture?.id) { // Use task(id:) to reload image when pictureId changes | ||||||
|  |                 if let serverUrl = appState.serverUrl, let pictureId = post.publisher.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token { | ||||||
|  |                     await imageLoader.loadImage(from: imageUrl, token: token) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if let title = post.title, !title.isEmpty { | ||||||
|  |                 Text(title) | ||||||
|  |                     .font(.headline) | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if let content = post.content, !content.isEmpty { | ||||||
|  |                 Text(content) | ||||||
|  |                     .font(.body) | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if !post.attachments.isEmpty { | ||||||
|  |                 AttachmentView(attachment: post.attachments[0]) | ||||||
|  |                 if post.attachments.count > 1 { | ||||||
|  |                     HStack(spacing: 8) { | ||||||
|  |                         Image(systemName: "paperclip.circle.fill") | ||||||
|  |                             .frame(width: 12, height: 12) | ||||||
|  |                             .foregroundStyle(.gray) | ||||||
|  |                         Text("\(post.attachments.count - 1)+ attachments") | ||||||
|  |                             .font(.footnote) | ||||||
|  |                             .foregroundStyle(.gray) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }.padding(.vertical) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct PostDetailView: View { | ||||||
|  |     let post: SnPost | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @StateObject private var publisherImageLoader = ImageLoader() // Instantiate ImageLoader for publisher avatar | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         ScrollView { | ||||||
|  |             VStack(alignment: .leading, spacing: 8) { | ||||||
|  |                 HStack { | ||||||
|  |                     if publisherImageLoader.isLoading { | ||||||
|  |                         ProgressView() | ||||||
|  |                             .frame(width: 32, height: 32) | ||||||
|  |                     } else if let image = publisherImageLoader.image { | ||||||
|  |                         image | ||||||
|  |                             .resizable() | ||||||
|  |                             .frame(width: 32, height: 32) | ||||||
|  |                             .clipShape(Circle()) | ||||||
|  |                     } else if let errorMessage = publisherImageLoader.errorMessage { | ||||||
|  |                         Text("Failed: \(errorMessage)") | ||||||
|  |                             .font(.caption) | ||||||
|  |                             .foregroundColor(.red) | ||||||
|  |                             .frame(width: 32, height: 32) | ||||||
|  |                     } else { | ||||||
|  |                         Image(systemName: "person.circle.fill") | ||||||
|  |                             .resizable() | ||||||
|  |                             .frame(width: 32, height: 32) | ||||||
|  |                             .clipShape(Circle()) | ||||||
|  |                             .foregroundColor(.gray) | ||||||
|  |                     } | ||||||
|  |                     Text("@\(post.publisher.name)") | ||||||
|  |                         .font(.headline) | ||||||
|  |                 } | ||||||
|  |                 // Use task(id:) to reload image when pictureId changes | ||||||
|  |                 .task(id: post.publisher.picture?.id) { | ||||||
|  |                     if let serverUrl = appState.serverUrl, let pictureId = post.publisher.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token { | ||||||
|  |                         await publisherImageLoader.loadImage(from: imageUrl, token: token) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 if let title = post.title, !title.isEmpty { | ||||||
|  |                     Text(title) | ||||||
|  |                         .font(.title2) | ||||||
|  |                         .bold() | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 if let content = post.content, !content.isEmpty { | ||||||
|  |                     Text(content) | ||||||
|  |                         .font(.body) | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 if !post.attachments.isEmpty { | ||||||
|  |                     Text("Attachments").font(.headline) | ||||||
|  |                     ForEach(post.attachments) { attachment in | ||||||
|  |                         AttachmentView(attachment: attachment) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 if !post.tags.isEmpty { | ||||||
|  |                     Text("Tags").font(.headline) | ||||||
|  |                     FlowLayout(alignment: .leading, spacing: 4) { | ||||||
|  |                         ForEach(post.tags) { tag in | ||||||
|  |                             Text("#\(tag.name ?? tag.slug)") | ||||||
|  |                                 .font(.caption) | ||||||
|  |                                 .padding(.horizontal, 8) | ||||||
|  |                                 .padding(.vertical, 3) | ||||||
|  |                                 .background(Capsule().fill(Color.accentColor.opacity(0.2))) | ||||||
|  |                                 .cornerRadius(5) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .padding() | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Post") | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								ios/WatchRunner Watch App/Views/StatusCreationView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,132 @@ | |||||||
|  | // | ||||||
|  | //  StatusCreationView.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/30. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct StatusCreationView: View { | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @Environment(\.dismiss) var dismiss | ||||||
|  |      | ||||||
|  |     let initialStatus: SnAccountStatus? | ||||||
|  |      | ||||||
|  |     @State private var attitude: Int | ||||||
|  |     @State private var isInvisible: Bool | ||||||
|  |     @State private var isNotDisturb: Bool | ||||||
|  |     @State private var label: String | ||||||
|  |     @State private var isSubmitting: Bool = false | ||||||
|  |     @State private var error: Error? = nil | ||||||
|  |      | ||||||
|  |     private let networkService = NetworkService() | ||||||
|  |      | ||||||
|  |     init(initialStatus: SnAccountStatus? = nil) { | ||||||
|  |         self.initialStatus = initialStatus | ||||||
|  |         _attitude = State(initialValue: initialStatus?.attitude ?? 1) | ||||||
|  |         _isInvisible = State(initialValue: initialStatus?.isInvisible ?? false) | ||||||
|  |         _isNotDisturb = State(initialValue: initialStatus?.isNotDisturb ?? false) | ||||||
|  |         _label = State(initialValue: initialStatus?.label ?? "") | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         ScrollView { | ||||||
|  |             VStack(spacing: 16) { | ||||||
|  |                 // Title | ||||||
|  |                 Text("Set Status") | ||||||
|  |                     .font(.headline) | ||||||
|  |                     .padding(.top) | ||||||
|  |                  | ||||||
|  |                 // Label TextField | ||||||
|  |                 TextField("Status label", text: $label) | ||||||
|  |                     .textFieldStyle(.automatic) | ||||||
|  |                     .padding(.horizontal) | ||||||
|  |                  | ||||||
|  |                 // Attitude Picker | ||||||
|  |                 VStack(alignment: .leading, spacing: 8) { | ||||||
|  |                     Text("Mood") | ||||||
|  |                         .font(.subheadline) | ||||||
|  |                         .foregroundColor(.secondary) | ||||||
|  |                      | ||||||
|  |                     Picker("Attitude", selection: $attitude) { | ||||||
|  |                         Text("😊 Positive").tag(0) | ||||||
|  |                         Text("😐 Neutral").tag(1) | ||||||
|  |                         Text("😢 Negative").tag(2) | ||||||
|  |                     } | ||||||
|  |                     .pickerStyle(.wheel) | ||||||
|  |                     .frame(height: 80) | ||||||
|  |                 } | ||||||
|  |                 .padding(.horizontal) | ||||||
|  |                  | ||||||
|  |                 // Toggles | ||||||
|  |                 VStack(spacing: 12) { | ||||||
|  |                     Toggle("Invisible", isOn: $isInvisible) | ||||||
|  |                         .padding(.horizontal) | ||||||
|  |                      | ||||||
|  |                     Toggle("Do Not Disturb", isOn: $isNotDisturb) | ||||||
|  |                         .padding(.horizontal) | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 // Error message | ||||||
|  |                 if let error = error { | ||||||
|  |                     Text("Error: \(error.localizedDescription)") | ||||||
|  |                         .foregroundColor(.red) | ||||||
|  |                         .font(.caption) | ||||||
|  |                         .padding(.horizontal) | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 // Buttons | ||||||
|  |                 HStack(spacing: 12) { | ||||||
|  |                     Button("Cancel") { | ||||||
|  |                         dismiss() | ||||||
|  |                     } | ||||||
|  |                     .buttonStyle(.glass) | ||||||
|  |                      | ||||||
|  |                     Button(isSubmitting ? "Saving..." : "Save") { | ||||||
|  |                         Task { | ||||||
|  |                             await submitStatus() | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     .buttonStyle(.glassProminent) | ||||||
|  |                     .disabled(isSubmitting) | ||||||
|  |                 } | ||||||
|  |                 .padding(.horizontal) | ||||||
|  |                 .padding(.bottom) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Status") | ||||||
|  |         .navigationBarTitleDisplayMode(.inline) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private func submitStatus() async { | ||||||
|  |         guard let token = appState.token, let serverUrl = appState.serverUrl else { | ||||||
|  |             error = NSError(domain: "StatusCreationView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"]) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         isSubmitting = true | ||||||
|  |         error = nil | ||||||
|  |          | ||||||
|  |         do { | ||||||
|  |             _ = try await networkService.createOrUpdateStatus( | ||||||
|  |                 attitude: attitude, | ||||||
|  |                 isInvisible: isInvisible, | ||||||
|  |                 isNotDisturb: isNotDisturb, | ||||||
|  |                 label: label.isEmpty ? nil : label, | ||||||
|  |                 token: token, | ||||||
|  |                 serverUrl: serverUrl | ||||||
|  |             ) | ||||||
|  |             dismiss() | ||||||
|  |         } catch { | ||||||
|  |             self.error = error | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         isSubmitting = false | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     StatusCreationView() | ||||||
|  |         .environmentObject(AppState()) | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								ios/WatchRunner Watch App/Views/VideoPlayerView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | |||||||
|  | import SwiftUI | ||||||
|  | import AVKit | ||||||
|  | import AVFoundation | ||||||
|  |  | ||||||
|  | struct VideoPlayerView: View { | ||||||
|  |     let videoUrl: URL | ||||||
|  |  | ||||||
|  |     var body: some View { | ||||||
|  |         VideoPlayer(player: AVPlayer(url: videoUrl)) | ||||||
|  |             .edgesIgnoringSafeArea(.all) // Make it full screen | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								ios/WatchRunner Watch App/WatchRunnerApp.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | // | ||||||
|  | //  WatchRunnerApp.swift | ||||||
|  | //  WatchRunner Watch App | ||||||
|  | // | ||||||
|  | //  Created by LittleSheep on 2025/10/28. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | @main | ||||||
|  | struct WatchRunner_Watch_AppApp: App { | ||||||
|  |     var body: some Scene { | ||||||
|  |         WindowGroup { | ||||||
|  |             ContentView() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								ios/WatchRunner-Watch-App-Info.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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>UIBackgroundModes</key> | ||||||
|  | 	<array> | ||||||
|  | 		<string>remote-notification</string> | ||||||
|  | 	</array> | ||||||
|  | </dict> | ||||||
|  | </plist> | ||||||
| @@ -130,7 +130,6 @@ void main() async { | |||||||
|           debugPrint('[Wayland] setAsFrameless failed: $e'); |           debugPrint('[Wayland] setAsFrameless failed: $e'); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       await windowManager.setAsFrameless(); |  | ||||||
|       await windowManager.setMinimumSize(defaultSize); |       await windowManager.setMinimumSize(defaultSize); | ||||||
|       await windowManager.show(); |       await windowManager.show(); | ||||||
|       await windowManager.focus(); |       await windowManager.focus(); | ||||||
|   | |||||||
| @@ -47,9 +47,12 @@ sealed class SnPost with _$SnPost { | |||||||
|     @Default([]) List<SnPostTag> tags, |     @Default([]) List<SnPostTag> tags, | ||||||
|     @Default([]) List<SnPostCategory> categories, |     @Default([]) List<SnPostCategory> categories, | ||||||
|     @Default([]) List<dynamic> collections, |     @Default([]) List<dynamic> collections, | ||||||
|  |     @Default([]) List<SnPostFeaturedRecord> featuredRecords, | ||||||
|     @Default(null) DateTime? createdAt, |     @Default(null) DateTime? createdAt, | ||||||
|     @Default(null) DateTime? updatedAt, |     @Default(null) DateTime? updatedAt, | ||||||
|     DateTime? deletedAt, |     DateTime? deletedAt, | ||||||
|  |     @Default(false) bool repliedGone, | ||||||
|  |     @Default(false) bool forwardedGone, | ||||||
|     @Default(false) bool isTruncated, |     @Default(false) bool isTruncated, | ||||||
|   }) = _SnPost; |   }) = _SnPost; | ||||||
|  |  | ||||||
| @@ -161,3 +164,19 @@ sealed class SnPostReaction with _$SnPostReaction { | |||||||
|   factory SnPostReaction.fromJson(Map<String, dynamic> json) => |   factory SnPostReaction.fromJson(Map<String, dynamic> json) => | ||||||
|       _$SnPostReactionFromJson(json); |       _$SnPostReactionFromJson(json); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @freezed | ||||||
|  | sealed class SnPostFeaturedRecord with _$SnPostFeaturedRecord { | ||||||
|  |   const factory SnPostFeaturedRecord({ | ||||||
|  |     required String id, | ||||||
|  |     required String postId, | ||||||
|  |     required DateTime? featuredAt, | ||||||
|  |     required int socialCredits, | ||||||
|  |     required DateTime createdAt, | ||||||
|  |     required DateTime updatedAt, | ||||||
|  |     required DateTime? deletedAt, | ||||||
|  |   }) = _SnPostFeaturedRecord; | ||||||
|  |  | ||||||
|  |   factory SnPostFeaturedRecord.fromJson(Map<String, dynamic> json) => | ||||||
|  |       _$SnPostFeaturedRecordFromJson(json); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ T _$identity<T>(T value) => value; | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnPost { | 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; String? get slug; int get type; Map<String, dynamic>? get meta; SnPostEmbedView? get embedView; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; int get awardedScore; int? get pinMode; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> 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; String? get slug; int get type; Map<String, dynamic>? get meta; SnPostEmbedView? get embedView; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; int get awardedScore; int? get pinMode; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; List<SnPostFeaturedRecord> get featuredRecords; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get repliedGone; bool get forwardedGone; bool get isTruncated; | ||||||
| /// Create a copy of SnPost | /// Create a copy of SnPost | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -28,16 +28,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | 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.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.embedView, embedView) || other.embedView == embedView)&&(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.awardedScore, awardedScore) || other.awardedScore == awardedScore)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(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)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&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)); |   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.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.embedView, embedView) || other.embedView == embedView)&&(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.awardedScore, awardedScore) || other.awardedScore == awardedScore)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(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)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&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)&&const DeepCollectionEquality().equals(other.featuredRecords, featuredRecords)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.repliedGone, repliedGone) || other.repliedGone == repliedGone)&&(identical(other.forwardedGone, forwardedGone) || other.forwardedGone == forwardedGone)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),embedView,viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,awardedScore,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,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]); | int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),embedView,viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,awardedScore,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,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),const DeepCollectionEquality().hash(featuredRecords),createdAt,updatedAt,deletedAt,repliedGone,forwardedGone,isTruncated]); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, embedView: $embedView, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, awardedScore: $awardedScore, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, 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, slug: $slug, type: $type, meta: $meta, embedView: $embedView, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, awardedScore: $awardedScore, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, featuredRecords: $featuredRecords, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, repliedGone: $repliedGone, forwardedGone: $forwardedGone, isTruncated: $isTruncated)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -48,7 +48,7 @@ abstract mixin class $SnPostCopyWith<$Res>  { | |||||||
|   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; |   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, SnPostEmbedView? embedView, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int awardedScore, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> 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, String? slug, int type, Map<String, dynamic>? meta, SnPostEmbedView? embedView, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int awardedScore, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, List<SnPostFeaturedRecord> featuredRecords, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool repliedGone, bool forwardedGone, bool isTruncated | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -65,7 +65,7 @@ class _$SnPostCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnPost | /// Create a copy of SnPost | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? embedView = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? awardedScore = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = 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,}) { | @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? slug = freezed,Object? type = null,Object? meta = freezed,Object? embedView = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? awardedScore = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = 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? featuredRecords = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? repliedGone = null,Object? forwardedGone = null,Object? isTruncated = null,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -102,10 +102,13 @@ as Map<String, bool>,reactions: null == reactions ? _self.reactions : reactions | |||||||
| as List<dynamic>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable | as List<dynamic>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable | ||||||
| as List<SnPostTag>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable | as List<SnPostTag>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable | ||||||
| as List<SnPostCategory>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable | as List<SnPostCategory>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable | ||||||
| as List<dynamic>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as List<dynamic>,featuredRecords: null == featuredRecords ? _self.featuredRecords : featuredRecords // ignore: cast_nullable_to_non_nullable | ||||||
|  | as List<SnPostFeaturedRecord>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,isTruncated: null == isTruncated ? _self.isTruncated : isTruncated // ignore: cast_nullable_to_non_nullable | as DateTime?,repliedGone: null == repliedGone ? _self.repliedGone : repliedGone // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool,forwardedGone: null == forwardedGone ? _self.forwardedGone : forwardedGone // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool,isTruncated: null == isTruncated ? _self.isTruncated : isTruncated // ignore: cast_nullable_to_non_nullable | ||||||
| as bool, | as bool, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
| @@ -257,10 +260,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,  String? slug,  int type,  Map<String, dynamic>? meta,  SnPostEmbedView? embedView,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  int awardedScore,  int? pinMode,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> 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,  String? slug,  int type,  Map<String, dynamic>? meta,  SnPostEmbedView? embedView,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  int awardedScore,  int? pinMode,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> categories,  List<dynamic> collections,  List<SnPostFeaturedRecord> featuredRecords,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool repliedGone,  bool forwardedGone,  bool isTruncated)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnPost() when $default != null: | case _SnPost() when $default != null: | ||||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.embedView,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.awardedScore,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_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 $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.embedView,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.awardedScore,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.featuredRecords,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.repliedGone,_that.forwardedGone,_that.isTruncated);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -278,10 +281,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,  String? slug,  int type,  Map<String, dynamic>? meta,  SnPostEmbedView? embedView,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  int awardedScore,  int? pinMode,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> 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,  String? slug,  int type,  Map<String, dynamic>? meta,  SnPostEmbedView? embedView,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  int awardedScore,  int? pinMode,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> categories,  List<dynamic> collections,  List<SnPostFeaturedRecord> featuredRecords,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool repliedGone,  bool forwardedGone,  bool isTruncated)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnPost(): | case _SnPost(): | ||||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.embedView,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.awardedScore,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_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.slug,_that.type,_that.meta,_that.embedView,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.awardedScore,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.featuredRecords,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.repliedGone,_that.forwardedGone,_that.isTruncated);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -295,10 +298,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,  String? slug,  int type,  Map<String, dynamic>? meta,  SnPostEmbedView? embedView,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  int awardedScore,  int? pinMode,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> 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,  String? slug,  int type,  Map<String, dynamic>? meta,  SnPostEmbedView? embedView,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  int awardedScore,  int? pinMode,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> categories,  List<dynamic> collections,  List<SnPostFeaturedRecord> featuredRecords,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool repliedGone,  bool forwardedGone,  bool isTruncated)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnPost() when $default != null: | case _SnPost() when $default != null: | ||||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.embedView,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.awardedScore,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_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 $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.embedView,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.awardedScore,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.featuredRecords,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.repliedGone,_that.forwardedGone,_that.isTruncated);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -310,7 +313,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnPost implements SnPost { | 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.slug, this.type = 0, final  Map<String, dynamic>? meta, this.embedView, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.awardedScore = 0, this.pinMode, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, 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<SnPostTag> tags = const [], final  List<SnPostCategory> 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; |   const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final  Map<String, dynamic>? meta, this.embedView, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.awardedScore = 0, this.pinMode, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, 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<SnPostTag> tags = const [], final  List<SnPostCategory> categories = const [], final  List<dynamic> collections = const [], final  List<SnPostFeaturedRecord> featuredRecords = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.repliedGone = false, this.forwardedGone = false, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections,_featuredRecords = featuredRecords; | ||||||
|   factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json); |   factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @@ -398,9 +401,18 @@ class _SnPost implements SnPost { | |||||||
|   return EqualUnmodifiableListView(_collections); |   return EqualUnmodifiableListView(_collections); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  final  List<SnPostFeaturedRecord> _featuredRecords; | ||||||
|  | @override@JsonKey() List<SnPostFeaturedRecord> get featuredRecords { | ||||||
|  |   if (_featuredRecords is EqualUnmodifiableListView) return _featuredRecords; | ||||||
|  |   // ignore: implicit_dynamic_type | ||||||
|  |   return EqualUnmodifiableListView(_featuredRecords); | ||||||
|  | } | ||||||
|  |  | ||||||
| @override@JsonKey() final  DateTime? createdAt; | @override@JsonKey() final  DateTime? createdAt; | ||||||
| @override@JsonKey() final  DateTime? updatedAt; | @override@JsonKey() final  DateTime? updatedAt; | ||||||
| @override final  DateTime? deletedAt; | @override final  DateTime? deletedAt; | ||||||
|  | @override@JsonKey() final  bool repliedGone; | ||||||
|  | @override@JsonKey() final  bool forwardedGone; | ||||||
| @override@JsonKey() final  bool isTruncated; | @override@JsonKey() final  bool isTruncated; | ||||||
|  |  | ||||||
| /// Create a copy of SnPost | /// Create a copy of SnPost | ||||||
| @@ -416,16 +428,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | 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.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.embedView, embedView) || other.embedView == embedView)&&(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.awardedScore, awardedScore) || other.awardedScore == awardedScore)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(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)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&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)); |   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.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.embedView, embedView) || other.embedView == embedView)&&(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.awardedScore, awardedScore) || other.awardedScore == awardedScore)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(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)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&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)&&const DeepCollectionEquality().equals(other._featuredRecords, _featuredRecords)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.repliedGone, repliedGone) || other.repliedGone == repliedGone)&&(identical(other.forwardedGone, forwardedGone) || other.forwardedGone == forwardedGone)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),embedView,viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,awardedScore,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,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]); | int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),embedView,viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,awardedScore,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,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),const DeepCollectionEquality().hash(_featuredRecords),createdAt,updatedAt,deletedAt,repliedGone,forwardedGone,isTruncated]); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, embedView: $embedView, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, awardedScore: $awardedScore, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, 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, slug: $slug, type: $type, meta: $meta, embedView: $embedView, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, awardedScore: $awardedScore, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, featuredRecords: $featuredRecords, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, repliedGone: $repliedGone, forwardedGone: $forwardedGone, isTruncated: $isTruncated)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -436,7 +448,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> { | |||||||
|   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; |   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, SnPostEmbedView? embedView, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int awardedScore, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> 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, String? slug, int type, Map<String, dynamic>? meta, SnPostEmbedView? embedView, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int awardedScore, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, List<SnPostFeaturedRecord> featuredRecords, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool repliedGone, bool forwardedGone, bool isTruncated | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -453,7 +465,7 @@ class __$SnPostCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnPost | /// Create a copy of SnPost | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? embedView = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? awardedScore = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = 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,}) { | @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? slug = freezed,Object? type = null,Object? meta = freezed,Object? embedView = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? awardedScore = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = 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? featuredRecords = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? repliedGone = null,Object? forwardedGone = null,Object? isTruncated = null,}) { | ||||||
|   return _then(_SnPost( |   return _then(_SnPost( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -490,10 +502,13 @@ as Map<String, bool>,reactions: null == reactions ? _self._reactions : reactions | |||||||
| as List<dynamic>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable | as List<dynamic>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable | ||||||
| as List<SnPostTag>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable | as List<SnPostTag>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable | ||||||
| as List<SnPostCategory>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable | as List<SnPostCategory>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable | ||||||
| as List<dynamic>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as List<dynamic>,featuredRecords: null == featuredRecords ? _self._featuredRecords : featuredRecords // ignore: cast_nullable_to_non_nullable | ||||||
|  | as List<SnPostFeaturedRecord>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,isTruncated: null == isTruncated ? _self.isTruncated : isTruncated // ignore: cast_nullable_to_non_nullable | as DateTime?,repliedGone: null == repliedGone ? _self.repliedGone : repliedGone // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool,forwardedGone: null == forwardedGone ? _self.forwardedGone : forwardedGone // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool,isTruncated: null == isTruncated ? _self.isTruncated : isTruncated // ignore: cast_nullable_to_non_nullable | ||||||
| as bool, | as bool, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
| @@ -2205,4 +2220,279 @@ $SnAccountCopyWith<$Res>? get account { | |||||||
| } | } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | mixin _$SnPostFeaturedRecord { | ||||||
|  |  | ||||||
|  |  String get id; String get postId; DateTime? get featuredAt; int get socialCredits; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
|  | /// Create a copy of SnPostFeaturedRecord | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $SnPostFeaturedRecordCopyWith<SnPostFeaturedRecord> get copyWith => _$SnPostFeaturedRecordCopyWithImpl<SnPostFeaturedRecord>(this as SnPostFeaturedRecord, _$identity); | ||||||
|  |  | ||||||
|  |   /// Serializes this SnPostFeaturedRecord to a JSON map. | ||||||
|  |   Map<String, dynamic> toJson(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostFeaturedRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.postId, postId) || other.postId == postId)&&(identical(other.featuredAt, featuredAt) || other.featuredAt == featuredAt)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,id,postId,featuredAt,socialCredits,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnPostFeaturedRecord(id: $id, postId: $postId, featuredAt: $featuredAt, socialCredits: $socialCredits, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class $SnPostFeaturedRecordCopyWith<$Res>  { | ||||||
|  |   factory $SnPostFeaturedRecordCopyWith(SnPostFeaturedRecord value, $Res Function(SnPostFeaturedRecord) _then) = _$SnPostFeaturedRecordCopyWithImpl; | ||||||
|  | @useResult | ||||||
|  | $Res call({ | ||||||
|  |  String id, String postId, DateTime? featuredAt, int socialCredits, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class _$SnPostFeaturedRecordCopyWithImpl<$Res> | ||||||
|  |     implements $SnPostFeaturedRecordCopyWith<$Res> { | ||||||
|  |   _$SnPostFeaturedRecordCopyWithImpl(this._self, this._then); | ||||||
|  |  | ||||||
|  |   final SnPostFeaturedRecord _self; | ||||||
|  |   final $Res Function(SnPostFeaturedRecord) _then; | ||||||
|  |  | ||||||
|  | /// Create a copy of SnPostFeaturedRecord | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? postId = null,Object? featuredAt = freezed,Object? socialCredits = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|  |   return _then(_self.copyWith( | ||||||
|  | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,postId: null == postId ? _self.postId : postId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,featuredAt: freezed == featuredAt ? _self.featuredAt : featuredAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?,socialCredits: null == socialCredits ? _self.socialCredits : socialCredits // ignore: cast_nullable_to_non_nullable | ||||||
|  | as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// Adds pattern-matching-related methods to [SnPostFeaturedRecord]. | ||||||
|  | extension SnPostFeaturedRecordPatterns on SnPostFeaturedRecord { | ||||||
|  | /// 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( _SnPostFeaturedRecord value)?  $default,{required TResult orElse(),}){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnPostFeaturedRecord() 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( _SnPostFeaturedRecord value)  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnPostFeaturedRecord(): | ||||||
|  | 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( _SnPostFeaturedRecord value)?  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnPostFeaturedRecord() when $default != null: | ||||||
|  | return $default(_that);case _: | ||||||
|  |   return null; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A variant of `when` that fallback to an `orElse` callback. | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return orElse(); | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String postId,  DateTime? featuredAt,  int socialCredits,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnPostFeaturedRecord() when $default != null: | ||||||
|  | return $default(_that.id,_that.postId,_that.featuredAt,_that.socialCredits,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|  |   return orElse(); | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A `switch`-like method, using callbacks. | ||||||
|  | /// | ||||||
|  | /// As opposed to `map`, this offers destructuring. | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case Subclass2(:final field2): | ||||||
|  | ///     return ...; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String postId,  DateTime? featuredAt,  int socialCredits,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnPostFeaturedRecord(): | ||||||
|  | return $default(_that.id,_that.postId,_that.featuredAt,_that.socialCredits,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
|  | } | ||||||
|  | /// A variant of `when` that fallback to returning `null` | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return null; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String postId,  DateTime? featuredAt,  int socialCredits,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnPostFeaturedRecord() when $default != null: | ||||||
|  | return $default(_that.id,_that.postId,_that.featuredAt,_that.socialCredits,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|  |   return null; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | @JsonSerializable() | ||||||
|  |  | ||||||
|  | class _SnPostFeaturedRecord implements SnPostFeaturedRecord { | ||||||
|  |   const _SnPostFeaturedRecord({required this.id, required this.postId, required this.featuredAt, required this.socialCredits, required this.createdAt, required this.updatedAt, required this.deletedAt}); | ||||||
|  |   factory _SnPostFeaturedRecord.fromJson(Map<String, dynamic> json) => _$SnPostFeaturedRecordFromJson(json); | ||||||
|  |  | ||||||
|  | @override final  String id; | ||||||
|  | @override final  String postId; | ||||||
|  | @override final  DateTime? featuredAt; | ||||||
|  | @override final  int socialCredits; | ||||||
|  | @override final  DateTime createdAt; | ||||||
|  | @override final  DateTime updatedAt; | ||||||
|  | @override final  DateTime? deletedAt; | ||||||
|  |  | ||||||
|  | /// Create a copy of SnPostFeaturedRecord | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | _$SnPostFeaturedRecordCopyWith<_SnPostFeaturedRecord> get copyWith => __$SnPostFeaturedRecordCopyWithImpl<_SnPostFeaturedRecord>(this, _$identity); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | Map<String, dynamic> toJson() { | ||||||
|  |   return _$SnPostFeaturedRecordToJson(this, ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostFeaturedRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.postId, postId) || other.postId == postId)&&(identical(other.featuredAt, featuredAt) || other.featuredAt == featuredAt)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,id,postId,featuredAt,socialCredits,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnPostFeaturedRecord(id: $id, postId: $postId, featuredAt: $featuredAt, socialCredits: $socialCredits, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class _$SnPostFeaturedRecordCopyWith<$Res> implements $SnPostFeaturedRecordCopyWith<$Res> { | ||||||
|  |   factory _$SnPostFeaturedRecordCopyWith(_SnPostFeaturedRecord value, $Res Function(_SnPostFeaturedRecord) _then) = __$SnPostFeaturedRecordCopyWithImpl; | ||||||
|  | @override @useResult | ||||||
|  | $Res call({ | ||||||
|  |  String id, String postId, DateTime? featuredAt, int socialCredits, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class __$SnPostFeaturedRecordCopyWithImpl<$Res> | ||||||
|  |     implements _$SnPostFeaturedRecordCopyWith<$Res> { | ||||||
|  |   __$SnPostFeaturedRecordCopyWithImpl(this._self, this._then); | ||||||
|  |  | ||||||
|  |   final _SnPostFeaturedRecord _self; | ||||||
|  |   final $Res Function(_SnPostFeaturedRecord) _then; | ||||||
|  |  | ||||||
|  | /// Create a copy of SnPostFeaturedRecord | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? postId = null,Object? featuredAt = freezed,Object? socialCredits = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|  |   return _then(_SnPostFeaturedRecord( | ||||||
|  | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,postId: null == postId ? _self.postId : postId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,featuredAt: freezed == featuredAt ? _self.featuredAt : featuredAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?,socialCredits: null == socialCredits ? _self.socialCredits : socialCredits // ignore: cast_nullable_to_non_nullable | ||||||
|  | as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime?, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| // dart format on | // dart format on | ||||||
|   | |||||||
| @@ -85,6 +85,11 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost( | |||||||
|           .toList() ?? |           .toList() ?? | ||||||
|       const [], |       const [], | ||||||
|   collections: json['collections'] as List<dynamic>? ?? const [], |   collections: json['collections'] as List<dynamic>? ?? const [], | ||||||
|  |   featuredRecords: | ||||||
|  |       (json['featured_records'] as List<dynamic>?) | ||||||
|  |           ?.map((e) => SnPostFeaturedRecord.fromJson(e as Map<String, dynamic>)) | ||||||
|  |           .toList() ?? | ||||||
|  |       const [], | ||||||
|   createdAt: |   createdAt: | ||||||
|       json['created_at'] == null |       json['created_at'] == null | ||||||
|           ? null |           ? null | ||||||
| @@ -97,6 +102,8 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost( | |||||||
|       json['deleted_at'] == null |       json['deleted_at'] == null | ||||||
|           ? null |           ? null | ||||||
|           : DateTime.parse(json['deleted_at'] as String), |           : DateTime.parse(json['deleted_at'] as String), | ||||||
|  |   repliedGone: json['replied_gone'] as bool? ?? false, | ||||||
|  |   forwardedGone: json['forwarded_gone'] as bool? ?? false, | ||||||
|   isTruncated: json['is_truncated'] as bool? ?? false, |   isTruncated: json['is_truncated'] as bool? ?? false, | ||||||
| ); | ); | ||||||
|  |  | ||||||
| @@ -136,9 +143,12 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ | |||||||
|   'tags': instance.tags.map((e) => e.toJson()).toList(), |   'tags': instance.tags.map((e) => e.toJson()).toList(), | ||||||
|   'categories': instance.categories.map((e) => e.toJson()).toList(), |   'categories': instance.categories.map((e) => e.toJson()).toList(), | ||||||
|   'collections': instance.collections, |   'collections': instance.collections, | ||||||
|  |   'featured_records': instance.featuredRecords.map((e) => e.toJson()).toList(), | ||||||
|   'created_at': instance.createdAt?.toIso8601String(), |   'created_at': instance.createdAt?.toIso8601String(), | ||||||
|   'updated_at': instance.updatedAt?.toIso8601String(), |   'updated_at': instance.updatedAt?.toIso8601String(), | ||||||
|   'deleted_at': instance.deletedAt?.toIso8601String(), |   'deleted_at': instance.deletedAt?.toIso8601String(), | ||||||
|  |   'replied_gone': instance.repliedGone, | ||||||
|  |   'forwarded_gone': instance.forwardedGone, | ||||||
|   'is_truncated': instance.isTruncated, |   'is_truncated': instance.isTruncated, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -262,3 +272,33 @@ Map<String, dynamic> _$SnPostReactionToJson(_SnPostReaction instance) => | |||||||
|       'account': instance.account?.toJson(), |       'account': instance.account?.toJson(), | ||||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), |       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  | _SnPostFeaturedRecord _$SnPostFeaturedRecordFromJson( | ||||||
|  |   Map<String, dynamic> json, | ||||||
|  | ) => _SnPostFeaturedRecord( | ||||||
|  |   id: json['id'] as String, | ||||||
|  |   postId: json['post_id'] as String, | ||||||
|  |   featuredAt: | ||||||
|  |       json['featured_at'] == null | ||||||
|  |           ? null | ||||||
|  |           : DateTime.parse(json['featured_at'] as String), | ||||||
|  |   socialCredits: (json['social_credits'] as num).toInt(), | ||||||
|  |   createdAt: DateTime.parse(json['created_at'] as String), | ||||||
|  |   updatedAt: DateTime.parse(json['updated_at'] as String), | ||||||
|  |   deletedAt: | ||||||
|  |       json['deleted_at'] == null | ||||||
|  |           ? null | ||||||
|  |           : DateTime.parse(json['deleted_at'] as String), | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | Map<String, dynamic> _$SnPostFeaturedRecordToJson( | ||||||
|  |   _SnPostFeaturedRecord instance, | ||||||
|  | ) => <String, dynamic>{ | ||||||
|  |   'id': instance.id, | ||||||
|  |   'post_id': instance.postId, | ||||||
|  |   'featured_at': instance.featuredAt?.toIso8601String(), | ||||||
|  |   'social_credits': instance.socialCredits, | ||||||
|  |   'created_at': instance.createdAt.toIso8601String(), | ||||||
|  |   'updated_at': instance.updatedAt.toIso8601String(), | ||||||
|  |   'deleted_at': instance.deletedAt?.toIso8601String(), | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ sealed class SnSticker with _$SnSticker { | |||||||
|   const factory SnSticker({ |   const factory SnSticker({ | ||||||
|     required String id, |     required String id, | ||||||
|     required String slug, |     required String slug, | ||||||
|     required String imageId, |  | ||||||
|     required SnCloudFile image, |     required SnCloudFile image, | ||||||
|     required String packId, |     required String packId, | ||||||
|     required SnStickerPack? pack, |     required SnStickerPack? pack, | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ T _$identity<T>(T value) => value; | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnSticker { | mixin _$SnSticker { | ||||||
|  |  | ||||||
|  String get id; String get slug; String get imageId; SnCloudFile get image; String get packId; SnStickerPack? get pack; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; |  String get id; String get slug; SnCloudFile get image; String get packId; SnStickerPack? get pack; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnSticker | /// Create a copy of SnSticker | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -28,16 +28,16 @@ $SnStickerCopyWith<SnSticker> get copyWith => _$SnStickerCopyWithImpl<SnSticker> | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.imageId, imageId) || other.imageId == imageId)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,slug,imageId,image,packId,pack,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,slug,image,packId,pack,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnSticker(id: $id, slug: $slug, imageId: $imageId, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnSticker(id: $id, slug: $slug, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -48,7 +48,7 @@ abstract mixin class $SnStickerCopyWith<$Res>  { | |||||||
|   factory $SnStickerCopyWith(SnSticker value, $Res Function(SnSticker) _then) = _$SnStickerCopyWithImpl; |   factory $SnStickerCopyWith(SnSticker value, $Res Function(SnSticker) _then) = _$SnStickerCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String slug, String imageId, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String slug, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -65,11 +65,10 @@ class _$SnStickerCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnSticker | /// Create a copy of SnSticker | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? imageId = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||||
| as String,imageId: null == imageId ? _self.imageId : imageId // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable | as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable | ||||||
| as SnCloudFile,packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable | as SnCloudFile,packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,pack: freezed == pack ? _self.pack : pack // ignore: cast_nullable_to_non_nullable | as String,pack: freezed == pack ? _self.pack : pack // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -179,10 +178,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String slug,  String imageId,  SnCloudFile image,  String packId,  SnStickerPack? pack,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String slug,  SnCloudFile image,  String packId,  SnStickerPack? pack,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnSticker() when $default != null: | case _SnSticker() when $default != null: | ||||||
| return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.slug,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -200,10 +199,10 @@ return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String slug,  String imageId,  SnCloudFile image,  String packId,  SnStickerPack? pack,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String slug,  SnCloudFile image,  String packId,  SnStickerPack? pack,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnSticker(): | case _SnSticker(): | ||||||
| return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);} | return $default(_that.id,_that.slug,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -217,10 +216,10 @@ return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String slug,  String imageId,  SnCloudFile image,  String packId,  SnStickerPack? pack,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String slug,  SnCloudFile image,  String packId,  SnStickerPack? pack,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnSticker() when $default != null: | case _SnSticker() when $default != null: | ||||||
| return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.slug,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -232,12 +231,11 @@ return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnSticker implements SnSticker { | class _SnSticker implements SnSticker { | ||||||
|   const _SnSticker({required this.id, required this.slug, required this.imageId, required this.image, required this.packId, required this.pack, required this.createdAt, required this.updatedAt, required this.deletedAt}); |   const _SnSticker({required this.id, required this.slug, required this.image, required this.packId, required this.pack, required this.createdAt, required this.updatedAt, required this.deletedAt}); | ||||||
|   factory _SnSticker.fromJson(Map<String, dynamic> json) => _$SnStickerFromJson(json); |   factory _SnSticker.fromJson(Map<String, dynamic> json) => _$SnStickerFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @override final  String slug; | @override final  String slug; | ||||||
| @override final  String imageId; |  | ||||||
| @override final  SnCloudFile image; | @override final  SnCloudFile image; | ||||||
| @override final  String packId; | @override final  String packId; | ||||||
| @override final  SnStickerPack? pack; | @override final  SnStickerPack? pack; | ||||||
| @@ -258,16 +256,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.imageId, imageId) || other.imageId == imageId)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,slug,imageId,image,packId,pack,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,slug,image,packId,pack,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnSticker(id: $id, slug: $slug, imageId: $imageId, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnSticker(id: $id, slug: $slug, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -278,7 +276,7 @@ abstract mixin class _$SnStickerCopyWith<$Res> implements $SnStickerCopyWith<$Re | |||||||
|   factory _$SnStickerCopyWith(_SnSticker value, $Res Function(_SnSticker) _then) = __$SnStickerCopyWithImpl; |   factory _$SnStickerCopyWith(_SnSticker value, $Res Function(_SnSticker) _then) = __$SnStickerCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String slug, String imageId, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String slug, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -295,11 +293,10 @@ class __$SnStickerCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnSticker | /// Create a copy of SnSticker | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? imageId = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnSticker( |   return _then(_SnSticker( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||||
| as String,imageId: null == imageId ? _self.imageId : imageId // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable | as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable | ||||||
| as SnCloudFile,packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable | as SnCloudFile,packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,pack: freezed == pack ? _self.pack : pack // ignore: cast_nullable_to_non_nullable | as String,pack: freezed == pack ? _self.pack : pack // ignore: cast_nullable_to_non_nullable | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ part of 'sticker.dart'; | |||||||
| _SnSticker _$SnStickerFromJson(Map<String, dynamic> json) => _SnSticker( | _SnSticker _$SnStickerFromJson(Map<String, dynamic> json) => _SnSticker( | ||||||
|   id: json['id'] as String, |   id: json['id'] as String, | ||||||
|   slug: json['slug'] as String, |   slug: json['slug'] as String, | ||||||
|   imageId: json['image_id'] as String, |  | ||||||
|   image: SnCloudFile.fromJson(json['image'] as Map<String, dynamic>), |   image: SnCloudFile.fromJson(json['image'] as Map<String, dynamic>), | ||||||
|   packId: json['pack_id'] as String, |   packId: json['pack_id'] as String, | ||||||
|   pack: |   pack: | ||||||
| @@ -28,7 +27,6 @@ Map<String, dynamic> _$SnStickerToJson(_SnSticker instance) => | |||||||
|     <String, dynamic>{ |     <String, dynamic>{ | ||||||
|       'id': instance.id, |       'id': instance.id, | ||||||
|       'slug': instance.slug, |       'slug': instance.slug, | ||||||
|       'image_id': instance.imageId, |  | ||||||
|       'image': instance.image.toJson(), |       'image': instance.image.toJson(), | ||||||
|       'pack_id': instance.packId, |       'pack_id': instance.packId, | ||||||
|       'pack': instance.pack?.toJson(), |       'pack': instance.pack?.toJson(), | ||||||
|   | |||||||