Compare commits

...

7 Commits

Author SHA1 Message Date
926ae5402f 🐛 Fix bugs 2025-10-29 01:50:27 +08:00
1a37d384e6 ♻️ Refactor watchOS content view 2025-10-29 01:26:27 +08:00
d4cf598f69 Image rendering on watchOS 2025-10-29 00:47:23 +08:00
0106c08891 🐛 Fix API requesting on watchOS app 2025-10-28 23:20:52 +08:00
9697def808 Watch connectivity on iOS 2025-10-28 23:16:44 +08:00
6572875229 🎉 Created a watchOS app that compiles 2025-10-28 22:29:05 +08:00
66590b9079 🐛 Fix ios native code 2025-10-28 22:19:51 +08:00
28 changed files with 1546 additions and 7 deletions

View File

@@ -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.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -28,6 +25,8 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup
target 'Runner' do
platform :ios, '15.0'
use_frameworks!
use_modular_headers!
@@ -50,6 +49,16 @@ target 'Runner' do
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|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)

View File

@@ -219,6 +219,21 @@ PODS:
- irondash_engine_context (0.0.1):
- Flutter
- 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):
- Flutter
- flutter_webrtc
@@ -333,6 +348,7 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- Kingfisher (~> 8.0)
- KingfisherWebP
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
@@ -375,6 +391,8 @@ SPEC REPOS:
- GoogleDataTransport
- GoogleUtilities
- Kingfisher
- KingfisherWebP
- libwebp
- nanopb
- OrderedSet
- PromisesObjC
@@ -520,6 +538,8 @@ SPEC CHECKSUMS:
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
@@ -551,6 +571,6 @@ SPEC CHECKSUMS:
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f
PODFILE CHECKSUM: 3096dc559be56aca856e757e1dc65ca039801e2e
COCOAPODS: 1.16.2

View File

@@ -3,13 +3,14 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
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 */; };
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, ); }; };
@@ -20,6 +21,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
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 */; };
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 */; };
@@ -58,6 +60,17 @@
/* End PBXContainerItemProxy 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 */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -84,6 +97,7 @@
/* End PBXCopyFilesBuildPhase 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>"; };
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>"; };
@@ -100,6 +114,7 @@
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>"; };
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>"; };
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; };
@@ -111,6 +126,8 @@
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>"; };
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>"; };
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>"; };
@@ -120,6 +137,7 @@
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>"; };
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>"; };
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; };
@@ -162,6 +180,11 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = "WatchRunner Watch App";
sourceTree = "<group>";
};
73268D272DEB012A0076E970 /* Services */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
@@ -205,6 +228,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
7310A7D12EB10962002C0FD3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A1D34487886D362AC8B99B2E /* Pods_WatchRunner_Watch_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
73ACDFA82E3D0E6100B63535 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -258,6 +289,7 @@
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
802C1CFCA7F1E069AAEFB454 /* Pods_WatchRunner_Watch_App.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -280,6 +312,9 @@
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */,
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.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;
sourceTree = "<group>";
@@ -303,6 +338,7 @@
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
91E124CE95BCB4DCD890160D /* Pods */,
@@ -319,6 +355,7 @@
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */,
);
name = Products;
sourceTree = "<group>";
@@ -363,6 +400,28 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
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 */ = {
isa = PBXNativeTarget;
buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */;
@@ -434,6 +493,7 @@
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
7310A7DE2EB10963002C0FD3 /* Embed Watch Content */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
@@ -463,7 +523,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1640;
LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -471,6 +531,9 @@
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
7310A7D32EB10962002C0FD3 = {
CreatedOnToolsVersion = 26.0.1;
};
73ACDFAA2E3D0E6100B63535 = {
CreatedOnToolsVersion = 16.4;
};
@@ -504,6 +567,7 @@
73CDD6792DEC00480059D95D /* SolianNotificationService */,
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
7310A7D32EB10962002C0FD3 /* WatchRunner Watch App */,
);
};
/* End PBXProject section */
@@ -516,6 +580,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
7310A7D22EB10962002C0FD3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
73ACDFA92E3D0E6100B63535 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -598,10 +669,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -659,10 +734,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -683,6 +762,49 @@
shellPath = /bin/sh;
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" */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -734,6 +856,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
7310A7D02EB10962002C0FD3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
73ACDFA72E3D0E6100B63535 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -943,6 +1072,144 @@
};
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_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 = 26.0;
};
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_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 = 26.0;
};
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_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 = 26.0;
};
name = Profile;
};
73ACDFC42E3D0E6100B63535 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -1487,6 +1754,16 @@
defaultConfigurationIsVisible = 0;
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" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@@ -1,9 +1,11 @@
import Flutter
import UIKit
import WatchConnectivity
@main
@objc class AppDelegate: FlutterAppDelegate {
let notifyDelegate = NotifyDelegate()
private var watchConnectivityService: WatchConnectivityService?
override func application(
_ application: UIApplication,
@@ -28,6 +30,55 @@ import UIKit
GeneratedPluginRegistrant.register(with: self)
if WCSession.isSupported() {
watchConnectivityService = WatchConnectivityService()
}
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)
}
}
}

View File

@@ -8,7 +8,7 @@
import Foundation
func getAttachmentUrl(for identifier: String) -> String {
let serverBaseUrl = getServerUrl()
let serverBaseUrl = UserDefaults.standard.getServerUrl()
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "icon.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,15 @@
//
// ContentView.swift
// WatchRunner Watch App
//
// Created by LittleSheep on 2025/10/28.
//
import SwiftUI
// The root view of the app.
struct ContentView: View {
var body: some View {
ExploreView()
}
}

View 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
}
}

View File

@@ -0,0 +1,126 @@
//
// 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
}

View 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())
}
}

View 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

View File

@@ -0,0 +1,103 @@
//
// 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 dataTask: URLSessionDataTask?
private let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
deinit {
dataTask?.cancel()
}
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)
print("[watchOS] Image loaded successfully from redirect URL.")
} 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)
print("[watchOS] Image loaded successfully from redirect URL using KingfisherWebP.")
} 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)
print("[watchOS] Image loaded successfully from initial URL.")
} 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)
print("[watchOS] Image loaded successfully from initial URL using KingfisherWebP.")
} 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()
}
}

View File

@@ -0,0 +1,69 @@
//
// NetworkService.swift
// WatchRunner Watch App
//
// Created by LittleSheep on 2025/10/29.
//
import Foundation
// MARK: - Network Service
class NetworkService {
private let session = URLSession.shared
func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> [SnActivity] {
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
return try decoder.decode([SnActivity].self, from: data)
}
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))
}
}
}

View File

@@ -0,0 +1,38 @@
//
// 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
private var wcService = WatchConnectivityService()
private var cancellables = Set<AnyCancellable>()
init() {
wcService.$token.combineLatest(wcService.$serverUrl)
.receive(on: DispatchQueue.main)
.sink { [weak self] token, serverUrl in
self?.token = token
self?.serverUrl = serverUrl
if token != nil && serverUrl != nil {
self?.isReady = true
}
}
.store(in: &cancellables)
}
func requestData() {
wcService.requestDataFromPhone()
}
}

View File

@@ -0,0 +1,72 @@
//
// 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?
private let session: WCSession
override init() {
self.session = .default
super.init()
print("[watchOS] Activating WCSession")
self.session.delegate = self
self.session.activate()
}
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
}
if let serverUrl = message["serverUrl"] as? String {
self.serverUrl = serverUrl
}
}
}
func requestDataFromPhone() {
guard session.isReachable else {
print("[watchOS] Phone is not reachable")
return
}
print("[watchOS] Requesting data from phone")
session.sendMessage(["request": "data"]) { [weak self] response in
print("[watchOS] Received reply: \(response)")
DispatchQueue.main.async {
if let token = response["token"] as? String {
self?.token = token
}
if let serverUrl = response["serverUrl"] as? String {
self?.serverUrl = serverUrl
}
}
} errorHandler: { error in
print("[watchOS] sendMessage failed with error: \(error.localizedDescription)")
}
}
}

View File

@@ -0,0 +1,21 @@
//
// 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)"
}
print("[watchOS] Generated image URL: \(urlString)")
return URL(string: urlString)
}

View File

@@ -0,0 +1,50 @@
//
// 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 errorMessage: String?
private let networkService = NetworkService()
let filter: String
private var isMock = false
private var hasFetched = false // Add this
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 } // Check hasFetched
guard !isLoading else { return }
isLoading = true
errorMessage = nil
hasFetched = true // Set hasFetched
do {
let fetchedActivities = try await networkService.fetchActivities(filter: filter, token: token, serverUrl: serverUrl)
self.activities = fetchedActivities
} catch {
self.errorMessage = error.localizedDescription
print("[watchOS] fetchActivities failed with error: \(error)")
hasFetched = false // Reset on error
}
isLoading = false
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,66 @@
//
// 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(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)")
}
}
}
}
.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)
}
}

View File

@@ -0,0 +1,38 @@
//
// AttachmentImageView.swift
// WatchRunner Watch App
//
// Created by LittleSheep on 2025/10/29.
//
import SwiftUI
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)
}
}
}
}

View File

@@ -0,0 +1,52 @@
//
// 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)
.frame(height: 100)
}
.navigationTitle("New Post")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Post") {
Task {
if let token = appState.token, let serverUrl = appState.serverUrl {
await viewModel.createPost(token: token, serverUrl: serverUrl)
}
}
}
.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 ?? "")
})
}
}
}

View 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")
}
}

View 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")
}
ActivityListView(filter: "Subscriptions")
.tag("Subscriptions")
.tabItem {
Label("Subscriptions", systemImage: "star")
}
ActivityListView(filter: "Friends")
.tag("Friends")
.tabItem {
Label("Friends", systemImage: "person.2")
}
}
.navigationTitle(selectedTab)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: { isComposing = true }) {
Label("Compose", systemImage: "plus")
}
}
}
.environmentObject(appState)
} else {
ProgressView { Text("Connecting to phone...") }
.onAppear {
appState.requestData()
}
}
}
.sheet(isPresented: $isComposing) {
ComposePostView()
.environmentObject(appState)
}
}
}

View File

@@ -0,0 +1,142 @@
//
// 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 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)
}
if let content = post.content, !content.isEmpty {
Text(content)
.font(.body)
}
}
}
}
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")
}
}

View 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()
}
}
}