✨ Image rendering on watchOS
This commit is contained in:
		
							
								
								
									
										15
									
								
								ios/Podfile
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								ios/Podfile
									
									
									
									
									
								
							| @@ -1,6 +1,3 @@ | |||||||
| # Uncomment this line to define a global platform for your project |  | ||||||
| platform :ios, '15.0' |  | ||||||
|  |  | ||||||
| # CocoaPods analytics sends network stats synchronously affecting flutter build latency. | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. | ||||||
| ENV['COCOAPODS_DISABLE_STATS'] = 'true' | ENV['COCOAPODS_DISABLE_STATS'] = 'true' | ||||||
|  |  | ||||||
| @@ -28,6 +25,8 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe | |||||||
| flutter_ios_podfile_setup | flutter_ios_podfile_setup | ||||||
|  |  | ||||||
| target 'Runner' do | target 'Runner' do | ||||||
|  |   platform :ios, '15.0' | ||||||
|  |  | ||||||
|   use_frameworks! |   use_frameworks! | ||||||
|   use_modular_headers! |   use_modular_headers! | ||||||
|  |  | ||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -219,6 +219,21 @@ PODS: | |||||||
|   - irondash_engine_context (0.0.1): |   - irondash_engine_context (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - Kingfisher (8.6.0) |   - Kingfisher (8.6.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): |   - livekit_client (2.5.3): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - flutter_webrtc |     - flutter_webrtc | ||||||
| @@ -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 | ||||||
| @@ -520,6 +538,8 @@ SPEC CHECKSUMS: | |||||||
|   image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 |   image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 | ||||||
|   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 |   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 | ||||||
|   Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0 |   Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0 | ||||||
|  |   KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402 | ||||||
|  |   libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 | ||||||
|   livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40 |   livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40 | ||||||
|   local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb |   local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb | ||||||
|   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 |   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 | ||||||
| @@ -551,6 +571,6 @@ SPEC CHECKSUMS: | |||||||
|   wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 |   wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 | ||||||
|   WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e |   WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e | ||||||
|  |  | ||||||
| PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f | PODFILE CHECKSUM: 3096dc559be56aca856e757e1dc65ca039801e2e | ||||||
|  |  | ||||||
| COCOAPODS: 1.16.2 | COCOAPODS: 1.16.2 | ||||||
|   | |||||||
| @@ -21,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 */; }; | ||||||
| @@ -96,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>"; }; | ||||||
| @@ -124,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>"; }; | ||||||
| @@ -133,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; }; | ||||||
| @@ -227,6 +232,7 @@ | |||||||
| 			isa = PBXFrameworksBuildPhase; | 			isa = PBXFrameworksBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| 			files = ( | 			files = ( | ||||||
|  | 				A1D34487886D362AC8B99B2E /* Pods_WatchRunner_Watch_App.framework in Frameworks */, | ||||||
| 			); | 			); | ||||||
| 			runOnlyForDeploymentPostprocessing = 0; | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
| 		}; | 		}; | ||||||
| @@ -283,6 +289,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>"; | ||||||
| @@ -305,6 +312,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>"; | ||||||
| @@ -394,9 +404,11 @@ | |||||||
| 			isa = PBXNativeTarget; | 			isa = PBXNativeTarget; | ||||||
| 			buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "WatchRunner Watch App" */; | 			buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "WatchRunner Watch App" */; | ||||||
| 			buildPhases = ( | 			buildPhases = ( | ||||||
|  | 				DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */, | ||||||
| 				7310A7D02EB10962002C0FD3 /* Sources */, | 				7310A7D02EB10962002C0FD3 /* Sources */, | ||||||
| 				7310A7D12EB10962002C0FD3 /* Frameworks */, | 				7310A7D12EB10962002C0FD3 /* Frameworks */, | ||||||
| 				7310A7D22EB10962002C0FD3 /* Resources */, | 				7310A7D22EB10962002C0FD3 /* Resources */, | ||||||
|  | 				C74B07D6587D29C67A198025 /* [CP] Embed Pods Frameworks */, | ||||||
| 			); | 			); | ||||||
| 			buildRules = ( | 			buildRules = ( | ||||||
| 			); | 			); | ||||||
| @@ -750,6 +762,49 @@ | |||||||
| 			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", | ||||||
|  | 			); | ||||||
|  | 			inputPaths = ( | ||||||
|  | 			); | ||||||
|  | 			name = "[CP] Embed Pods Frameworks"; | ||||||
|  | 			outputFileListPaths = ( | ||||||
|  | 				"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist", | ||||||
|  | 			); | ||||||
|  | 			outputPaths = ( | ||||||
|  | 			); | ||||||
|  | 			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; | ||||||
| @@ -1019,6 +1074,7 @@ | |||||||
| 		}; | 		}; | ||||||
| 		7310A7E02EB10963002C0FD3 /* Debug */ = { | 		7310A7E02EB10963002C0FD3 /* Debug */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = 86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
| @@ -1033,7 +1089,7 @@ | |||||||
| 				CURRENT_PROJECT_VERSION = 1; | 				CURRENT_PROJECT_VERSION = 1; | ||||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||||
| 				ENABLE_PREVIEWS = YES; | 				ENABLE_PREVIEWS = YES; | ||||||
| 				ENABLE_USER_SCRIPT_SANDBOXING = YES; | 				ENABLE_USER_SCRIPT_SANDBOXING = NO; | ||||||
| 				GCC_C_LANGUAGE_STANDARD = gnu17; | 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||||
| 				GENERATE_INFOPLIST_FILE = YES; | 				GENERATE_INFOPLIST_FILE = YES; | ||||||
| 				INFOPLIST_KEY_CFBundleDisplayName = WatchRunner; | 				INFOPLIST_KEY_CFBundleDisplayName = WatchRunner; | ||||||
| @@ -1066,6 +1122,7 @@ | |||||||
| 		}; | 		}; | ||||||
| 		7310A7E12EB10963002C0FD3 /* Release */ = { | 		7310A7E12EB10963002C0FD3 /* Release */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
| @@ -1080,7 +1137,7 @@ | |||||||
| 				CURRENT_PROJECT_VERSION = 1; | 				CURRENT_PROJECT_VERSION = 1; | ||||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||||
| 				ENABLE_PREVIEWS = YES; | 				ENABLE_PREVIEWS = YES; | ||||||
| 				ENABLE_USER_SCRIPT_SANDBOXING = YES; | 				ENABLE_USER_SCRIPT_SANDBOXING = NO; | ||||||
| 				GCC_C_LANGUAGE_STANDARD = gnu17; | 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||||
| 				GENERATE_INFOPLIST_FILE = YES; | 				GENERATE_INFOPLIST_FILE = YES; | ||||||
| 				INFOPLIST_KEY_CFBundleDisplayName = WatchRunner; | 				INFOPLIST_KEY_CFBundleDisplayName = WatchRunner; | ||||||
| @@ -1110,6 +1167,7 @@ | |||||||
| 		}; | 		}; | ||||||
| 		7310A7E22EB10963002C0FD3 /* Profile */ = { | 		7310A7E22EB10963002C0FD3 /* Profile */ = { | ||||||
| 			isa = XCBuildConfiguration; | 			isa = XCBuildConfiguration; | ||||||
|  | 			baseConfigurationReference = 103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */; | ||||||
| 			buildSettings = { | 			buildSettings = { | ||||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||||
| @@ -1124,7 +1182,7 @@ | |||||||
| 				CURRENT_PROJECT_VERSION = 1; | 				CURRENT_PROJECT_VERSION = 1; | ||||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||||
| 				ENABLE_PREVIEWS = YES; | 				ENABLE_PREVIEWS = YES; | ||||||
| 				ENABLE_USER_SCRIPT_SANDBOXING = YES; | 				ENABLE_USER_SCRIPT_SANDBOXING = NO; | ||||||
| 				GCC_C_LANGUAGE_STANDARD = gnu17; | 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||||
| 				GENERATE_INFOPLIST_FILE = YES; | 				GENERATE_INFOPLIST_FILE = YES; | ||||||
| 				INFOPLIST_KEY_CFBundleDisplayName = WatchRunner; | 				INFOPLIST_KEY_CFBundleDisplayName = WatchRunner; | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  |  | ||||||
| // | // | ||||||
| //  ContentView.swift | //  ContentView.swift | ||||||
| //  WatchRunner Watch App | //  WatchRunner Watch App | ||||||
| @@ -8,6 +9,8 @@ | |||||||
| import SwiftUI | import SwiftUI | ||||||
| import Combine | import Combine | ||||||
| import WatchConnectivity | import WatchConnectivity | ||||||
|  | import Kingfisher // Import Kingfisher | ||||||
|  | import KingfisherWebP // Import KingfisherWebP | ||||||
|  |  | ||||||
| // MARK: - App State | // MARK: - App State | ||||||
|  |  | ||||||
| @@ -139,8 +142,11 @@ enum ActivityData: Codable { | |||||||
|  |  | ||||||
| struct SnPost: Codable, Identifiable { | struct SnPost: Codable, Identifiable { | ||||||
|     let id: String |     let id: String | ||||||
|     let content: String? |  | ||||||
|     let title: String? |     let title: String? | ||||||
|  |     let content: String? | ||||||
|  |     let publisher: SnPublisher | ||||||
|  |     let attachments: [SnCloudFile] | ||||||
|  |     let tags: [SnPostTag] | ||||||
| } | } | ||||||
|  |  | ||||||
| struct DiscoveryData: Codable { | struct DiscoveryData: Codable { | ||||||
| @@ -194,7 +200,20 @@ struct SnRealm: Codable, Identifiable { | |||||||
| struct SnPublisher: Codable, Identifiable { | struct SnPublisher: Codable, Identifiable { | ||||||
|     let id: String |     let id: String | ||||||
|     let name: String |     let name: String | ||||||
|  |     let nick: String? | ||||||
|     let description: 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 { | struct SnWebArticle: Codable, Identifiable { | ||||||
| @@ -203,6 +222,18 @@ struct SnWebArticle: Codable, Identifiable { | |||||||
|     let url: String |     let url: String | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // 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)" | ||||||
|  |     } | ||||||
|  |     print("[watchOS] Generated image URL: \(urlString)") | ||||||
|  |     return URL(string: urlString) | ||||||
|  | } | ||||||
|  |  | ||||||
| // MARK: - Network Service | // MARK: - Network Service | ||||||
|  |  | ||||||
| @@ -250,13 +281,19 @@ class ActivityViewModel: ObservableObject { | |||||||
|  |  | ||||||
|     private let networkService = NetworkService() |     private let networkService = NetworkService() | ||||||
|     let filter: String |     let filter: String | ||||||
|  |     private var isMock = false | ||||||
|      |      | ||||||
|     init(filter: String) { |     init(filter: String, mockActivities: [SnActivity]? = nil) { | ||||||
|         self.filter = filter |         self.filter = filter | ||||||
|  |         if let mockActivities = mockActivities { | ||||||
|  |             self.activities = mockActivities | ||||||
|  |             self.isMock = true | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     func fetchActivities(appState: AppState) async { |     func fetchActivities(token: String, serverUrl: String) async { | ||||||
|         guard !isLoading, appState.isReady, let token = appState.token, let serverUrl = appState.serverUrl else { return } |         if isMock { return } | ||||||
|  |         guard !isLoading else { return } | ||||||
|         isLoading = true |         isLoading = true | ||||||
|         errorMessage = nil |         errorMessage = nil | ||||||
|  |  | ||||||
| @@ -272,14 +309,178 @@ class ActivityViewModel: ObservableObject { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // 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 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: - Image Loader | ||||||
|  |  | ||||||
|  | @MainActor | ||||||
|  | class ImageLoader: ObservableObject { | ||||||
|  |     @Published var image: Image? | ||||||
|  |     @Published var errorMessage: String? | ||||||
|  |     @Published var isLoading = false | ||||||
|  |  | ||||||
|  |     private var dataTask: URLSessionDataTask? | ||||||
|  |     private let session: URLSession | ||||||
|  |  | ||||||
|  |     init(session: URLSession = .shared) { | ||||||
|  |         self.session = session | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func loadImage(from initialUrl: URL, token: String) async { | ||||||
|  |         isLoading = true | ||||||
|  |         errorMessage = nil | ||||||
|  |         image = nil | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             // First request with Authorization header | ||||||
|  |             var request = URLRequest(url: initialUrl) | ||||||
|  |             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 { | ||||||
|  |                 if httpResponse.statusCode == 302, let redirectLocation = httpResponse.allHeaderFields["Location"] as? String, let redirectUrl = URL(string: redirectLocation) { | ||||||
|  |                     print("[watchOS] Redirecting to: \(redirectUrl)") | ||||||
|  |                     // Second request to the redirected URL (S3 signed URL) without Authorization header | ||||||
|  |                     let (redirectData, _) = try await session.data(from: redirectUrl) | ||||||
|  |                     if let uiImage = UIImage(data: redirectData) { | ||||||
|  |                         self.image = Image(uiImage: uiImage) | ||||||
|  |                     } else { | ||||||
|  |                         // Try KingfisherWebP for WebP | ||||||
|  |                         let processor = WebPProcessor.default // Correct usage | ||||||
|  |                         if let kfImage = processor.process(item: .data(redirectData), options: KingfisherParsedOptionsInfo( | ||||||
|  |                             [ | ||||||
|  |                                 .processor(processor), | ||||||
|  |                                 .loadDiskFileSynchronously, | ||||||
|  |                                 .cacheOriginalImage | ||||||
|  |                             ] | ||||||
|  |                         )) { | ||||||
|  |                             self.image = Image(uiImage: kfImage) | ||||||
|  |                         } else { | ||||||
|  |                             self.errorMessage = "Invalid image data from redirect (could not decode with KingfisherWebP)." | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else if httpResponse.statusCode == 200 { | ||||||
|  |                     if let uiImage = UIImage(data: data) { | ||||||
|  |                         self.image = Image(uiImage: uiImage) | ||||||
|  |                     } else { | ||||||
|  |                         // Try KingfisherWebP for WebP | ||||||
|  |                         let processor = WebPProcessor.default // Correct usage | ||||||
|  |                         if let kfImage = processor.process(item: .data(data), options: KingfisherParsedOptionsInfo( | ||||||
|  |                             [ | ||||||
|  |                                 .processor(processor), | ||||||
|  |                                 .loadDiskFileSynchronously, | ||||||
|  |                                 .cacheOriginalImage | ||||||
|  |                             ] | ||||||
|  |                         )) { | ||||||
|  |                             self.image = Image(uiImage: kfImage) | ||||||
|  |                         } else { | ||||||
|  |                             self.errorMessage = "Invalid image data (could not decode with KingfisherWebP)." | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     self.errorMessage = "HTTP Status Code: \(httpResponse.statusCode)" | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch { | ||||||
|  |             self.errorMessage = error.localizedDescription | ||||||
|  |             print("[watchOS] Image loading failed: \(error.localizedDescription)") | ||||||
|  |         } | ||||||
|  |         isLoading = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func cancel() { | ||||||
|  |         dataTask?.cancel() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| // MARK: - Views | // MARK: - Views | ||||||
|  |  | ||||||
| struct ActivityListView: View { | struct ActivityListView: View { | ||||||
|     @StateObject private var viewModel: ActivityViewModel |     @StateObject private var viewModel: ActivityViewModel | ||||||
|     @EnvironmentObject var appState: AppState |     @EnvironmentObject var appState: AppState | ||||||
|  |  | ||||||
|     init(filter: String) { |     init(filter: String, mockActivities: [SnActivity]? = nil) { | ||||||
|         _viewModel = StateObject(wrappedValue: ActivityViewModel(filter: filter)) |         _viewModel = StateObject(wrappedValue: ActivityViewModel(filter: filter, mockActivities: mockActivities)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var body: some View { |     var body: some View { | ||||||
| @@ -302,8 +503,10 @@ struct ActivityListView: View { | |||||||
|                     switch activity.type { |                     switch activity.type { | ||||||
|                     case "posts.new", "posts.new.replies": |                     case "posts.new", "posts.new.replies": | ||||||
|                         if case .post(let post) = activity.data { |                         if case .post(let post) = activity.data { | ||||||
|  |                             NavigationLink(destination: PostDetailView(post: post)) { | ||||||
|                                 PostRowView(post: post) |                                 PostRowView(post: post) | ||||||
|                             } |                             } | ||||||
|  |                         } | ||||||
|                     case "discovery": |                     case "discovery": | ||||||
|                          if case .discovery(let discoveryData) = activity.data { |                          if case .discovery(let discoveryData) = activity.data { | ||||||
|                              DiscoveryView(discoveryData: discoveryData) |                              DiscoveryView(discoveryData: discoveryData) | ||||||
| @@ -315,7 +518,10 @@ struct ActivityListView: View { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         .task { |         .task { | ||||||
|             await viewModel.fetchActivities(appState: appState) |             // Only fetch if appState is ready and token/serverUrl are available | ||||||
|  |             if appState.isReady, let token = appState.token, let serverUrl = appState.serverUrl { | ||||||
|  |                 await viewModel.fetchActivities(token: token, serverUrl: serverUrl) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         .navigationTitle(viewModel.filter) |         .navigationTitle(viewModel.filter) | ||||||
|         .navigationBarTitleDisplayMode(.inline) |         .navigationBarTitleDisplayMode(.inline) | ||||||
| @@ -324,12 +530,51 @@ struct ActivityListView: View { | |||||||
|  |  | ||||||
| struct PostRowView: View { | struct PostRowView: View { | ||||||
|     let post: SnPost |     let post: SnPost | ||||||
|  |     @EnvironmentObject var appState: AppState | ||||||
|  |     @StateObject private var imageLoader = ImageLoader() // Instantiate ImageLoader | ||||||
|  |  | ||||||
|     var body: some View { |     var body: some View { | ||||||
|         VStack(alignment: .leading, spacing: 4) { |         VStack(alignment: .leading, spacing: 4) { | ||||||
|             Text(post.title ?? "Post") |             HStack { | ||||||
|  |                 if let serverUrl = appState.serverUrl, let pictureId = post.publisher.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token { | ||||||
|  |                     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) |                     .font(.headline) | ||||||
|             if let content = post.content { |             } | ||||||
|  |              | ||||||
|  |             if let content = post.content, !content.isEmpty { | ||||||
|                 Text(content) |                 Text(content) | ||||||
|                     .font(.body) |                     .font(.body) | ||||||
|             } |             } | ||||||
| @@ -337,27 +582,215 @@ struct PostRowView: View { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | 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 let serverUrl = appState.serverUrl, let pictureId = post.publisher.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token { | ||||||
|  |                         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) | ||||||
|  |                 } | ||||||
|  |                 .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 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 { | ||||||
|  |                     Divider() | ||||||
|  |                     Text("Attachments").font(.headline) | ||||||
|  |                     ForEach(post.attachments) { attachment in | ||||||
|  |                         AttachmentImageView(attachment: attachment) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 if !post.tags.isEmpty { | ||||||
|  |                     Divider() | ||||||
|  |                     Text("Tags").font(.headline) | ||||||
|  |                     FlowLayout(alignment: .leading, spacing: 4) { | ||||||
|  |                         ForEach(post.tags) { tag in | ||||||
|  |                             Text("#\(tag.name ?? tag.slug)") | ||||||
|  |                                 .font(.caption) | ||||||
|  |                                 .padding(.horizontal, 6) | ||||||
|  |                                 .padding(.vertical, 3) | ||||||
|  |                                 .background(Capsule().fill(Color.accentColor.opacity(0.2))) | ||||||
|  |                                 .cornerRadius(5) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .padding() | ||||||
|  |         } | ||||||
|  |         .navigationTitle("Post") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct AttachmentImageView: View { | ||||||
|  |     let attachment: SnCloudFile | ||||||
|  |     @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) | ||||||
|  |             } else if let errorMessage = imageLoader.errorMessage { | ||||||
|  |                 Text("Failed to load attachment: \(errorMessage)") | ||||||
|  |                     .font(.caption) | ||||||
|  |                     .foregroundColor(.red) | ||||||
|  |             } else { | ||||||
|  |                 Text("File: \(attachment.id)") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .task(id: attachment.id) { | ||||||
|  |             if let serverUrl = appState.serverUrl, let imageUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl), let token = appState.token, attachment.mimeType?.starts(with: "image") == true { | ||||||
|  |                 await imageLoader.loadImage(from: imageUrl, token: token) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| struct DiscoveryView: View { | struct DiscoveryView: View { | ||||||
|     let discoveryData: DiscoveryData |     let discoveryData: DiscoveryData | ||||||
|  |  | ||||||
|     var body: some View { |     var body: some View { | ||||||
|  |         NavigationLink(destination: DiscoveryDetailView(discoveryData: discoveryData)) { | ||||||
|             VStack(alignment: .leading) { |             VStack(alignment: .leading) { | ||||||
|                 Text("Discovery") |                 Text("Discovery") | ||||||
|                     .font(.headline) |                     .font(.headline) | ||||||
|                 .padding(.bottom, 2) |                 Text("\(discoveryData.items.count) new items to discover") | ||||||
|             ForEach(discoveryData.items) { item in |                     .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 { |             switch item.data { | ||||||
|             case .realm(let realm): |             case .realm(let realm): | ||||||
|                     Text("Realm: \(realm.name)") |                 Text("Realm").font(.headline) | ||||||
|  |                 Text(realm.name).foregroundColor(.secondary) | ||||||
|             case .publisher(let publisher): |             case .publisher(let publisher): | ||||||
|                     Text("Publisher: \(publisher.name)") |                 Text("Publisher").font(.headline) | ||||||
|  |                 Text(publisher.name).foregroundColor(.secondary) | ||||||
|             case .article(let article): |             case .article(let article): | ||||||
|                     Text("Article: \(article.title)") |                 Text("Article").font(.headline) | ||||||
|  |                 Text(article.title).foregroundColor(.secondary) | ||||||
|             case .unknown: |             case .unknown: | ||||||
|                     Text("Unknown discovery item") |                 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") | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -409,6 +842,36 @@ struct ContentView: View { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| #Preview { | #if DEBUG | ||||||
|     ContentView() | 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 | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     NavigationStack { | ||||||
|  |         ActivityListView(filter: "Preview", mockActivities: SnActivity.mock) | ||||||
|  |             .environmentObject(AppState()) | ||||||
|  |     } | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user