Compare commits
	
		
			188 Commits
		
	
	
		
			1abbd85614
			...
			v3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 01cc71fd47 | |||
| a2b0cd0b6a | |||
| 7f971bcee3 | |||
| 7de98a1731 | |||
| b52eb95b14 | |||
| b3ef7d6ad0 | |||
| d28c11940d | |||
| 504322c2dd | |||
| a07ec3ca36 | |||
| d96691e920 | |||
| 6273b2d917 | |||
| ab90d244b5 | |||
| dc6af6d9e5 | |||
| 0ca801d963 | |||
| 3edcdd72af | |||
| 402bb3fe04 | |||
| 8ba55eb1be | |||
| 983ae2a1fc | |||
| 6fc94001b3 | |||
| 44dbcfdc94 | |||
| b57caf56db | |||
| dbcd1b6d36 | |||
| a8055de910 | |||
| 49b15e7674 | |||
| e2369c40db | |||
| 44c5d91620 | |||
| 7a5a2407b7 | |||
| 234434f102 | |||
| 9c3b228d02 | |||
| 82682cae9a | |||
| fcbd5fe680 | |||
| ad91b17af7 | |||
| 24fa637329 | |||
| 926ae5402f | |||
| 1a37d384e6 | |||
| d4cf598f69 | |||
| 0106c08891 | |||
| 9697def808 | |||
| 6572875229 | |||
| 66590b9079 | |||
| 08b9604b55 | |||
| 0602bbd277 | |||
| 76e7ba7898 | |||
| 6e6616b236 | |||
| 071d51b25e | |||
| a958362461 | |||
| 6749bb00fe | |||
| 11fb20c673 | |||
| a7990f83db | |||
| 5f4cdf7937 | |||
| 3330ca14dd | |||
| 1719b1c8fe | |||
| 3c2c51bfaf | |||
| 239d6750ff | |||
| 8b0c91977a | |||
| f74cca8464 | |||
| 08091d51bf | |||
| 481190811b | |||
| 4b32b65d1c | |||
| 50ac7109bb | |||
| 62da279c71 | |||
| fde6dbf891 | |||
| 613bf4fb42 | |||
| 00ae586016 | |||
| ea0d132dce | |||
| aa2df1e847 | |||
| 50672795f3 | |||
| 383de9568d | |||
| 01fa228e45 | |||
| 1e71ad33a6 | |||
| 92c0260ecd | |||
| 0a161ad255 | |||
| c003f27b9a | |||
| 19db8309c4 | |||
| aa72ce08e8 | |||
| 4639b00b86 | |||
| cc5460ea55 | |||
| eafac811e6 | |||
| e3be691596 | |||
| aa180a1358 | |||
| c2707b8af1 | |||
| 62fd0500f3 | |||
| eeae865cc8 | |||
| cdf1413fe0 | |||
| 327b4c04f1 | |||
| bd903ce29c | |||
| 1b8ecb15ce | |||
| d4e380a97a | |||
| 126048b4fa | |||
| 8bec18813d | |||
| 1ae81794b1 | |||
| 2a7d12de48 | |||
| 64c60ead48 | |||
| 001549b190 | |||
| 4595865ad3 | |||
|  | 1834643167 | ||
|  | 0e816eaa3e | ||
|  | 7c1f24b824 | ||
| c6594ea2ce | |||
| 3bec6e683e | |||
| 83e92e2eed | |||
|  | b7d44d96ba | ||
| a83b929d42 | |||
| 9423affa75 | |||
| cda23db609 | |||
| 61074bc5a3 | |||
| 5feafa9255 | |||
| e604577c1f | |||
| af0ddd1273 | |||
| 8a6bb34808 | |||
| 4ef8445c77 | |||
| ec39ad6ca3 | |||
| eabb3154f1 | |||
| 910bf20eef | |||
| 5efa9b2ae8 | |||
| dd3e39e891 | |||
| b6896ded23 | |||
| f28a73ff9c | |||
| a014b64235 | |||
| 7e0e7c20d7 | |||
| 389fa515ba | |||
| 681ead02eb | |||
| 8d1c145b0b | |||
| 51b4754182 | |||
| 8a2b321701 | |||
| f685a7a249 | |||
| 76009147e9 | |||
| ce12f28e56 | |||
| 3604373a1e | |||
| 9704a4c2c7 | |||
| 67def56ad1 | |||
| 1be33916af | |||
| e8ff1bfd22 | |||
| 3ae56f3d89 | |||
| 707143e998 | |||
| 1fd34eb2a3 | |||
| d7ca41e946 | |||
| ad9fb0719a | |||
| e2d315afd4 | |||
| 6124dbfd79 | |||
| 5327f04ec0 | |||
| 41c56a2319 | |||
| f9d033542e | |||
| 91784e65e6 | |||
| 9d39c6a825 | |||
| 537e49f1a4 | |||
| 75bbd4df71 | |||
| 6ef4580d93 | |||
| 6ffd498761 | |||
| 27157e7cc1 | |||
| bbb07d574a | |||
| c660a419e2 | |||
| c3f61467c8 | |||
| 9bc47df452 | |||
| 9ef8ca4d45 | |||
| b55cbd08d1 | |||
| 8c6bd0feaa | |||
| 7dd4b20628 | |||
| fec0cb7640 | |||
| 75deb04a2b | |||
| 7c7ed21a96 | |||
| a201f20793 | |||
| 598c51bc1a | |||
| e1ea61c5f1 | |||
| ac424bde36 | |||
| b43b70df3f | |||
| 4321aa621a | |||
| d5d275fb43 | |||
| 6bb3307144 | |||
| 391604d4a2 | |||
| 1d9361c12f | |||
| a129b9cdd0 | |||
| 3bf815ac61 | |||
| 77bae4d6fd | |||
| 0a301c4c9b | |||
| 27b390a51c | |||
| 018386d14e | |||
| 3825d7c6c7 | |||
| bf930291e4 | |||
| a8c4988790 | |||
| 28dd204b1a | |||
| 3cbc1a59a7 | |||
| 277e9ae3d1 | |||
| 27b3ca25b7 | |||
| f871cd3b62 | |||
| a8a59ee30c | |||
| 2cd1416a13 | |||
| 6be7dfbc61 | 
| @@ -62,3 +62,9 @@ If you want to build the release version, use the flutter build command. Learn m | ||||
| ```bash | ||||
| flutter build <platform> | ||||
| ``` | ||||
|  | ||||
| ### Known Issues | ||||
|  | ||||
| Due to the issues with the flutter build tools, [see](https://github.com/flutter/flutter/issues/160622). | ||||
|  | ||||
| Since there is a watchOS app for iOS, you're unable to use the flutter cli to run iOS app. Use xcode instead. | ||||
| @@ -75,3 +75,4 @@ dependencies { | ||||
| flutter { | ||||
|     source = "../.." | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -48,6 +48,8 @@ | ||||
|     "deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.", | ||||
|     "deletePost": "Delete Post", | ||||
|     "deletePostHint": "Are you sure to delete this post?", | ||||
|     "deleteMessage": "Delete Message", | ||||
|     "deleteMessageConfirmation": "Are you sure you want to delete this message?", | ||||
|     "copyLink": "Copy Link", | ||||
|     "postCreateAccountTitle": "Thanks for joining!", | ||||
|     "postCreateAccountNext": "What's next?", | ||||
| @@ -133,6 +135,11 @@ | ||||
|     "reactionPositive": "Postive", | ||||
|     "reactionNegative": "Negative", | ||||
|     "reactionNeutral": "Neutral", | ||||
|     "customReaction": "Custom Reaction", | ||||
|     "customReactions": "Custom Reactions", | ||||
|     "stickerPlaceholder": "Sticker Placeholder", | ||||
|     "reactionAttitude": "Reaction Attitude", | ||||
|     "addReaction": "Add Reaction", | ||||
|     "connectionConnected": "Connected", | ||||
|     "connectionDisconnected": "Disconnected", | ||||
|     "connectionReconnecting": "Reconnecting", | ||||
| @@ -164,8 +171,8 @@ | ||||
|     "checkInResultLevel3": "Good Luck", | ||||
|     "checkInResultLevel4": "Best Luck", | ||||
|     "checkInActivityTitle": "{} checked in on {} and got a {}", | ||||
|     "eventCalander": "Event Calander", | ||||
|     "eventCalanderEmpty": "No events on that day.", | ||||
|     "eventCalendar": "Event Calendar", | ||||
|     "eventCalendarEmpty": "No events on that day.", | ||||
|     "fortuneGraph": "Fortune Trend", | ||||
|     "noFortuneData": "No fortune data available for this month.", | ||||
|     "creatorHub": "Creator Hub", | ||||
| @@ -261,7 +268,6 @@ | ||||
|     "walletCurrencyShortPoints": "NSP", | ||||
|     "walletCurrencyGolds": "The Solar Dollars", | ||||
|     "walletCurrencyShortGolds": "NSD", | ||||
|     "retry": "Retry", | ||||
|     "creatorHubUnselectedHint": "Pick / create a publisher to get started.", | ||||
|     "relationships": "Relationships", | ||||
|     "addFriend": "Send a Friend Request", | ||||
| @@ -312,6 +318,8 @@ | ||||
|     "settingsBackgroundImageClear": "Clear Background Image", | ||||
|     "settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image", | ||||
|     "messageNone": "No content to display", | ||||
|     "messageUpdateLinks": "Server generated links previews", | ||||
|     "messageUpdateEdited": "Edited a message", | ||||
|     "unreadMessages": { | ||||
|         "one": "{} unread message", | ||||
|         "other": "{} unread messages" | ||||
| @@ -471,7 +479,6 @@ | ||||
|     "accountProfileView": "View Profile", | ||||
|     "unspecified": "Unspecified", | ||||
|     "added": "Added", | ||||
|     "preview": "Preview", | ||||
|     "togglePreview": "Toggle Preview", | ||||
|     "subscribe": "Subscribe", | ||||
|     "unsubscribe": "Unsubscribe", | ||||
| @@ -632,6 +639,10 @@ | ||||
|     "chatNotJoined": "You have not joined this chat yet.", | ||||
|     "chatUnableJoin": "You can't join this chat due to it's access control settings.", | ||||
|     "chatJoin": "Join the Chat", | ||||
|     "chatReplyingTo": "Replying to {}", | ||||
|     "chatForwarding": "Forwarding message", | ||||
|     "chatEditing": "Editing message", | ||||
|     "chatNoContent": "No content", | ||||
|     "realmJoin": "Join the Realm", | ||||
|     "realmJoinSuccess": "Successfully joined the realm.", | ||||
|     "search": "Search", | ||||
| @@ -860,6 +871,7 @@ | ||||
|     "pollShortTextAnswerPreview": "Short text answer (preview)", | ||||
|     "award": "Award", | ||||
|     "awardPost": "Award Post", | ||||
|     "awardPoints": "Awarded {} points", | ||||
|     "awardMessage": "Message", | ||||
|     "awardMessageHint": "Enter your award message...", | ||||
|     "awardAttitude": "Attitude", | ||||
| @@ -1016,6 +1028,10 @@ | ||||
|     "searchLinks": "Links", | ||||
|     "searchAttachments": "Attachments", | ||||
|     "noMessagesFound": "No messages found", | ||||
|     "Searching...": "Searching...", | ||||
|     "searchError": "Search failed. Please try again.", | ||||
|     "tryDifferentKeywords": "Try different keywords or remove search filters", | ||||
|     "retry": "Retry", | ||||
|     "openInBrowser": "Open in Browser", | ||||
|     "highlightPost": "Highlight Post", | ||||
|     "filters": "Filters", | ||||
| @@ -1054,6 +1070,7 @@ | ||||
|     "iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">", | ||||
|     "parseIframe": "Parse Iframe", | ||||
|     "messageActions": "Message Actions", | ||||
|     "messageContent": "Message Content", | ||||
|     "viewEmbedLoadHint": "Tap to load", | ||||
|     "levelingStage1": "Novice", | ||||
|     "levelingStage2": "Apprentice", | ||||
| @@ -1095,6 +1112,9 @@ | ||||
|     "openReleasePage": "Open release page", | ||||
|     "postCompose": "Compose Post", | ||||
|     "postPublish": "Publish Post", | ||||
|     "restoreDraftTitle": "Restore Draft", | ||||
|     "restoreDraftMessage": "A draft was found. Do you want to restore it?", | ||||
|     "draft": "Draft", | ||||
|     "purchaseGift": "Purchase Gift", | ||||
|     "selectRecipient": "Select Recipient", | ||||
|     "changeRecipient": "Change Recipient", | ||||
| @@ -1198,5 +1218,89 @@ | ||||
|     "transferRemark": "Transfer Remark", | ||||
|     "addRemarkForTransfer": "Add remark for transfer", | ||||
|     "enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer", | ||||
|     "transferCreatedSuccessfully": "Transfer created successfully!" | ||||
|     "transferCreatedSuccessfully": "Transfer created successfully!", | ||||
|     "postUpdate": "Update", | ||||
|     "fileMetadata": "File Metadata", | ||||
|     "resend": "Resend", | ||||
|     "fileInfoTitle": "File Information", | ||||
|     "download": "Download", | ||||
|     "info": "Info", | ||||
|     "noStickers": "No Stickers", | ||||
|     "noStickersInPack": "This pack does not contains stickers", | ||||
|     "noStickerPacks": "No Sticker Packs", | ||||
|     "refresh": "Refresh", | ||||
|     "spoiler": "Spoiler", | ||||
|     "activityHeatmap": "Activity Heatmap", | ||||
|     "custom": "Custom", | ||||
|     "usernameColor": "Username Color", | ||||
|     "colorType": "Color Type", | ||||
|     "plain": "Plain", | ||||
|     "gradient": "Gradient", | ||||
|     "colorValue": "Color Value", | ||||
|     "gradientDirection": "Gradient Direction", | ||||
|     "gradientDirectionToRight": "To Right", | ||||
|     "gradientDirectionToLeft": "To Left", | ||||
|     "gradientDirectionToBottom": "To Bottom", | ||||
|     "gradientDirectionToTop": "To Top", | ||||
|     "gradientDirectionToBottomRight": "To Bottom Right", | ||||
|     "gradientDirectionToBottomLeft": "To Bottom Left", | ||||
|     "gradientDirectionToTopRight": "To Top Right", | ||||
|     "gradientDirectionToTopLeft": "To Top Left", | ||||
|     "gradientColors": "Gradient Colors", | ||||
|     "color": "Color", | ||||
|     "addColor": "Add Color", | ||||
|     "preview": "Preview", | ||||
|     "availableWithYourPlan": "Available with your plan", | ||||
|     "upgradeRequired": "Upgrade required", | ||||
|     "settingsDisableAnimation": "Disable Animation", | ||||
|     "addTag": "Add Tag", | ||||
|     "postFeaturedOn": "Post featured on {}", | ||||
|     "messageSentAt": "Sent at {}", | ||||
|     "myTickets": "My Tickets", | ||||
|     "drawHistory": "Draw History", | ||||
|     "lottery": "Lottery", | ||||
|     "noLotteryTickets": "No lottery tickets yet", | ||||
|     "buyYourFirstTicket": "Buy your first lottery ticket to get started!", | ||||
|     "buyTicket": "Buy Ticket", | ||||
|     "ticketNumbers": "Numbers: {}, Special: {}", | ||||
|     "cost": "Cost", | ||||
|     "multiplier": "Multiplier", | ||||
|     "prizeWon": "Prize Won", | ||||
|     "pending": "Pending", | ||||
|     "drawn": "Drawn", | ||||
|     "won": "Won", | ||||
|     "lost": "Lost", | ||||
|     "noDrawHistory": "No draw history yet", | ||||
|     "buyLotteryTicket": "Buy Lottery Ticket", | ||||
|     "selectNumbers": "Select Numbers", | ||||
|     "select5UniqueNumbers": "Select 5 unique numbers", | ||||
|     "selectSpecialNumber": "Select Special Number", | ||||
|     "selectMultiplier": "Select Multiplier", | ||||
|     "baseCost": "Base Cost", | ||||
|     "totalCost": "Total Cost", | ||||
|     "prizeStructure": "Prize Structure", | ||||
|     "enterPinToConfirmPurchase": "Enter your PIN to confirm purchase", | ||||
|     "ticketPurchasedSuccessfully": "Ticket purchased successfully!", | ||||
|     "winningNumbers": "Winning Numbers", | ||||
|     "specialNumber": "Special Number", | ||||
|     "totalTickets": "Total Tickets", | ||||
|     "totalWinners": "Total Winners", | ||||
|     "prizePool": "Prize Pool", | ||||
|     "enterPinToConfirmPayment": "Enter your PIN code to confirm payment", | ||||
|     "purchase": "Purchase", | ||||
|     "multiplierLabel": "Multiplier", | ||||
|     "specialOnly": "Special Only", | ||||
|     "matches": "Matches", | ||||
|     "thoughtDefaultTopic": "Reflection", | ||||
|     "thoughtAiName": "SN-chan", | ||||
|     "thoughtUserName": "You", | ||||
|     "thoughtStreamingHint": "Sn-chan is thinking...", | ||||
|     "thoughtInputHint": "Ask sn-chan anything...", | ||||
|     "thoughtNewConversation": "Start New Conversation", | ||||
|     "thoughtParseError": "Failed to parse AI response", | ||||
|     "thoughtFunctionCall": "Function Call", | ||||
|     "aiThought": "AI Thought", | ||||
|     "aiThoughtTitle": "Let sn-chan think", | ||||
|     "postReferenceUnavailable": "Referenced post is unavailable", | ||||
|     "fabLocation": "FAB Location" | ||||
| } | ||||
|   | ||||
| @@ -940,7 +940,7 @@ | ||||
|     "editBot": "编辑机器人", | ||||
|     "botAutomatedBy": "由 {} 自动化", | ||||
|     "botDetails": "机器人详情", | ||||
|     "overview": "总揽", | ||||
|     "overview": "总览", | ||||
|     "keys": "密钥", | ||||
|     "botNotFound": "机器人未找到。", | ||||
|     "newBotKey": "新建密钥", | ||||
| @@ -1060,7 +1060,7 @@ | ||||
|     "selectPool": "选择储存池", | ||||
|     "choosePool": "选择一个储存池", | ||||
|     "errorLoadingPools": "加载池时出错", | ||||
|     "quotaCostInfo": "此上传将消耗{} 配额点", | ||||
|     "quotaCostInfo": "此上传将消耗 {} 配额点", | ||||
|     "uploadConstraints": "上传限制", | ||||
|     "fileSizeExceeded": "文件大小超过了 {} 的最大限制", | ||||
|     "fileTypeNotAccepted": "此储存池不接受该文件类型", | ||||
| @@ -1076,5 +1076,19 @@ | ||||
|     "recycledFilesDeleted": "被回收文件成功删除", | ||||
|     "failedToDeleteRecycledFiles": "删除被回收文件失败", | ||||
|     "upload": "上传", | ||||
|     "systemWallet": "中央统筹" | ||||
|     "systemWallet": "中央统筹", | ||||
|     "postCompose": "撰写帖子", | ||||
|     "postPublish": "发布帖子", | ||||
|     "restoreDraftTitle": "恢复草稿", | ||||
|     "restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?", | ||||
|     "draft": "草稿", | ||||
|     "thoughtDefaultTopic": "寻思", | ||||
|     "thoughtAiName": "SN 酱", | ||||
|     "thoughtUserName": "您", | ||||
|     "thoughtStreamingHint": "SN 酱正在思考...", | ||||
|     "thoughtInputHint": "问 SN 酱任何问题...", | ||||
|     "thoughtNewConversation": "开始新对话", | ||||
|     "thoughtParseError": "解析 AI 响应失败", | ||||
|     "aiThought": "寻思", | ||||
|     "aiThoughtTitle": "让 SN 酱寻思寻思" | ||||
| } | ||||
| @@ -1075,5 +1075,7 @@ | ||||
|     "deleteRecycledFiles": "刪除已回收檔案", | ||||
|     "recycledFilesDeleted": "已回收檔案刪除成功", | ||||
|     "failedToDeleteRecycledFiles": "已回收檔案刪除失敗", | ||||
|     "upload": "上傳" | ||||
|     "upload": "上傳", | ||||
|     "postCompose": "撰寫帖子", | ||||
|     "postPublish": "發佈帖子" | ||||
| } | ||||
							
								
								
									
										15
									
								
								ios/Podfile
									
									
									
									
									
								
							
							
						
						| @@ -1,4 +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. | ||||
| @@ -32,6 +31,8 @@ target 'Runner' do | ||||
|   use_modular_headers! | ||||
|  | ||||
|   pod 'Alamofire' | ||||
|   pod 'Kingfisher', '~> 8.0' | ||||
|   pod 'KingfisherWebP' | ||||
|  | ||||
|   flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) | ||||
|  | ||||
| @@ -41,8 +42,6 @@ target 'Runner' do | ||||
|  | ||||
|   target 'SolianNotificationService' do | ||||
|     inherit! :search_paths | ||||
|     pod 'Kingfisher', '~> 8.0' | ||||
|     pod 'Alamofire' | ||||
|   end | ||||
|  | ||||
|   target 'SolianShareExtension' do | ||||
| @@ -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) | ||||
|   | ||||
							
								
								
									
										181
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							
							
						
						| @@ -44,83 +44,83 @@ PODS: | ||||
|     - Flutter | ||||
|   - file_saver (0.0.1): | ||||
|     - Flutter | ||||
|   - Firebase/CoreOnly (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|   - Firebase/Crashlytics (12.2.0): | ||||
|   - Firebase/CoreOnly (12.4.0): | ||||
|     - FirebaseCore (~> 12.4.0) | ||||
|   - Firebase/Crashlytics (12.4.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseCrashlytics (~> 12.2.0) | ||||
|   - Firebase/Messaging (12.2.0): | ||||
|     - FirebaseCrashlytics (~> 12.4.0) | ||||
|   - Firebase/Messaging (12.4.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 12.2.0) | ||||
|   - firebase_analytics (12.0.2): | ||||
|     - FirebaseMessaging (~> 12.4.0) | ||||
|   - firebase_analytics (12.0.3): | ||||
|     - firebase_core | ||||
|     - FirebaseAnalytics (= 12.2.0) | ||||
|     - FirebaseAnalytics (= 12.4.0) | ||||
|     - Flutter | ||||
|   - firebase_core (4.1.1): | ||||
|     - Firebase/CoreOnly (= 12.2.0) | ||||
|   - firebase_core (4.2.0): | ||||
|     - Firebase/CoreOnly (= 12.4.0) | ||||
|     - Flutter | ||||
|   - firebase_crashlytics (5.0.2): | ||||
|     - Firebase/Crashlytics (= 12.2.0) | ||||
|   - firebase_crashlytics (5.0.3): | ||||
|     - Firebase/Crashlytics (= 12.4.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - firebase_messaging (16.0.2): | ||||
|     - Firebase/Messaging (= 12.2.0) | ||||
|   - firebase_messaging (16.0.3): | ||||
|     - Firebase/Messaging (= 12.4.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - FirebaseAnalytics (12.2.0): | ||||
|     - FirebaseAnalytics/Default (= 12.2.0) | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|   - FirebaseAnalytics (12.4.0): | ||||
|     - FirebaseAnalytics/Default (= 12.4.0) | ||||
|     - FirebaseCore (~> 12.4.0) | ||||
|     - FirebaseInstallations (~> 12.4.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseAnalytics/Default (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|     - GoogleAppMeasurement/Default (= 12.2.0) | ||||
|   - FirebaseAnalytics/Default (12.4.0): | ||||
|     - FirebaseCore (~> 12.4.0) | ||||
|     - FirebaseInstallations (~> 12.4.0) | ||||
|     - GoogleAppMeasurement/Default (= 12.4.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseCore (12.2.0): | ||||
|     - FirebaseCoreInternal (~> 12.2.0) | ||||
|   - FirebaseCore (12.4.0): | ||||
|     - FirebaseCoreInternal (~> 12.4.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Logger (~> 8.1) | ||||
|   - FirebaseCoreExtension (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|   - FirebaseCoreInternal (12.2.0): | ||||
|   - FirebaseCoreExtension (12.4.0): | ||||
|     - FirebaseCore (~> 12.4.0) | ||||
|   - FirebaseCoreInternal (12.4.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|   - FirebaseCrashlytics (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|     - FirebaseRemoteConfigInterop (~> 12.2.0) | ||||
|     - FirebaseSessions (~> 12.2.0) | ||||
|   - FirebaseCrashlytics (12.4.0): | ||||
|     - FirebaseCore (~> 12.4.0) | ||||
|     - FirebaseInstallations (~> 12.4.0) | ||||
|     - FirebaseRemoteConfigInterop (~> 12.4.0) | ||||
|     - FirebaseSessions (~> 12.4.0) | ||||
|     - GoogleDataTransport (~> 10.1) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - nanopb (~> 3.30910.0) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseInstallations (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|   - FirebaseInstallations (12.4.0): | ||||
|     - FirebaseCore (~> 12.4.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|   - FirebaseMessaging (12.4.0): | ||||
|     - FirebaseCore (~> 12.4.0) | ||||
|     - FirebaseInstallations (~> 12.4.0) | ||||
|     - GoogleDataTransport (~> 10.1) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Reachability (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseRemoteConfigInterop (12.2.0) | ||||
|   - FirebaseSessions (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseCoreExtension (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|   - FirebaseRemoteConfigInterop (12.4.0) | ||||
|   - FirebaseSessions (12.4.0): | ||||
|     - FirebaseCore (~> 12.4.0) | ||||
|     - FirebaseCoreExtension (~> 12.4.0) | ||||
|     - FirebaseInstallations (~> 12.4.0) | ||||
|     - GoogleDataTransport (~> 10.1) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
| @@ -157,27 +157,28 @@ PODS: | ||||
|   - gal (1.0.0): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - GoogleAdsOnDeviceConversion (2.3.0): | ||||
|   - GoogleAdsOnDeviceConversion (3.1.0): | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Logger (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/Core (12.2.0): | ||||
|   - GoogleAppMeasurement/Core (12.4.0): | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/Default (12.2.0): | ||||
|     - GoogleAdsOnDeviceConversion (= 2.3.0) | ||||
|     - GoogleAppMeasurement/Core (= 12.2.0) | ||||
|     - GoogleAppMeasurement/IdentitySupport (= 12.2.0) | ||||
|   - GoogleAppMeasurement/Default (12.4.0): | ||||
|     - GoogleAdsOnDeviceConversion (~> 3.1.0) | ||||
|     - GoogleAppMeasurement/Core (= 12.4.0) | ||||
|     - GoogleAppMeasurement/IdentitySupport (= 12.4.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/IdentitySupport (12.2.0): | ||||
|     - GoogleAppMeasurement/Core (= 12.2.0) | ||||
|   - GoogleAppMeasurement/IdentitySupport (12.4.0): | ||||
|     - GoogleAppMeasurement/Core (= 12.4.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
| @@ -217,8 +218,23 @@ PODS: | ||||
|     - Flutter | ||||
|   - irondash_engine_context (0.0.1): | ||||
|     - Flutter | ||||
|   - Kingfisher (8.5.0) | ||||
|   - livekit_client (2.5.0): | ||||
|   - Kingfisher (8.6.1) | ||||
|   - 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 | ||||
|     - WebRTC-SDK (= 137.7151.04) | ||||
| @@ -254,9 +270,9 @@ PODS: | ||||
|   - record_ios (1.1.0): | ||||
|     - Flutter | ||||
|   - SAMKeychain (1.5.3) | ||||
|   - SDWebImage (5.21.2): | ||||
|     - SDWebImage/Core (= 5.21.2) | ||||
|   - SDWebImage/Core (5.21.2) | ||||
|   - SDWebImage (5.21.3): | ||||
|     - SDWebImage/Core (= 5.21.3) | ||||
|   - SDWebImage/Core (5.21.3) | ||||
|   - share_plus (0.0.1): | ||||
|     - Flutter | ||||
|   - shared_preferences_foundation (0.0.1): | ||||
| @@ -332,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`) | ||||
| @@ -374,6 +391,8 @@ SPEC REPOS: | ||||
|     - GoogleDataTransport | ||||
|     - GoogleUtilities | ||||
|     - Kingfisher | ||||
|     - KingfisherWebP | ||||
|     - libwebp | ||||
|     - nanopb | ||||
|     - OrderedSet | ||||
|     - PromisesObjC | ||||
| @@ -486,20 +505,20 @@ SPEC CHECKSUMS: | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be | ||||
|   file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 | ||||
|   Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1 | ||||
|   firebase_analytics: 8c78ce6224e0623152379d6cc7ef3d9098477b7e | ||||
|   firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d | ||||
|   firebase_crashlytics: e55dcf895eed0dd87c447dd5aff8db7f1bb8bbdb | ||||
|   firebase_messaging: 38c66c1184695b0c87abe51d40fc590718abed1a | ||||
|   FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8 | ||||
|   FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd | ||||
|   FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5 | ||||
|   FirebaseCoreInternal: 56ea29f3dad2894f81b060f706f9d53509b6ed3b | ||||
|   FirebaseCrashlytics: f83cbf176d5c637ade108c0aacf1ccbd5ec499bf | ||||
|   FirebaseInstallations: 3e884b01feabdf67582a80f3250425a00979b4ed | ||||
|   FirebaseMessaging: 43ec73bbfedd0c385a849bb91593ab4ad4b9e48e | ||||
|   FirebaseRemoteConfigInterop: 0896fd52ab72586a355c8f389ff85aaa9e5375e1 | ||||
|   FirebaseSessions: f4692789e770bec66ce17d772c0e9561c4f11737 | ||||
|   Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e | ||||
|   firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835 | ||||
|   firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464 | ||||
|   firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed | ||||
|   firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4 | ||||
|   FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f | ||||
|   FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 | ||||
|   FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018 | ||||
|   FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6 | ||||
|   FirebaseCrashlytics: a6ece278a837c7e88de2d9b5da0a3542f2342395 | ||||
|   FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2 | ||||
|   FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5 | ||||
|   FirebaseRemoteConfigInterop: 1e31ec72b89c9924367c59bfb5ec9ab60d1d6766 | ||||
|   FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d | ||||
|   Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 | ||||
|   flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9 | ||||
|   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 | ||||
| @@ -512,14 +531,16 @@ SPEC CHECKSUMS: | ||||
|   flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 | ||||
|   flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22 | ||||
|   gal: baecd024ebfd13c441269ca7404792a7152fde89 | ||||
|   GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe | ||||
|   GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af | ||||
|   GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1 | ||||
|   GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||
|   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | ||||
|   image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 | ||||
|   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 | ||||
|   Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c | ||||
|   livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4 | ||||
|   Kingfisher: 7ac7a7288653787a54206b11a3c74f49ab650f1f | ||||
|   KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402 | ||||
|   libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 | ||||
|   livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40 | ||||
|   local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb | ||||
|   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 | ||||
|   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 | ||||
| @@ -528,16 +549,16 @@ SPEC CHECKSUMS: | ||||
|   OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 | ||||
|   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 | ||||
|   pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c | ||||
|   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 | ||||
|   pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed | ||||
|   path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 | ||||
|   pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0 | ||||
|   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 | ||||
|   PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 | ||||
|   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 | ||||
|   record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374 | ||||
|   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c | ||||
|   SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a | ||||
|   SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a | ||||
|   share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a | ||||
|   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 | ||||
|   shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb | ||||
|   sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 | ||||
|   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 | ||||
|   sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b | ||||
| @@ -545,11 +566,11 @@ SPEC CHECKSUMS: | ||||
|   super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 | ||||
|   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 | ||||
|   syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14 | ||||
|   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d | ||||
|   url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b | ||||
|   volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 | ||||
|   wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 | ||||
|   WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e | ||||
|  | ||||
| PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f | ||||
| PODFILE CHECKSUM: fa173dbf2c15b3248d7fd65204fa9d6c4a6f13d6 | ||||
|  | ||||
| COCOAPODS: 1.16.2 | ||||
|   | ||||
| @@ -10,6 +10,7 @@ | ||||
| 		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 = 12; | ||||
| 			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,13 @@ | ||||
| /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ | ||||
|  | ||||
| /* Begin PBXFileSystemSynchronizedRootGroup section */ | ||||
| 		7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */ = { | ||||
| 			isa = PBXFileSystemSynchronizedRootGroup; | ||||
| 			exceptions = ( | ||||
| 			); | ||||
| 			path = "WatchRunner Watch App"; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		73268D272DEB012A0076E970 /* Services */ = { | ||||
| 			isa = PBXFileSystemSynchronizedRootGroup; | ||||
| 			exceptions = ( | ||||
| @@ -205,6 +230,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 +291,7 @@ | ||||
| 				7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */, | ||||
| 				73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */, | ||||
| 				73ACDFB82E3D0E6100B63535 /* UIKit.framework */, | ||||
| 				802C1CFCA7F1E069AAEFB454 /* Pods_WatchRunner_Watch_App.framework */, | ||||
| 			); | ||||
| 			name = Frameworks; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -280,6 +314,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 +340,7 @@ | ||||
| 				73CDD67B2DEC00480059D95D /* SolianNotificationService */, | ||||
| 				73C305CF2E0BE878009035B9 /* SolianShareExtension */, | ||||
| 				73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */, | ||||
| 				7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */, | ||||
| 				97C146EF1CF9000F007C117D /* Products */, | ||||
| 				331C8082294A63A400263BE5 /* RunnerTests */, | ||||
| 				91E124CE95BCB4DCD890160D /* Pods */, | ||||
| @@ -319,6 +357,7 @@ | ||||
| 				73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */, | ||||
| 				73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */, | ||||
| 				73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */, | ||||
| 				7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */, | ||||
| 			); | ||||
| 			name = Products; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -363,6 +402,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 */, | ||||
| 				E29ECA5954168075BDB000DC /* [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 +495,7 @@ | ||||
| 				97C146EA1CF9000F007C117D /* Sources */, | ||||
| 				97C146EB1CF9000F007C117D /* Frameworks */, | ||||
| 				73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */, | ||||
| 				7310A7DE2EB10963002C0FD3 /* Embed Watch Content */, | ||||
| 				97C146EC1CF9000F007C117D /* Resources */, | ||||
| 				9705A1C41CF9048500538489 /* Embed Frameworks */, | ||||
| 				3B06AD1E1E4923F5004D2608 /* Thin Binary */, | ||||
| @@ -463,7 +525,7 @@ | ||||
| 			isa = PBXProject; | ||||
| 			attributes = { | ||||
| 				BuildIndependentTargetsInParallel = YES; | ||||
| 				LastSwiftUpdateCheck = 1640; | ||||
| 				LastSwiftUpdateCheck = 2600; | ||||
| 				LastUpgradeCheck = 1510; | ||||
| 				ORGANIZATIONNAME = ""; | ||||
| 				TargetAttributes = { | ||||
| @@ -471,6 +533,9 @@ | ||||
| 						CreatedOnToolsVersion = 14.0; | ||||
| 						TestTargetID = 97C146ED1CF9000F007C117D; | ||||
| 					}; | ||||
| 					7310A7D32EB10962002C0FD3 = { | ||||
| 						CreatedOnToolsVersion = 26.0.1; | ||||
| 					}; | ||||
| 					73ACDFAA2E3D0E6100B63535 = { | ||||
| 						CreatedOnToolsVersion = 16.4; | ||||
| 					}; | ||||
| @@ -504,6 +569,7 @@ | ||||
| 				73CDD6792DEC00480059D95D /* SolianNotificationService */, | ||||
| 				73C305CD2E0BE878009035B9 /* SolianShareExtension */, | ||||
| 				73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */, | ||||
| 				7310A7D32EB10962002C0FD3 /* WatchRunner Watch App */, | ||||
| 			); | ||||
| 		}; | ||||
| /* End PBXProject section */ | ||||
| @@ -516,6 +582,13 @@ | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		7310A7D22EB10962002C0FD3 /* Resources */ = { | ||||
| 			isa = PBXResourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		73ACDFA92E3D0E6100B63535 /* Resources */ = { | ||||
| 			isa = PBXResourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| @@ -683,6 +756,45 @@ | ||||
| 			shellPath = /bin/sh; | ||||
| 			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; | ||||
| 		}; | ||||
| 		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; | ||||
| 		}; | ||||
| 		E29ECA5954168075BDB000DC /* [CP] Embed Pods Frameworks */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			inputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist", | ||||
| 			); | ||||
| 			name = "[CP] Embed Pods Frameworks"; | ||||
| 			outputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist", | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 			shellPath = /bin/sh; | ||||
| 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks.sh\"\n"; | ||||
| 			showEnvVarsInLog = 0; | ||||
| 		}; | ||||
| 		E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| @@ -734,6 +846,13 @@ | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		7310A7D02EB10962002C0FD3 /* Sources */ = { | ||||
| 			isa = PBXSourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		73ACDFA72E3D0E6100B63535 /* Sources */ = { | ||||
| 			isa = PBXSourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| @@ -873,6 +992,7 @@ | ||||
| 				CUSTOM_GROUP_ID = group.solsynth.solian; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				ENABLE_BITCODE = NO; | ||||
| 				EXCLUDED_SOURCE_FILE_NAMES = ""; | ||||
| 				INFOPLIST_FILE = Runner/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||
| @@ -883,10 +1003,12 @@ | ||||
| 				); | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)"; | ||||
| 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				VERSIONING_SYSTEM = "apple-generic"; | ||||
| 				WATCHOS_DEPLOYMENT_TARGET = 11.6; | ||||
| 			}; | ||||
| 			name = Profile; | ||||
| 		}; | ||||
| @@ -894,6 +1016,7 @@ | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = 14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; | ||||
| 				BUNDLE_LOADER = "$(TEST_HOST)"; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| @@ -902,6 +1025,8 @@ | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SUPPORTS_MACCATALYST = YES; | ||||
| 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| @@ -913,6 +1038,7 @@ | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = 14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; | ||||
| 				BUNDLE_LOADER = "$(TEST_HOST)"; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| @@ -921,6 +1047,8 @@ | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SUPPORTS_MACCATALYST = YES; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; | ||||
| 			}; | ||||
| @@ -930,6 +1058,7 @@ | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; | ||||
| 				BUNDLE_LOADER = "$(TEST_HOST)"; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| @@ -938,11 +1067,162 @@ | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SUPPORTS_MACCATALYST = YES; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; | ||||
| 			}; | ||||
| 			name = Profile; | ||||
| 		}; | ||||
| 		7310A7E02EB10963002C0FD3 /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = 86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||
| 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||
| 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; | ||||
| 				CLANG_ENABLE_OBJC_WEAK = YES; | ||||
| 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES; | ||||
| 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; | ||||
| 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				ENABLE_PREVIEWS = YES; | ||||
| 				ENABLE_USER_SCRIPT_SANDBOXING = NO; | ||||
| 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||
| 				GENERATE_INFOPLIST_FILE = YES; | ||||
| 				INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist"; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; | ||||
| 				INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian; | ||||
| 				INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 18.6; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| 				); | ||||
| 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES; | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; | ||||
| 				MTL_FAST_MATH = YES; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SDKROOT = watchos; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				STRING_CATALOG_GENERATE_SYMBOLS = YES; | ||||
| 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; | ||||
| 				SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||||
| 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||
| 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TARGETED_DEVICE_FAMILY = 4; | ||||
| 				WATCHOS_DEPLOYMENT_TARGET = 11.6; | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| 		7310A7E12EB10963002C0FD3 /* Release */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||
| 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||
| 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; | ||||
| 				CLANG_ENABLE_OBJC_WEAK = YES; | ||||
| 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES; | ||||
| 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; | ||||
| 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				ENABLE_PREVIEWS = YES; | ||||
| 				ENABLE_USER_SCRIPT_SANDBOXING = NO; | ||||
| 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||
| 				GENERATE_INFOPLIST_FILE = YES; | ||||
| 				INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist"; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; | ||||
| 				INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian; | ||||
| 				INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 18.6; | ||||
| 				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; | ||||
| 				SUPPORTED_PLATFORMS = "watchsimulator watchos"; | ||||
| 				SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||||
| 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TARGETED_DEVICE_FAMILY = 4; | ||||
| 				WATCHOS_DEPLOYMENT_TARGET = 11.6; | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| 		7310A7E22EB10963002C0FD3 /* Profile */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = 103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | ||||
| 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | ||||
| 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; | ||||
| 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; | ||||
| 				CLANG_ENABLE_OBJC_WEAK = YES; | ||||
| 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES; | ||||
| 				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; | ||||
| 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				ENABLE_PREVIEWS = YES; | ||||
| 				ENABLE_USER_SCRIPT_SANDBOXING = NO; | ||||
| 				GCC_C_LANGUAGE_STANDARD = gnu17; | ||||
| 				GENERATE_INFOPLIST_FILE = YES; | ||||
| 				INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist"; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; | ||||
| 				INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian; | ||||
| 				INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 18.6; | ||||
| 				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; | ||||
| 				SUPPORTED_PLATFORMS = "watchsimulator watchos"; | ||||
| 				SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||||
| 				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TARGETED_DEVICE_FAMILY = 4; | ||||
| 				WATCHOS_DEPLOYMENT_TARGET = 11.6; | ||||
| 			}; | ||||
| 			name = Profile; | ||||
| 		}; | ||||
| 		73ACDFC42E3D0E6100B63535 /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			buildSettings = { | ||||
| @@ -976,6 +1256,7 @@ | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||
| @@ -1016,6 +1297,7 @@ | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TARGETED_DEVICE_FAMILY = "1,2"; | ||||
| @@ -1054,6 +1336,7 @@ | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				TARGETED_DEVICE_FAMILY = "1,2"; | ||||
| @@ -1095,6 +1378,7 @@ | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_ENABLE_EXPLICIT_MODULES = NO; | ||||
| @@ -1138,6 +1422,7 @@ | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_ENABLE_EXPLICIT_MODULES = NO; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| @@ -1179,6 +1464,7 @@ | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SKIP_INSTALL = YES; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SWIFT_EMIT_LOC_STRINGS = YES; | ||||
| 				SWIFT_ENABLE_EXPLICIT_MODULES = NO; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| @@ -1428,6 +1714,7 @@ | ||||
| 				CUSTOM_GROUP_ID = group.solsynth.solian; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				ENABLE_BITCODE = NO; | ||||
| 				EXCLUDED_SOURCE_FILE_NAMES = ""; | ||||
| 				INFOPLIST_FILE = Runner/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||
| @@ -1443,6 +1730,7 @@ | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				VERSIONING_SYSTEM = "apple-generic"; | ||||
| 				WATCHOS_DEPLOYMENT_TARGET = 11.6; | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| @@ -1457,6 +1745,7 @@ | ||||
| 				CUSTOM_GROUP_ID = group.solsynth.solian; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				ENABLE_BITCODE = NO; | ||||
| 				EXCLUDED_SOURCE_FILE_NAMES = ""; | ||||
| 				INFOPLIST_FILE = Runner/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||
| @@ -1465,12 +1754,15 @@ | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| 				); | ||||
| 				ONLY_ACTIVE_ARCH = NO; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)"; | ||||
| 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				VERSIONING_SYSTEM = "apple-generic"; | ||||
| 				WATCHOS_DEPLOYMENT_TARGET = 11.6; | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| @@ -1487,6 +1779,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 = ( | ||||
|   | ||||
| @@ -20,6 +20,20 @@ | ||||
|                ReferencedContainer = "container:Runner.xcodeproj"> | ||||
|             </BuildableReference> | ||||
|          </BuildActionEntry> | ||||
|          <BuildActionEntry | ||||
|             buildForTesting = "YES" | ||||
|             buildForRunning = "YES" | ||||
|             buildForProfiling = "YES" | ||||
|             buildForArchiving = "YES" | ||||
|             buildForAnalyzing = "YES"> | ||||
|             <BuildableReference | ||||
|                BuildableIdentifier = "primary" | ||||
|                BlueprintIdentifier = "7310A7D32EB10962002C0FD3" | ||||
|                BuildableName = "WatchRunner Watch App.app" | ||||
|                BlueprintName = "WatchRunner Watch App" | ||||
|                ReferencedContainer = "container:Runner.xcodeproj"> | ||||
|             </BuildableReference> | ||||
|          </BuildActionEntry> | ||||
|       </BuildActionEntries> | ||||
|    </BuildAction> | ||||
|    <TestAction | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import Flutter | ||||
| import UIKit | ||||
| import WatchConnectivity | ||||
|  | ||||
| @main | ||||
| @objc class AppDelegate: FlutterAppDelegate { | ||||
|     let notifyDelegate = NotifyDelegate() | ||||
|     private static var sharedWatchConnectivityService: WatchConnectivityService? | ||||
|  | ||||
|     override func application( | ||||
|         _ application: UIApplication, | ||||
| @@ -23,11 +25,85 @@ import UIKit | ||||
|             intentIdentifiers: [], | ||||
|             options: [] | ||||
|         ) | ||||
|          | ||||
|         UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) | ||||
|          | ||||
|         GeneratedPluginRegistrant.register(with: self) | ||||
|          | ||||
|         // Always initialize and retain a strong reference | ||||
|         if WCSession.isSupported() { | ||||
|             AppDelegate.sharedWatchConnectivityService = WatchConnectivityService.shared | ||||
|         } else { | ||||
|             print("[iOS] WCSession not supported on this device.") | ||||
|         } | ||||
|          | ||||
|         return super.application(application, didFinishLaunchingWithOptions: launchOptions) | ||||
|     } | ||||
| } | ||||
|  | ||||
| final class WatchConnectivityService: NSObject, WCSessionDelegate { | ||||
|     static let shared = WatchConnectivityService() | ||||
|     private let session: WCSession = .default | ||||
|      | ||||
|     private override init() { | ||||
|         super.init() | ||||
|         print("[iOS] Activating WCSession...") | ||||
|         session.delegate = self | ||||
|         session.activate() | ||||
|     } | ||||
|  | ||||
|     // MARK: - WCSessionDelegate | ||||
|  | ||||
|     func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { | ||||
|         if let error = error { | ||||
|             print("[iOS] WCSession activation failed: \(error.localizedDescription)") | ||||
|         } else { | ||||
|             print("[iOS] WCSession activated with state: \(activationState.rawValue)") | ||||
|             if activationState == .activated { | ||||
|                 sendDataToWatch() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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() | ||||
|              | ||||
|             var data: [String: Any] = ["serverUrl": serverUrl ?? ""] | ||||
|             if let token = token { | ||||
|                 data["token"] = token | ||||
|             } | ||||
|              | ||||
|             print("[iOS] Replying with data: \(data)") | ||||
|             replyHandler(data) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func sendDataToWatch() { | ||||
|         guard session.activationState == .activated else { | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         let token = UserDefaults.standard.getFlutterToken() | ||||
|         let serverUrl = UserDefaults.standard.getServerUrl() | ||||
|          | ||||
|         var data: [String: Any] = ["serverUrl": serverUrl ?? ""] | ||||
|         if let token = token { | ||||
|             data["token"] = token | ||||
|         } | ||||
|          | ||||
|         do { | ||||
|             try session.updateApplicationContext(data) | ||||
|             print("[iOS] Sent application context: \(data)") | ||||
|         } catch { | ||||
|             print("[iOS] Failed to send application context: \(error.localizedDescription)") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1 +1,334 @@ | ||||
| {"images":[{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@2x.png","scale":"2x","platform":"ios"},{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@3x.png","scale":"3x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@2x.png","scale":"2x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@3x.png","scale":"3x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@2x.png","scale":"2x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@3x.png","scale":"3x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@2x.png","scale":"2x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@3x.png","scale":"3x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@2x.png","scale":"2x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@3x.png","scale":"3x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@2x.png","scale":"2x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@3x.png","scale":"3x","platform":"ios"},{"size":"68x68","idiom":"universal","filename":"Icon-App-68x68@2x.png","scale":"2x","platform":"ios"},{"size":"76x76","idiom":"universal","filename":"Icon-App-76x76@2x.png","scale":"2x","platform":"ios"},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x","platform":"ios"},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-1024x1024@1x.png","scale":"1x","platform":"ios"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"68x68","idiom":"universal","filename":"Icon-App-Dark-68x68@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"76x76","idiom":"universal","filename":"Icon-App-Dark-76x76@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-Dark-83.5x83.5@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-Dark-1024x1024@1x.png","scale":"1x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]}],"info":{"version":1,"author":"xcode"}} | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "filename" : "Icon-App-20x20@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "20x20" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-20x20@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "20x20" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-29x29@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "29x29" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-29x29@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "29x29" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-38x38@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "38x38" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-38x38@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "38x38" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-40x40@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "40x40" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-40x40@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "40x40" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-60x60@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "60x60" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-60x60@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "60x60" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-64x64@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "64x64" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-64x64@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "64x64" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-68x68@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "68x68" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-76x76@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "76x76" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-83.5x83.5@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "83.5x83.5" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-1024x1024@1x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "1x", | ||||
|       "size" : "1024x1024" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-20x20@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "20x20" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-20x20@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "20x20" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-29x29@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "29x29" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-29x29@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "29x29" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-38x38@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "38x38" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-38x38@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "38x38" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-40x40@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "40x40" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-40x40@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "40x40" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-60x60@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "60x60" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-60x60@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "60x60" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-64x64@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "64x64" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-64x64@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "64x64" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-68x68@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "68x68" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-76x76@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "76x76" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-83.5x83.5@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "83.5x83.5" | ||||
|     }, | ||||
|     { | ||||
|       "appearances" : [ | ||||
|         { | ||||
|           "appearance" : "luminosity", | ||||
|           "value" : "dark" | ||||
|         } | ||||
|       ], | ||||
|       "filename" : "Icon-App-Dark-1024x1024@1x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "1x", | ||||
|       "size" : "1024x1024" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "Icon-App-1024x1024@1x.png", | ||||
|       "idiom" : "ios-marketing", | ||||
|       "scale" : "1x", | ||||
|       "size" : "1024x1024" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
|   | ||||
| Before Width: | Height: | Size: 295 B | 
| Before Width: | Height: | Size: 282 B | 
| Before Width: | Height: | Size: 406 B | 
| Before Width: | Height: | Size: 762 B | 
| @@ -95,6 +95,8 @@ | ||||
| 		<string>UIInterfaceOrientationLandscapeRight</string> | ||||
| 		<string>UIInterfaceOrientationPortrait</string> | ||||
| 	</array> | ||||
| 	<key>WKCompanionAppBundleIdentifier</key> | ||||
| 	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||||
| 	<key>UISupportedInterfaceOrientations~ipad</key> | ||||
| 	<array> | ||||
| 		<string>UIInterfaceOrientationLandscapeLeft</string> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| import Foundation | ||||
|  | ||||
| func getAttachmentUrl(for identifier: String) -> String { | ||||
|     let serverBaseUrl = "https://api.solian.app" | ||||
|     let serverBaseUrl = UserDefaults.standard.getServerUrl() | ||||
|      | ||||
|     return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)" | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,6 @@ extension UserDefaults { | ||||
|     } | ||||
|      | ||||
|     func getServerUrl(forKey key: String = "app_server_url") -> String { | ||||
|         return self.getFlutterValue(forKey: key) ?? "https://nt.solian.app" | ||||
|         return self.getFlutterValue(forKey: key) ?? "https://api.solian.app" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -59,14 +59,16 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|         } | ||||
|  | ||||
|         let pfpIdentifier = meta["pfp"] as? String | ||||
|          | ||||
|         let metaCopy = meta as? [String: Any] ?? [:] | ||||
|         let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil | ||||
|  | ||||
|         let handle = INPersonHandle(value: "\(metaCopy["user_id"] ?? "")", type: .unknown) | ||||
|  | ||||
|         if let pfpUrl = pfpUrl, let url = URL(string: pfpUrl) { | ||||
|             let targetSize = 512 | ||||
|             let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit) | ||||
|  | ||||
|         KingfisherManager.shared.retrieveImage(with: URL(string: pfpUrl!)!, options: [.processor(scaleProcessor)], completionHandler: { result in | ||||
|             KingfisherManager.shared.retrieveImage(with: url, options: [.processor(scaleProcessor)], completionHandler: { result in | ||||
|                 var image: Data? | ||||
|                 switch result { | ||||
|                 case .success(let value): | ||||
| @@ -75,7 +77,6 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|                     print("Unable to get pfp url: \(error)") | ||||
|                 } | ||||
|  | ||||
|             let handle = INPersonHandle(value: "\(metaCopy["user_id"] ?? "")", type: .unknown) | ||||
|                 let sender = INPerson( | ||||
|                     personHandle: handle, | ||||
|                     nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"), | ||||
| @@ -87,14 +88,26 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|  | ||||
|                 let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body) | ||||
|                 self.donateInteraction(for: intent) | ||||
|             let updatedContent = try? request.content.updating(from: intent) | ||||
|  | ||||
|                 content.categoryIdentifier = "CHAT_MESSAGE" | ||||
|             if let updatedContent = updatedContent { | ||||
|                 self.contentHandler?(updatedContent) | ||||
|                 self.contentHandler?(content) | ||||
|             }) | ||||
|         } else { | ||||
|             let sender = INPerson( | ||||
|                 personHandle: handle, | ||||
|                 nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"), | ||||
|                 displayName: content.title, | ||||
|                 image: nil, | ||||
|                 contactIdentifier: nil, | ||||
|                 customIdentifier: nil | ||||
|             ) | ||||
|  | ||||
|             let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body) | ||||
|             self.donateInteraction(for: intent) | ||||
|  | ||||
|             content.categoryIdentifier = "CHAT_MESSAGE" | ||||
|             self.contentHandler?(content) | ||||
|         } | ||||
|         }) | ||||
|     } | ||||
|      | ||||
|     private func handleDefaultNotification(content: UNMutableNotificationContent) throws { | ||||
|   | ||||
| @@ -0,0 +1,15 @@ | ||||
| { | ||||
|   "colors" : [ | ||||
|     { | ||||
|       "color" : { | ||||
|         "platform" : "universal", | ||||
|         "reference" : "systemIndigoColor" | ||||
|       }, | ||||
|       "idiom" : "universal" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,318 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "filename" : "icon-ios-20x20@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "20x20" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-20x20@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "20x20" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-29x29@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "29x29" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-29x29@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "29x29" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-38x38@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "38x38" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-38x38@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "38x38" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-40x40@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "40x40" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-40x40@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "40x40" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-60x60@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "60x60" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-60x60@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "60x60" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-64x64@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "64x64" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-64x64@3x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "3x", | ||||
|       "size" : "64x64" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-68x68@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "68x68" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-76x76@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "76x76" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-83.5x83.5@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "scale" : "2x", | ||||
|       "size" : "83.5x83.5" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-ios-1024x1024.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "ios", | ||||
|       "size" : "1024x1024" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-16x16.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "1x", | ||||
|       "size" : "16x16" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-16x16@2x.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "2x", | ||||
|       "size" : "16x16" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-32x32.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "1x", | ||||
|       "size" : "32x32" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-32x32@2x.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "2x", | ||||
|       "size" : "32x32" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-128x128.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "1x", | ||||
|       "size" : "128x128" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-128x128@2x.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "2x", | ||||
|       "size" : "128x128" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-256x256.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "1x", | ||||
|       "size" : "256x256" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-256x256@2x.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "2x", | ||||
|       "size" : "256x256" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-512x512.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "1x", | ||||
|       "size" : "512x512" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-mac-512x512@2x.png", | ||||
|       "idiom" : "mac", | ||||
|       "scale" : "2x", | ||||
|       "size" : "512x512" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-22x22@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "22x22" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-24x24@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "24x24" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-27.5x27.5@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "27.5x27.5" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-29x29@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "29x29" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-30x30@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "30x30" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-32x32@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "32x32" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-33x33@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "33x33" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-40x40@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "40x40" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-43.5x43.5@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "43.5x43.5" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-44x44@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "44x44" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-46x46@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "46x46" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-50x50@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "50x50" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-51x51@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "51x51" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-54x54@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "54x54" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-86x86@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "86x86" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-98x98@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "98x98" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-108x108@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "108x108" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-117x117@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "117x117" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-129x129@2x.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "scale" : "2x", | ||||
|       "size" : "129x129" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "icon-watchos-1024x1024.png", | ||||
|       "idiom" : "universal", | ||||
|       "platform" : "watchos", | ||||
|       "size" : "1024x1024" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
| After Width: | Height: | Size: 45 KiB | 
| After Width: | Height: | Size: 1.5 KiB | 
| After Width: | Height: | Size: 2.5 KiB | 
| After Width: | Height: | Size: 2.4 KiB | 
| After Width: | Height: | Size: 4.1 KiB | 
| After Width: | Height: | Size: 3.4 KiB | 
| After Width: | Height: | Size: 5.7 KiB | 
| After Width: | Height: | Size: 3.6 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 9.6 KiB | 
| After Width: | Height: | Size: 6.6 KiB | 
| After Width: | Height: | Size: 10 KiB | 
| After Width: | Height: | Size: 7.0 KiB | 
| After Width: | Height: | Size: 7.8 KiB | 
| After Width: | Height: | Size: 8.8 KiB | 
| After Width: | Height: | Size: 6.6 KiB | 
| After Width: | Height: | Size: 14 KiB | 
| After Width: | Height: | Size: 473 B | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 14 KiB | 
| After Width: | Height: | Size: 30 KiB | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 2.7 KiB | 
| After Width: | Height: | Size: 30 KiB | 
| After Width: | Height: | Size: 45 KiB | 
| After Width: | Height: | Size: 45 KiB | 
| After Width: | Height: | Size: 12 KiB | 
| After Width: | Height: | Size: 13 KiB | 
| After Width: | Height: | Size: 14 KiB | 
| After Width: | Height: | Size: 1.7 KiB | 
| After Width: | Height: | Size: 1.9 KiB | 
| After Width: | Height: | Size: 2.3 KiB | 
| After Width: | Height: | Size: 2.4 KiB | 
| After Width: | Height: | Size: 2.5 KiB | 
| After Width: | Height: | Size: 2.7 KiB | 
| After Width: | Height: | Size: 2.9 KiB | 
| After Width: | Height: | Size: 3.6 KiB | 
| After Width: | Height: | Size: 4.1 KiB | 
| After Width: | Height: | Size: 4.2 KiB | 
| After Width: | Height: | Size: 4.4 KiB | 
| After Width: | Height: | Size: 4.9 KiB | 
| After Width: | Height: | Size: 5.0 KiB | 
| After Width: | Height: | Size: 5.3 KiB | 
| After Width: | Height: | Size: 9.1 KiB | 
| After Width: | Height: | Size: 10 KiB | 
							
								
								
									
										6
									
								
								ios/WatchRunner Watch App/Assets.xcassets/Contents.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										21
									
								
								ios/WatchRunner Watch App/Assets.xcassets/Logo.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "filename" : "icon.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								ios/WatchRunner Watch App/Assets.xcassets/Logo.imageset/icon.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 70 KiB | 
							
								
								
									
										50
									
								
								ios/WatchRunner Watch App/ContentView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | ||||
| // | ||||
| //  ContentView.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/28. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| // The root view of the app. | ||||
| struct ContentView: View { | ||||
|     @StateObject private var appState = AppState() | ||||
|     @State private var selection: Panel? = .explore | ||||
|      | ||||
|     enum Panel: Hashable { | ||||
|         case explore | ||||
|         case chat | ||||
|         case notifications | ||||
|         case account | ||||
|     } | ||||
|      | ||||
|     var body: some View { | ||||
|         NavigationSplitView { | ||||
|             List(selection: $selection) { | ||||
|                 AppInfoHeaderView() | ||||
|                     .listRowBackground(Color.clear) | ||||
|                     .environmentObject(appState) | ||||
|                  | ||||
|                 Label("Explore", systemImage: "globe.fill").tag(Panel.explore) | ||||
|                 Label("Chat", systemImage: "message.fill").tag(Panel.chat) | ||||
|                 Label("Notifications", systemImage: "bell.fill").tag(Panel.notifications) | ||||
|                 Label("Account", systemImage: "person.circle.fill").tag(Panel.account) | ||||
|             } | ||||
|             .listStyle(.automatic) | ||||
|         } detail: { | ||||
|             switch selection { | ||||
|             case .explore: | ||||
|                 ExploreView().environmentObject(appState) | ||||
|             case .chat: | ||||
|                 ChatView().environmentObject(appState) | ||||
|             case .notifications: | ||||
|                 NotificationView().environmentObject(appState) | ||||
|             case .account: | ||||
|                 AccountView().environmentObject(appState) | ||||
|             case .none: | ||||
|                 Text("Select a panel") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										88
									
								
								ios/WatchRunner Watch App/Layouts/FlowLayout.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,88 @@ | ||||
| // | ||||
| //  FlowLayout.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| // MARK: - Custom Layouts | ||||
|  | ||||
| struct FlowLayout: Layout { | ||||
|     var alignment: HorizontalAlignment = .leading | ||||
|     var spacing: CGFloat = 10 | ||||
|  | ||||
|     func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { | ||||
|         let containerWidth = proposal.width ?? 0 | ||||
|         let sizes = subviews.map { $0.sizeThatFits(.unspecified) } | ||||
|  | ||||
|         var currentX: CGFloat = 0 | ||||
|         var currentY: CGFloat = 0 | ||||
|         var lineHeight: CGFloat = 0 | ||||
|         var totalHeight: CGFloat = 0 | ||||
|  | ||||
|         for size in sizes { | ||||
|             if currentX + size.width > containerWidth { | ||||
|                 // New line | ||||
|                 currentX = 0 | ||||
|                 currentY += lineHeight + spacing | ||||
|                 totalHeight = currentY + size.height | ||||
|                 lineHeight = 0 | ||||
|             } | ||||
|  | ||||
|             currentX += size.width + spacing | ||||
|             lineHeight = max(lineHeight, size.height) | ||||
|         } | ||||
|         totalHeight = currentY + lineHeight | ||||
|  | ||||
|         return CGSize(width: containerWidth, height: totalHeight) | ||||
|     } | ||||
|  | ||||
|     func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { | ||||
|         let containerWidth = bounds.width | ||||
|         let sizes = subviews.map { $0.sizeThatFits(.unspecified) } | ||||
|  | ||||
|         var currentX: CGFloat = 0 | ||||
|         var currentY: CGFloat = 0 | ||||
|         var lineHeight: CGFloat = 0 | ||||
|         var lineElements: [(offset: Int, size: CGSize)] = [] | ||||
|  | ||||
|         func placeLine() { | ||||
|             let lineWidth = lineElements.map { $0.size.width }.reduce(0, +) + CGFloat(lineElements.count - 1) * spacing | ||||
|             var startX: CGFloat = 0 | ||||
|             switch alignment { | ||||
|             case .leading: | ||||
|                 startX = bounds.minX | ||||
|             case .center: | ||||
|                 startX = bounds.minX + (containerWidth - lineWidth) / 2 | ||||
|             case .trailing: | ||||
|                 startX = bounds.maxX - lineWidth | ||||
|             default: | ||||
|                 startX = bounds.minX | ||||
|             } | ||||
|  | ||||
|             var xOffset = startX | ||||
|             for (offset, size) in lineElements { | ||||
|                 subviews[offset].place(at: CGPoint(x: xOffset, y: bounds.minY + currentY), proposal: ProposedViewSize(size)) // Use bounds.minY + currentY | ||||
|                 xOffset += size.width + spacing | ||||
|             } | ||||
|             lineElements.removeAll() // Clear elements for the next line | ||||
|         } | ||||
|  | ||||
|         for (offset, size) in sizes.enumerated() { | ||||
|             if currentX + size.width > containerWidth && !lineElements.isEmpty { | ||||
|                 // New line | ||||
|                 placeLine() | ||||
|                 currentX = 0 | ||||
|                 currentY += lineHeight + spacing | ||||
|                 lineHeight = 0 | ||||
|             } | ||||
|  | ||||
|             lineElements.append((offset, size)) | ||||
|             currentX += size.width + spacing | ||||
|             lineHeight = max(lineHeight, size.height) | ||||
|         } | ||||
|         placeLine() // Place the last line | ||||
|     } | ||||
| } | ||||
							
								
								
									
										365
									
								
								ios/WatchRunner Watch App/Models/Models.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,365 @@ | ||||
| //  Models.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| // MARK: - Models | ||||
|  | ||||
| struct AppToken: Codable { | ||||
|     let token: String | ||||
| } | ||||
|  | ||||
| struct SnActivity: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let type: String | ||||
|     let data: ActivityData? | ||||
|     let createdAt: Date | ||||
| } | ||||
|  | ||||
| enum ActivityData: Codable { | ||||
|     case post(SnPost) | ||||
|     case discovery(DiscoveryData) | ||||
|     case unknown | ||||
|  | ||||
|     init(from decoder: Decoder) throws { | ||||
|         let container = try decoder.singleValueContainer() | ||||
|         if let post = try? container.decode(SnPost.self) { | ||||
|             self = .post(post) | ||||
|             return | ||||
|         } | ||||
|         if let discoveryData = try? container.decode(DiscoveryData.self) { | ||||
|             self = .discovery(discoveryData) | ||||
|             return | ||||
|         } | ||||
|         self = .unknown | ||||
|     } | ||||
|  | ||||
|     func encode(to encoder: Encoder) throws { | ||||
|         // Not needed for decoding | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SnPost: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let title: String? | ||||
|     let content: String? | ||||
|     let publisher: SnPublisher | ||||
|     let attachments: [SnCloudFile] | ||||
|     let tags: [SnPostTag] | ||||
| } | ||||
|  | ||||
| struct DiscoveryData: Codable { | ||||
|     let items: [DiscoveryItem] | ||||
| } | ||||
|  | ||||
| struct DiscoveryItem: Codable, Identifiable { | ||||
|     var id = UUID() | ||||
|     let type: String | ||||
|     let data: DiscoveryItemData | ||||
|  | ||||
|     enum CodingKeys: String, CodingKey { | ||||
|         case type, data | ||||
|     } | ||||
| } | ||||
|  | ||||
| enum DiscoveryItemData: Codable { | ||||
|     case realm(SnRealm) | ||||
|     case publisher(SnPublisher) | ||||
|     case article(SnWebArticle) | ||||
|     case unknown | ||||
|  | ||||
|     init(from decoder: Decoder) throws { | ||||
|         let container = try decoder.singleValueContainer() | ||||
|         if let realm = try? container.decode(SnRealm.self) { | ||||
|             self = .realm(realm) | ||||
|             return | ||||
|         } | ||||
|         if let publisher = try? container.decode(SnPublisher.self) { | ||||
|             self = .publisher(publisher) | ||||
|             return | ||||
|         } | ||||
|         if let article = try? container.decode(SnWebArticle.self) { | ||||
|             self = .article(article) | ||||
|             return | ||||
|         } | ||||
|         self = .unknown | ||||
|     } | ||||
|  | ||||
|     func encode(to encoder: Encoder) throws { | ||||
|         // Not needed for decoding | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SnRealm: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let name: String | ||||
|     let description: String? | ||||
| } | ||||
|  | ||||
| struct SnPublisher: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let name: String | ||||
|     let nick: String? | ||||
|     let description: String? | ||||
|     let picture: SnCloudFile? | ||||
| } | ||||
|  | ||||
| struct SnCloudFile: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let mimeType: String? | ||||
| } | ||||
|  | ||||
| struct SnPostTag: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let slug: String | ||||
|     let name: String? | ||||
| } | ||||
|  | ||||
| struct SnWebArticle: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let title: String | ||||
|     let url: String | ||||
| } | ||||
|  | ||||
| struct SnNotification: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let topic: String | ||||
|     let title: String | ||||
|     let subtitle: String | ||||
|     let content: String | ||||
|     let meta: [String: AnyCodable]? | ||||
|     let priority: Int | ||||
|     let viewedAt: Date? | ||||
|     let accountId: String | ||||
|     let createdAt: Date | ||||
|     let updatedAt: Date | ||||
|     let deletedAt: Date? | ||||
|  | ||||
|     enum CodingKeys: String, CodingKey { | ||||
|         case id | ||||
|         case topic | ||||
|         case title | ||||
|         case subtitle | ||||
|         case content | ||||
|         case meta | ||||
|         case priority | ||||
|         case viewedAt = "viewedAt" | ||||
|         case accountId = "accountId" | ||||
|         case createdAt = "createdAt" | ||||
|         case updatedAt = "updatedAt" | ||||
|         case deletedAt = "deletedAt" | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct AnyCodable: Codable { | ||||
|     let value: Any | ||||
|  | ||||
|     init(_ value: Any) { | ||||
|         self.value = value | ||||
|     } | ||||
|  | ||||
|     init(from decoder: Decoder) throws { | ||||
|         let container = try decoder.singleValueContainer() | ||||
|         if let intValue = try? container.decode(Int.self) { | ||||
|             value = intValue | ||||
|         } else if let doubleValue = try? container.decode(Double.self) { | ||||
|             value = doubleValue | ||||
|         } else if let boolValue = try? container.decode(Bool.self) { | ||||
|             value = boolValue | ||||
|         } else if let stringValue = try? container.decode(String.self) { | ||||
|             value = stringValue | ||||
|         } else if let arrayValue = try? container.decode([AnyCodable].self) { | ||||
|             value = arrayValue | ||||
|         } else if let dictValue = try? container.decode([String: AnyCodable].self) { | ||||
|             value = dictValue | ||||
|         } else { | ||||
|             value = NSNull() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func encode(to encoder: Encoder) throws { | ||||
|         var container = encoder.singleValueContainer() | ||||
|         switch value { | ||||
|         case let intValue as Int: | ||||
|             try container.encode(intValue) | ||||
|         case let doubleValue as Double: | ||||
|             try container.encode(doubleValue) | ||||
|         case let boolValue as Bool: | ||||
|             try container.encode(boolValue) | ||||
|         case let stringValue as String: | ||||
|             try container.encode(stringValue) | ||||
|         case let arrayValue as [AnyCodable]: | ||||
|             try container.encode(arrayValue) | ||||
|         case let dictValue as [String: AnyCodable]: | ||||
|             try container.encode(dictValue) | ||||
|         default: | ||||
|             try container.encodeNil() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct NotificationResponse { | ||||
|     let notifications: [SnNotification] | ||||
|     let total: Int | ||||
|     let hasMore: Bool | ||||
| } | ||||
|  | ||||
| struct ActivityResponse { | ||||
|     let activities: [SnActivity] | ||||
|     let hasMore: Bool | ||||
|     let nextCursor: String? | ||||
| } | ||||
|  | ||||
| struct SnAccount: Codable { | ||||
|     let id: String | ||||
|     let name: String | ||||
|     let nick: String | ||||
|     let profile: SnUserProfile | ||||
|     let createdAt: Date | ||||
| } | ||||
|  | ||||
| struct SnUserProfile: Codable { | ||||
|     let bio: String? | ||||
|     let picture: SnCloudFile? | ||||
|     let background: SnCloudFile? | ||||
|     let level: Int | ||||
|     let experience: Int | ||||
|     let levelingProgress: Double | ||||
| } | ||||
|  | ||||
| struct SnAccountStatus: Codable { | ||||
|     let id: String | ||||
|     let attitude: Int | ||||
|     let isOnline: Bool | ||||
|     let isInvisible: Bool | ||||
|     let isNotDisturb: Bool | ||||
|     let isCustomized: Bool | ||||
|     let label: String | ||||
|     let meta: [String: AnyCodable]? | ||||
|     let clearedAt: Date? | ||||
|     let accountId: String | ||||
|     let createdAt: Date | ||||
|     let updatedAt: Date | ||||
|     let deletedAt: Date? | ||||
| } | ||||
|  | ||||
| // MARK: - Chat Models | ||||
|  | ||||
| struct SnChatRoom: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let name: String? | ||||
|     let description: String? | ||||
|     let type: Int | ||||
|     let isPublic: Bool | ||||
|     let isCommunity: Bool | ||||
|     let picture: SnCloudFile? | ||||
|     let background: SnCloudFile? | ||||
|     let realmId: String? | ||||
|     let realm: SnRealm? | ||||
|     let createdAt: Date | ||||
|     let updatedAt: Date | ||||
|     let deletedAt: Date? | ||||
|     let members: [SnChatMember]? | ||||
| } | ||||
|  | ||||
| struct SnChatMessage: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let type: String | ||||
|     let content: String? | ||||
|     let nonce: String? | ||||
|     let meta: [String: AnyCodable] | ||||
|     let membersMentioned: [String]? | ||||
|     let editedAt: Date? | ||||
|     let attachments: [SnCloudFile] | ||||
|     let reactions: [SnChatReaction] | ||||
|     let repliedMessageId: String? | ||||
|     let forwardedMessageId: String? | ||||
|     let senderId: String | ||||
|     let sender: SnChatMember | ||||
|     let chatRoomId: String | ||||
|     let createdAt: Date | ||||
|     let updatedAt: Date | ||||
|     let deletedAt: Date? | ||||
|  | ||||
|     enum CodingKeys: String, CodingKey { | ||||
|         case id, type, content, nonce, meta, membersMentioned, editedAt, attachments, reactions, repliedMessageId, forwardedMessageId, senderId, sender, chatRoomId, createdAt, updatedAt, deletedAt | ||||
|     } | ||||
|  | ||||
|     init(from decoder: Decoder) throws { | ||||
|         let container = try decoder.container(keyedBy: CodingKeys.self) | ||||
|         id = try container.decode(String.self, forKey: .id) | ||||
|         type = try container.decode(String.self, forKey: .type) | ||||
|         content = try container.decodeIfPresent(String.self, forKey: .content) | ||||
|         nonce = try container.decodeIfPresent(String.self, forKey: .nonce) | ||||
|         meta = try container.decode([String: AnyCodable].self, forKey: .meta) | ||||
|         membersMentioned = try container.decodeIfPresent([String].self, forKey: .membersMentioned) ?? [] | ||||
|         editedAt = try container.decodeIfPresent(Date.self, forKey: .editedAt) | ||||
|         attachments = try container.decode([SnCloudFile].self, forKey: .attachments) | ||||
|         reactions = try container.decode([SnChatReaction].self, forKey: .reactions) | ||||
|         repliedMessageId = try container.decodeIfPresent(String.self, forKey: .repliedMessageId) | ||||
|         forwardedMessageId = try container.decodeIfPresent(String.self, forKey: .forwardedMessageId) | ||||
|         senderId = try container.decode(String.self, forKey: .senderId) | ||||
|         sender = try container.decode(SnChatMember.self, forKey: .sender) | ||||
|         chatRoomId = try container.decode(String.self, forKey: .chatRoomId) | ||||
|         createdAt = try container.decode(Date.self, forKey: .createdAt) | ||||
|         updatedAt = try container.decode(Date.self, forKey: .updatedAt) | ||||
|         deletedAt = try container.decodeIfPresent(Date.self, forKey: .deletedAt) | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SnChatReaction: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let messageId: String | ||||
|     let senderId: String | ||||
|     let sender: SnChatMember | ||||
|     let symbol: String | ||||
|     let attitude: Int | ||||
|     let createdAt: Date | ||||
|     let updatedAt: Date | ||||
|     let deletedAt: Date? | ||||
| } | ||||
|  | ||||
| struct SnChatMember: Codable, Identifiable { | ||||
|     let id: String | ||||
|     let chatRoomId: String | ||||
|     let chatRoom: SnChatRoom? | ||||
|     let accountId: String | ||||
|     let account: SnAccount | ||||
|     let nick: String? | ||||
|     let role: Int | ||||
|     let notify: Int | ||||
|     let joinedAt: Date? | ||||
|     let breakUntil: Date? | ||||
|     let timeoutUntil: Date? | ||||
|     let isBot: Bool | ||||
|     let status: SnAccountStatus? | ||||
|     let createdAt: Date | ||||
|     let updatedAt: Date | ||||
|     let deletedAt: Date? | ||||
| } | ||||
|  | ||||
| struct SnChatSummary: Codable { | ||||
|     let unreadCount: Int | ||||
|     let lastMessage: SnChatMessage? | ||||
| } | ||||
|  | ||||
| struct ChatRoomsResponse { | ||||
|     let rooms: [SnChatRoom] | ||||
| } | ||||
|  | ||||
| struct ChatInvitesResponse { | ||||
|     let invites: [SnChatMember] | ||||
| } | ||||
|  | ||||
| struct MessageSyncResponse: Codable { | ||||
|     let messages: [SnChatMessage] | ||||
|     let currentTimestamp: Date | ||||
|  | ||||
|     enum CodingKeys: String, CodingKey { | ||||
|         case messages | ||||
|         case currentTimestamp = "current_timestamp" | ||||
|     } | ||||
| } | ||||
							
								
								
									
										95
									
								
								ios/WatchRunner Watch App/Services/ImageLoader.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,95 @@ | ||||
| // | ||||
| //  ImageLoader.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
| import KingfisherWebP | ||||
| import Combine | ||||
|  | ||||
| // MARK: - Image Loader | ||||
|  | ||||
| @MainActor | ||||
| class ImageLoader: ObservableObject { | ||||
|     @Published var image: Image? | ||||
|     @Published var errorMessage: String? | ||||
|     @Published var isLoading = false | ||||
|  | ||||
|     private var currentTask: DownloadTask? | ||||
|  | ||||
|     init() {} | ||||
|  | ||||
|     deinit { | ||||
|         currentTask?.cancel() | ||||
|     } | ||||
|  | ||||
|     func loadImage(from initialUrl: URL, token: String) async { | ||||
|         isLoading = true | ||||
|         errorMessage = nil | ||||
|         image = nil | ||||
|  | ||||
|         // Create request modifier for authorization | ||||
|         let modifier = AnyModifier { request in | ||||
|             var r = request | ||||
|             r.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|             r.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|             return r | ||||
|         } | ||||
|  | ||||
|         // Use WebP processor as default since the app seems to handle WebP images | ||||
|         let processor = WebPProcessor.default | ||||
|  | ||||
|         // Use KingfisherManager to retrieve image with caching | ||||
|         currentTask = KingfisherManager.shared.retrieveImage( | ||||
|             with: initialUrl, | ||||
|             options: [ | ||||
|                 .requestModifier(modifier), | ||||
|                 .processor(processor), | ||||
|                 .cacheOriginalImage, // Cache the original image data | ||||
|                 .loadDiskFileSynchronously // Load from disk cache synchronously if available | ||||
|             ] | ||||
|         ) { [weak self] result in | ||||
|             guard let self = self else { return } | ||||
|  | ||||
|             Task { @MainActor in | ||||
|                 switch result { | ||||
|                 case .success(let value): | ||||
|                     self.image = Image(uiImage: value.image) | ||||
|                     self.isLoading = false | ||||
|                 case .failure(_): | ||||
|                     // If WebP processor fails (likely due to format), try with default processor | ||||
|                     let defaultProcessor = DefaultImageProcessor.default | ||||
|                     self.currentTask = KingfisherManager.shared.retrieveImage( | ||||
|                         with: initialUrl, | ||||
|                         options: [ | ||||
|                             .requestModifier(modifier), | ||||
|                             .processor(defaultProcessor), | ||||
|                             .cacheOriginalImage, | ||||
|                             .loadDiskFileSynchronously | ||||
|                         ] | ||||
|                     ) { [weak self] fallbackResult in | ||||
|                         guard let self = self else { return } | ||||
|  | ||||
|                         Task { @MainActor in | ||||
|                             switch fallbackResult { | ||||
|                             case .success(let value): | ||||
|                                 self.image = Image(uiImage: value.image) | ||||
|                             case .failure(let fallbackError): | ||||
|                                 self.errorMessage = fallbackError.localizedDescription | ||||
|                                 print("[watchOS] Image loading failed: \(fallbackError.localizedDescription)") | ||||
|                             } | ||||
|                             self.isLoading = false | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func cancel() { | ||||
|         currentTask?.cancel() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										637
									
								
								ios/WatchRunner Watch App/Services/NetworkService.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,637 @@ | ||||
| // | ||||
| //  NetworkService.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. // | ||||
|  | ||||
| import Combine | ||||
| import Foundation | ||||
|  | ||||
| // MARK: - WebSocket Data Structures | ||||
|  | ||||
| enum WebSocketState: Equatable { | ||||
|     case connected | ||||
|     case connecting | ||||
|     case disconnected | ||||
|     case serverDown | ||||
|     case duplicateDevice | ||||
|     case error(String) | ||||
|      | ||||
|     // Equatable conformance | ||||
|     static func == (lhs: WebSocketState, rhs: WebSocketState) -> Bool { | ||||
|         switch (lhs, rhs) { | ||||
|         case (.connected, .connected), | ||||
|             (.connecting, .connecting), | ||||
|             (.disconnected, .disconnected), | ||||
|             (.serverDown, .serverDown), | ||||
|             (.duplicateDevice, .duplicateDevice): | ||||
|             return true | ||||
|         case let (.error(a), .error(b)): | ||||
|             return a == b | ||||
|         default: | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct WebSocketPacket { | ||||
|     let type: String | ||||
|     let data: [String: Any]? | ||||
|     let endpoint: String? | ||||
|     let errorMessage: String? | ||||
| } | ||||
|  | ||||
| // MARK: - Network Service | ||||
|  | ||||
| class NetworkService { | ||||
|     private let session = URLSession.shared | ||||
|      | ||||
|     // Add a serial queue for WebSocket operations | ||||
|     private let webSocketQueue = DispatchQueue(label: "com.solian.websocketQueue") | ||||
|      | ||||
|     func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> ActivityResponse { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         var components = URLComponents(url: baseURL.appendingPathComponent("/sphere/activities"), resolvingAgainstBaseURL: false)! | ||||
|         var queryItems = [URLQueryItem(name: "take", value: "20")] | ||||
|         if filter.lowercased() != "explore" { | ||||
|             queryItems.append(URLQueryItem(name: "filter", value: filter.lowercased())) | ||||
|         } | ||||
|         if let cursor = cursor { | ||||
|             queryItems.append(URLQueryItem(name: "cursor", value: cursor)) | ||||
|         } | ||||
|         components.queryItems = queryItems | ||||
|          | ||||
|         var request = URLRequest(url: components.url!) | ||||
|         request.httpMethod = "GET" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|          | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, _) = try await session.data(for: request) | ||||
|          | ||||
|         let decoder = JSONDecoder() | ||||
|         decoder.dateDecodingStrategy = .iso8601 | ||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|          | ||||
|         let activities = try decoder.decode([SnActivity].self, from: data) | ||||
|          | ||||
|         let hasMore = (activities.first?.type ?? "empty") != "empty" | ||||
|         let nextCursor = activities.isEmpty ? nil : activities.map { $0.createdAt }.min()?.ISO8601Format() | ||||
|          | ||||
|         return ActivityResponse(activities: activities, hasMore: hasMore, nextCursor: nextCursor) | ||||
|     } | ||||
|      | ||||
|     func createPost(title: String, content: String, token: String, serverUrl: String) async throws { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/sphere/posts") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = "POST" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let body: [String: Any] = ["title": title, "content": content] | ||||
|         request.httpBody = try JSONSerialization.data(withJSONObject: body) | ||||
|          | ||||
|         let (data, response) = try await session.data(for: request) | ||||
|          | ||||
|         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 { | ||||
|             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||
|             print("[watchOS] createPost failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||
|             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func fetchNotifications(offset: Int = 0, take: Int = 20, token: String, serverUrl: String) async throws -> NotificationResponse { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         var components = URLComponents(url: baseURL.appendingPathComponent("/ring/notifications"), resolvingAgainstBaseURL: false)! | ||||
|         let queryItems = [URLQueryItem(name: "offset", value: String(offset)), URLQueryItem(name: "take", value: String(take))] | ||||
|         components.queryItems = queryItems | ||||
|          | ||||
|         var request = URLRequest(url: components.url!) | ||||
|         request.httpMethod = "GET" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, response) = try await session.data(for: request) | ||||
|          | ||||
|         let decoder = JSONDecoder() | ||||
|         decoder.dateDecodingStrategy = .iso8601 | ||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|          | ||||
|         let notifications = try decoder.decode([SnNotification].self, from: data) | ||||
|          | ||||
|         let httpResponse = response as? HTTPURLResponse | ||||
|         let total = Int(httpResponse?.value(forHTTPHeaderField: "X-Total") ?? "0") ?? 0 | ||||
|         let hasMore = offset + notifications.count < total | ||||
|          | ||||
|         return NotificationResponse(notifications: notifications, total: total, hasMore: hasMore) | ||||
|     } | ||||
|      | ||||
|     func fetchUserProfile(token: String, serverUrl: String) async throws -> SnAccount { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/pass/accounts/me") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = "GET" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, _) = try await session.data(for: request) | ||||
|          | ||||
|         let decoder = JSONDecoder() | ||||
|         decoder.dateDecodingStrategy = .iso8601 | ||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|          | ||||
|         return try decoder.decode(SnAccount.self, from: data) | ||||
|     } | ||||
|      | ||||
|     func fetchAccountStatus(token: String, serverUrl: String) async throws -> SnAccountStatus? { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = "GET" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, response) = try await session.data(for: request) | ||||
|          | ||||
|         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 { | ||||
|             return nil | ||||
|         } | ||||
|          | ||||
|         let decoder = JSONDecoder() | ||||
|         decoder.dateDecodingStrategy = .iso8601 | ||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|          | ||||
|         return try decoder.decode(SnAccountStatus.self, from: data) | ||||
|     } | ||||
|      | ||||
|     func createOrUpdateStatus(attitude: Int, isInvisible: Bool, isNotDisturb: Bool, label: String?, token: String, serverUrl: String) async throws -> SnAccountStatus { | ||||
|         // Check if there\'s already a customized status | ||||
|         let existingStatus = try? await fetchAccountStatus(token: token, serverUrl: serverUrl) | ||||
|         let method = (existingStatus?.isCustomized == true) ? "PATCH" : "POST" | ||||
|          | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = method | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         var body: [String: Any] = [ | ||||
|             "attitude": attitude, | ||||
|             "is_invisible": isInvisible, | ||||
|             "is_not_disturb": isNotDisturb, | ||||
|         ] | ||||
|          | ||||
|         if let label = label, !label.isEmpty { | ||||
|             body["label"] = label | ||||
|         } | ||||
|          | ||||
|         request.httpBody = try JSONSerialization.data(withJSONObject: body) | ||||
|          | ||||
|         let (data, response) = try await session.data(for: request) | ||||
|          | ||||
|         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 && httpResponse.statusCode != 200 { | ||||
|             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||
|             print("[watchOS] createOrUpdateStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||
|             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||
|         } | ||||
|          | ||||
|         let decoder = JSONDecoder() | ||||
|         decoder.dateDecodingStrategy = .iso8601 | ||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|          | ||||
|         return try decoder.decode(SnAccountStatus.self, from: data) | ||||
|     } | ||||
|      | ||||
|     func clearStatus(token: String, serverUrl: String) async throws { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = "DELETE" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, response) = try await session.data(for: request) | ||||
|          | ||||
|         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 { | ||||
|             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||
|             print("[watchOS] clearStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||
|             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - Chat API Methods | ||||
|      | ||||
|     func fetchChatRooms(token: String, serverUrl: String) async throws -> ChatRoomsResponse { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/sphere/chat") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = "GET" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, _) = try await session.data(for: request) | ||||
|          | ||||
|         let decoder = JSONDecoder() | ||||
|         decoder.dateDecodingStrategy = .iso8601 | ||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|          | ||||
|         let rooms = try decoder.decode([SnChatRoom].self, from: data) | ||||
|         return ChatRoomsResponse(rooms: rooms) | ||||
|     } | ||||
|      | ||||
|     func fetchChatRoom(identifier: String, token: String, serverUrl: String) async throws -> SnChatRoom { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/sphere/chat/\(identifier)") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = "GET" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, response) = try await session.data(for: request) | ||||
|          | ||||
|         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 { | ||||
|             throw URLError(.resourceUnavailable) | ||||
|         } | ||||
|          | ||||
|         let decoder = JSONDecoder() | ||||
|         decoder.dateDecodingStrategy = .iso8601 | ||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|          | ||||
|         return try decoder.decode(SnChatRoom.self, from: data) | ||||
|     } | ||||
|      | ||||
|     func fetchChatInvites(token: String, serverUrl: String) async throws -> ChatInvitesResponse { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/sphere/chat/invites") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = "GET" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, _) = try await session.data(for: request) | ||||
|          | ||||
|         let decoder = JSONDecoder() | ||||
|         decoder.dateDecodingStrategy = .iso8601 | ||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|          | ||||
|         let invites = try decoder.decode([SnChatMember].self, from: data) | ||||
|         return ChatInvitesResponse(invites: invites) | ||||
|     } | ||||
|      | ||||
|     func acceptChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/accept") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = "POST" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, response) = try await session.data(for: request) | ||||
|          | ||||
|         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { | ||||
|             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||
|             print("[watchOS] acceptChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||
|             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func declineChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|         let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/decline") | ||||
|          | ||||
|         var request = URLRequest(url: url) | ||||
|         request.httpMethod = "POST" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, response) = try await session.data(for: request) | ||||
|          | ||||
|         if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { | ||||
|             let responseBody = String(data: data, encoding: .utf8) ?? "" | ||||
|             print("[watchOS] declineChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)") | ||||
|             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - Message API Methods | ||||
|      | ||||
|     func fetchChatMessages(chatRoomId: String, token: String, serverUrl: String, before: Date? = nil, take: Int = 50) async throws -> [SnChatMessage] { | ||||
|         guard let baseURL = URL(string: serverUrl) else { | ||||
|             throw URLError(.badURL) | ||||
|         } | ||||
|          | ||||
|         // Try a different pattern: /sphere/chat/messages with roomId as query param | ||||
|         var components = URLComponents( | ||||
|             url: baseURL.appendingPathComponent("/sphere/chat/\(chatRoomId)/messages"), | ||||
|             resolvingAgainstBaseURL: false | ||||
|         )! | ||||
|         var queryItems = [ | ||||
|             URLQueryItem(name: "take", value: String(take)), | ||||
|         ] | ||||
|         if let before = before { | ||||
|             queryItems.append(URLQueryItem(name: "before", value: ISO8601DateFormatter().string(from: before))) | ||||
|         } | ||||
|         components.queryItems = queryItems | ||||
|          | ||||
|         var request = URLRequest(url: components.url!) | ||||
|         request.httpMethod = "GET" | ||||
|         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||
|         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||
|          | ||||
|         let (data, response) = try await session.data(for: request) | ||||
|          | ||||
|         if let httpResponse = response as? HTTPURLResponse { | ||||
|             _ = String(data: data, encoding: .utf8) ?? "Unable to decode response body" | ||||
|              | ||||
|             if httpResponse.statusCode != 200 { | ||||
|                 print("[watchOS] fetchChatMessages failed with status \(httpResponse.statusCode)") | ||||
|                 throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Check if data is empty | ||||
|         if data.isEmpty { | ||||
|             print("[watchOS] fetchChatMessages received empty response data") | ||||
|             return [] | ||||
|         } | ||||
|          | ||||
|         let decoder = JSONDecoder() | ||||
|         decoder.dateDecodingStrategy = .iso8601 | ||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|          | ||||
|         do { | ||||
|             let messages = try decoder.decode([SnChatMessage].self, from: data) | ||||
|             print("[watchOS] fetchChatMessages successfully decoded \(messages.count) messages") | ||||
|             return messages | ||||
|         } catch { | ||||
|             print("error: ", error) | ||||
|             throw error | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - WebSocket | ||||
|  | ||||
|     private var webSocketTask: URLSessionWebSocketTask? | ||||
|     private var heartbeatTimer: Timer? | ||||
|     private var reconnectTimer: Timer? | ||||
|     private var isDisconnectingManually = false | ||||
|  | ||||
|     private var lastToken: String? | ||||
|     private var lastServerUrl: String? | ||||
|  | ||||
|     private var heartbeatAt: Date? | ||||
|     var heartbeatDelay: TimeInterval? | ||||
|  | ||||
|     private let connectLock = NSLock() | ||||
|      | ||||
|     private let packetSubject = PassthroughSubject<WebSocketPacket, Error>() | ||||
|     private let stateSubject = CurrentValueSubject<WebSocketState, Never>(.disconnected) // Changed to CurrentValueSubject | ||||
|      | ||||
|     private var currentConnectionState: WebSocketState = .disconnected { // New property | ||||
|         didSet { | ||||
|             // Only send updates if the state has actually changed | ||||
|             if oldValue != currentConnectionState { | ||||
|                 stateSubject.send(currentConnectionState) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var packetStream: AnyPublisher<WebSocketPacket, Error> { | ||||
|         packetSubject.eraseToAnyPublisher() | ||||
|     } | ||||
|      | ||||
|     var stateStream: AnyPublisher<WebSocketState, Never> { | ||||
|         stateSubject.eraseToAnyPublisher() | ||||
|     } | ||||
|      | ||||
|     func connectWebSocket(token: String, serverUrl: String) { | ||||
|         webSocketQueue.async { [weak self] in | ||||
|             guard let self = self else { return } | ||||
|  | ||||
|             self.connectLock.lock() | ||||
|             defer { self.connectLock.unlock() } | ||||
|              | ||||
|             // Prevent redundant connection attempts | ||||
|             if self.currentConnectionState == .connecting || self.currentConnectionState == .connected { | ||||
|                 print("[WebSocket] Already connecting or connected, ignoring new connect request.") | ||||
|                 return | ||||
|             } | ||||
|              | ||||
|             self.currentConnectionState = .connecting | ||||
|  | ||||
|             // Ensure any existing task is cancelled before starting a new one | ||||
|             self.webSocketTask?.cancel(with: .goingAway, reason: nil) | ||||
|             self.webSocketTask = nil | ||||
|  | ||||
|             self.isDisconnectingManually = false // Reset this flag for a new connection attempt | ||||
|  | ||||
|             self.lastToken = token | ||||
|             self.lastServerUrl = serverUrl | ||||
|  | ||||
|             guard var urlComponents = URLComponents(string: serverUrl) else { | ||||
|                 self.currentConnectionState = .error("Invalid server URL") | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             urlComponents.scheme = urlComponents.scheme?.replacingOccurrences(of: "http", with: "ws") | ||||
|             urlComponents.path = "/ws" | ||||
|             urlComponents.queryItems = [URLQueryItem(name: "deviceAlt", value: "watch")] | ||||
|  | ||||
|             guard let url = urlComponents.url else { | ||||
|                 self.currentConnectionState = .error("Invalid WebSocket URL") | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             var request = URLRequest(url: url) | ||||
|             request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||
|             request.addValue("application/json", forHTTPHeaderField: "Content-Type") | ||||
|              | ||||
|             print("[WebSocket] Trying connecting to \(url)") | ||||
|              | ||||
|             self.webSocketTask = self.session.webSocketTask(with: request) | ||||
|             self.webSocketTask?.resume() | ||||
|  | ||||
|             self.listenForWebSocketMessages() | ||||
|             self.scheduleHeartbeat() | ||||
|             self.currentConnectionState = .connected | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func listenForWebSocketMessages() { | ||||
|         // Ensure webSocketTask is still valid before attempting to receive | ||||
|         guard let task = webSocketTask else { | ||||
|             print("[WebSocket] listenForWebSocketMessages: webSocketTask is nil, stopping listen.") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         task.receive { [weak self] result in | ||||
|             guard let self = self else { return } | ||||
|              | ||||
|             switch result { | ||||
|             case .failure(let error): | ||||
|                 print("[WebSocket] Error in receiving message: \(error)") | ||||
|                 // Only attempt to reconnect if not manually disconnecting | ||||
|                 if !self.isDisconnectingManually { | ||||
|                     self.currentConnectionState = .error(error.localizedDescription) | ||||
|                     self.scheduleReconnect() | ||||
|                 } else { | ||||
|                     // If manually disconnecting, just ensure state is disconnected | ||||
|                     self.currentConnectionState = .disconnected | ||||
|                 } | ||||
|             case .success(let message): | ||||
|                 switch message { | ||||
|                 case .string(let text): | ||||
|                     self.handleWebSocketMessage(text: text) | ||||
|                 case .data(let data): | ||||
|                     if let text = String(data: data, encoding: .utf8) { | ||||
|                         self.handleWebSocketMessage(text: text) | ||||
|                     } | ||||
|                 @unknown default: | ||||
|                     break | ||||
|                 } | ||||
|                 // Continue listening for next message only if task is still valid | ||||
|                 if self.webSocketTask === task { // Check if it's the same task | ||||
|                     self.listenForWebSocketMessages() | ||||
|                 } else { | ||||
|                     print("[WebSocket] listenForWebSocketMessages: Task changed, stopping listen for old task.") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func handleWebSocketMessage(text: String) { | ||||
|         guard let data = text.data(using: .utf8) else { | ||||
|             print("[WebSocket] Could not convert message to data") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         do { | ||||
|             if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], | ||||
|                let type = json["type"] as? String | ||||
|             { | ||||
|                 let packet = WebSocketPacket( | ||||
|                     type: type, | ||||
|                     data: json["data"] as? [String: Any], | ||||
|                     endpoint: json["endpoint"] as? String, | ||||
|                     errorMessage: json["errorMessage"] as? String | ||||
|                 ) | ||||
|                  | ||||
|                 print("[WebSocket] Received packet: \(packet.type) \(packet.errorMessage ?? "")") | ||||
|                  | ||||
|                 if packet.type == "error.dupe" { | ||||
|                     self.currentConnectionState = .duplicateDevice | ||||
|                     self.disconnectWebSocket() | ||||
|                     return | ||||
|                 } | ||||
|                  | ||||
|                 if packet.type == "pong" { | ||||
|                     if let beatAt = self.heartbeatAt { | ||||
|                         let now = Date() | ||||
|                         self.heartbeatDelay = now.timeIntervalSince(beatAt) | ||||
|                         print("[WebSocket] Server respond last heartbeat for \((self.heartbeatDelay ?? 0) * 1000) ms") | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 self.packetSubject.send(packet) | ||||
|             } | ||||
|         } catch { | ||||
|             print("[WebSocket] Could not parse message json: \(error.localizedDescription)") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func scheduleReconnect() { | ||||
|         reconnectTimer?.invalidate() | ||||
|         reconnectTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in | ||||
|             guard let self = self, let token = self.lastToken, let serverUrl = self.lastServerUrl else { return } | ||||
|             print("[WebSocket] Attempting to reconnect...") | ||||
|              | ||||
|             // No need to call disconnectWebSocket here, connectWebSocket will handle cancelling old task | ||||
|             self.isDisconnectingManually = false // Reset for the new connection attempt | ||||
|              | ||||
|             self.connectWebSocket(token: token, serverUrl: serverUrl) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func scheduleHeartbeat() { | ||||
|         heartbeatTimer?.invalidate() | ||||
|         heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in | ||||
|             self?.beatTheHeart() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func beatTheHeart() { | ||||
|         heartbeatAt = Date() | ||||
|         print("[WebSocket] We\'re beating the heart! \(String(describing: self.heartbeatAt))") | ||||
|         sendWebSocketMessage(message: "{\"type\":\"ping\"}") | ||||
|     } | ||||
|      | ||||
|     func sendWebSocketMessage(message: String) { | ||||
|         webSocketTask?.send(.string(message)) { error in | ||||
|             if let error = error { | ||||
|                 print("[WebSocket] Error sending message: \(error.localizedDescription)") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func disconnectWebSocket() { | ||||
|         isDisconnectingManually = true | ||||
|         reconnectTimer?.invalidate() | ||||
|         heartbeatTimer?.invalidate() | ||||
|          | ||||
|         // Cancel the task and then nil it out | ||||
|         webSocketTask?.cancel(with: .goingAway, reason: nil) | ||||
|         webSocketTask = nil // Set to nil immediately after cancelling | ||||
|          | ||||
|         self.currentConnectionState = .disconnected | ||||
|     } | ||||
| } | ||||
							
								
								
									
										58
									
								
								ios/WatchRunner Watch App/State/AppState.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | ||||
| // | ||||
| //  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 | ||||
|     @Published var errorMessage: String? = nil | ||||
|  | ||||
|     let networkService = NetworkService() | ||||
|     private var wcService = WatchConnectivityService() | ||||
|     private var cancellables = Set<AnyCancellable>() | ||||
|     private var hasAttemptedConnection = false | ||||
|  | ||||
|     init() { | ||||
|         wcService.$token.combineLatest(wcService.$serverUrl, wcService.$isFetched, wcService.$errorMessage) | ||||
|             .receive(on: DispatchQueue.main) | ||||
|             .sink { [weak self] (token: String?, serverUrl: String?, isFetched: Bool?, errorMessage: String?) in | ||||
|                 guard let self = self else { return } | ||||
|                  | ||||
|                 self.token = token | ||||
|                 self.serverUrl = serverUrl | ||||
|                 self.errorMessage = errorMessage | ||||
|  | ||||
|                 if let token = token, let serverUrl = serverUrl, !token.isEmpty, !serverUrl.isEmpty { | ||||
|                     self.isReady = true | ||||
|                     // Only connect once when we have valid credentials and tried fetch from phone | ||||
|                     if !self.hasAttemptedConnection && isFetched == true { | ||||
|                         self.hasAttemptedConnection = true | ||||
|                         print("[AppState] Connecting WebSocket to server: \(serverUrl)") | ||||
|                         self.networkService.connectWebSocket(token: token, serverUrl: serverUrl) | ||||
|                     } | ||||
|                 } else { | ||||
|                     self.isReady = false | ||||
|                     if self.hasAttemptedConnection { | ||||
|                         self.hasAttemptedConnection = false | ||||
|                         // Disconnect WebSocket if token or serverUrl become invalid | ||||
|                         self.networkService.disconnectWebSocket() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             .store(in: &cancellables) | ||||
|     } | ||||
|      | ||||
|     func requestData() { | ||||
|         wcService.requestDataFromPhone() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										113
									
								
								ios/WatchRunner Watch App/State/WatchConnectivityService.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,113 @@ | ||||
| import WatchConnectivity | ||||
| import Combine | ||||
| import Foundation | ||||
|  | ||||
| class WatchConnectivityService: NSObject, WCSessionDelegate, ObservableObject { | ||||
|     @Published var token: String? | ||||
|     @Published var serverUrl: String? | ||||
|     @Published var isFetched: Bool? | ||||
|     @Published var errorMessage: String? | ||||
|  | ||||
|     private let session: WCSession | ||||
|     private let userDefaults = UserDefaults.standard | ||||
|     private let tokenKey = "token" | ||||
|     private let serverUrlKey = "serverUrl" | ||||
|  | ||||
|     override init() { | ||||
|         self.session = .default | ||||
|         super.init() | ||||
|         print("[watchOS] Activating WCSession") | ||||
|         self.session.delegate = self | ||||
|         self.session.activate() | ||||
|          | ||||
|         // Load cached data | ||||
|         self.token = userDefaults.string(forKey: tokenKey) | ||||
|         self.serverUrl = userDefaults.string(forKey: serverUrlKey) | ||||
|         self.isFetched = false | ||||
|     } | ||||
|  | ||||
|     func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { | ||||
|         if let error = error { | ||||
|             print("[watchOS] WCSession activation failed with error: \(error.localizedDescription)") | ||||
|             DispatchQueue.main.async { | ||||
|                 self.errorMessage = "WCSession activation failed: \(error.localizedDescription)" | ||||
|             } | ||||
|             return | ||||
|         } | ||||
|         print("[watchOS] WCSession activated with state: \(activationState.rawValue)") | ||||
|         if activationState == .activated { | ||||
|             requestDataFromPhone() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { | ||||
|         print("[watchOS] Received application context: \(applicationContext)") | ||||
|         DispatchQueue.main.async { | ||||
|             if let token = applicationContext["token"] as? String { | ||||
|                 self.token = token | ||||
|                 self.userDefaults.set(token, forKey: self.tokenKey) | ||||
|             } | ||||
|             if let serverUrl = applicationContext["serverUrl"] as? String { | ||||
|                 self.serverUrl = serverUrl | ||||
|                 self.userDefaults.set(serverUrl, forKey: self.serverUrlKey) | ||||
|             } | ||||
|             self.isFetched = true | ||||
|             self.errorMessage = nil | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { | ||||
|         print("[watchOS] Received message: \(message)") | ||||
|         DispatchQueue.main.async { | ||||
|             if let token = message["token"] as? String { | ||||
|                 self.token = token | ||||
|                 self.userDefaults.set(token, forKey: self.tokenKey) | ||||
|             } | ||||
|             if let serverUrl = message["serverUrl"] as? String { | ||||
|                 self.serverUrl = serverUrl | ||||
|                 self.userDefaults.set(serverUrl, forKey: self.serverUrlKey) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func requestDataFromPhone() { | ||||
|     // Check if we already have valid data to avoid unnecessary requests | ||||
|     if let token = self.token, let serverUrl = self.serverUrl, !token.isEmpty, !serverUrl.isEmpty { | ||||
|         print("[watchOS] Skipped fetch - already have valid data") | ||||
|         self.isFetched = true | ||||
|         return | ||||
|     } | ||||
|      | ||||
|     guard session.activationState == .activated else { | ||||
|         print("[watchOS] Session not activated yet, state: \(session.activationState.rawValue)") | ||||
|         DispatchQueue.main.async { | ||||
|             self.errorMessage = "Session not ready yet" | ||||
|         } | ||||
|         return | ||||
|     } | ||||
|      | ||||
|     print("[watchOS] Requesting data from phone") | ||||
|     session.sendMessage(["request": "data"]) { [weak self] response in | ||||
|         guard let self = self else { return } | ||||
|         print("[watchOS] Received reply: \(response)") | ||||
|         DispatchQueue.main.async { | ||||
|             self.isFetched = true | ||||
|             if let token = response["token"] as? String { | ||||
|                 self.token = token | ||||
|                 self.userDefaults.set(token, forKey: self.tokenKey) | ||||
|             } | ||||
|             if let serverUrl = response["serverUrl"] as? String { | ||||
|                 self.serverUrl = serverUrl | ||||
|                 self.userDefaults.set(serverUrl, forKey: self.serverUrlKey) | ||||
|             } | ||||
|             self.errorMessage = nil // Clear any previous errors | ||||
|         } | ||||
|     } errorHandler: { error in | ||||
|         print("[watchOS] sendMessage failed with error: \(error.localizedDescription)") | ||||
|         DispatchQueue.main.async { | ||||
|             self.errorMessage = "Failed to get data from phone: \(error.localizedDescription)" | ||||
|             // Don't set isFetched = true on error - allow retry | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
							
								
								
									
										20
									
								
								ios/WatchRunner Watch App/Utils/AttachmentUtils.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | ||||
| // | ||||
| //  AttachmentUtils.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| // MARK: - Helper Functions | ||||
|  | ||||
| func getAttachmentUrl(for fileId: String, serverUrl: String) -> URL? { | ||||
|     let urlString: String | ||||
|     if fileId.starts(with: "http") { | ||||
|         urlString = fileId | ||||
|     } else { | ||||
|         urlString = "\(serverUrl)/drive/files/\(fileId)" | ||||
|     } | ||||
|     return URL(string: urlString) | ||||
| } | ||||
							
								
								
									
										73
									
								
								ios/WatchRunner Watch App/ViewModels/ActivityViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,73 @@ | ||||
| // | ||||
| //  ActivityViewModel.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Combine | ||||
|  | ||||
| // MARK: - View Models | ||||
|  | ||||
| @MainActor | ||||
| class ActivityViewModel: ObservableObject { | ||||
|     @Published var activities: [SnActivity] = [] | ||||
|     @Published var isLoading = false | ||||
|     @Published var isLoadingMore = false | ||||
|     @Published var errorMessage: String? | ||||
|     @Published var hasMore = false | ||||
|  | ||||
|     private let networkService = NetworkService() | ||||
|     let filter: String | ||||
|     private var isMock = false | ||||
|     private var hasFetched = false | ||||
|     private var nextCursor: String? | ||||
|  | ||||
|     init(filter: String, mockActivities: [SnActivity]? = nil) { | ||||
|         self.filter = filter | ||||
|         if let mockActivities = mockActivities { | ||||
|             self.activities = mockActivities | ||||
|             self.isMock = true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func fetchActivities(token: String, serverUrl: String) async { | ||||
|         if isMock || hasFetched { return } | ||||
|         guard !isLoading else { return } | ||||
|         isLoading = true | ||||
|         errorMessage = nil | ||||
|         hasFetched = true | ||||
|         nextCursor = nil | ||||
|  | ||||
|         do { | ||||
|             let response = try await networkService.fetchActivities(filter: filter, cursor: nil, token: token, serverUrl: serverUrl) | ||||
|             self.activities = response.activities | ||||
|             self.hasMore = response.hasMore | ||||
|             self.nextCursor = response.nextCursor | ||||
|         } catch { | ||||
|             self.errorMessage = error.localizedDescription | ||||
|             print("[watchOS] fetchActivities failed with error: \(error)") | ||||
|             hasFetched = false | ||||
|         } | ||||
|  | ||||
|         isLoading = false | ||||
|     } | ||||
|  | ||||
|     func loadMoreActivities(token: String, serverUrl: String) async { | ||||
|         guard !isLoadingMore && hasMore && nextCursor != nil else { return } | ||||
|         isLoadingMore = true | ||||
|  | ||||
|         do { | ||||
|             let response = try await networkService.fetchActivities(filter: filter, cursor: nextCursor, token: token, serverUrl: serverUrl) | ||||
|             self.activities.append(contentsOf: response.activities) | ||||
|             self.hasMore = response.hasMore | ||||
|             self.nextCursor = response.nextCursor | ||||
|         } catch { | ||||
|             self.errorMessage = error.localizedDescription | ||||
|             print("[watchOS] loadMoreActivities failed with error: \(error)") | ||||
|         } | ||||
|  | ||||
|         isLoadingMore = false | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,35 @@ | ||||
| // | ||||
| //  ComposePostViewModel.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Combine | ||||
|  | ||||
| @MainActor | ||||
| class ComposePostViewModel: ObservableObject { | ||||
|     @Published var title = "" | ||||
|     @Published var content = "" | ||||
|     @Published var isPosting = false | ||||
|     @Published var errorMessage: String? | ||||
|     @Published var didPost = false | ||||
|  | ||||
|     private let networkService = NetworkService() | ||||
|  | ||||
|     func createPost(token: String, serverUrl: String) async { | ||||
|         guard !isPosting else { return } | ||||
|         isPosting = true | ||||
|         errorMessage = nil | ||||
|  | ||||
|         do { | ||||
|             try await networkService.createPost(title: title, content: content, token: token, serverUrl: serverUrl) | ||||
|             didPost = true | ||||
|         } catch { | ||||
|             errorMessage = error.localizedDescription | ||||
|         } | ||||
|  | ||||
|         isPosting = false | ||||
|     } | ||||
| } | ||||
							
								
								
									
										284
									
								
								ios/WatchRunner Watch App/Views/AccountView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,284 @@ | ||||
| // | ||||
| //  AccountView.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/30. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct AccountView: View { | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @State private var user: SnAccount? | ||||
|     @State private var status: SnAccountStatus? | ||||
|     @State private var isLoading = false | ||||
|     @State private var error: Error? | ||||
|     @State private var showingClearConfirmation = false | ||||
|  | ||||
|     @StateObject private var profileImageLoader = ImageLoader() | ||||
|     @StateObject private var bannerImageLoader = ImageLoader() | ||||
|  | ||||
|     private let networkService = NetworkService() | ||||
|      | ||||
|     var body: some View { | ||||
|         ScrollView { | ||||
|             if isLoading { | ||||
|                 ProgressView() | ||||
|                     .padding() | ||||
|             } else if let error = error { | ||||
|                 VStack { | ||||
|                     Text("Failed to load account") | ||||
|                         .foregroundColor(.red) | ||||
|                     Text(error.localizedDescription) | ||||
|                         .font(.caption) | ||||
|                         .foregroundColor(.secondary) | ||||
|                 } | ||||
|                 .padding() | ||||
|             } else if let user = user { | ||||
|                 VStack(spacing: 16) { | ||||
|                     // Banner | ||||
|                     if user.profile.background != nil { | ||||
|                         if bannerImageLoader.isLoading { | ||||
|                             ProgressView() | ||||
|                                 .frame(height: 80) | ||||
|                         } else if let bannerImage = bannerImageLoader.image { | ||||
|                             bannerImage | ||||
|                                 .resizable() | ||||
|                                 .aspectRatio(contentMode: .fill) | ||||
|                                 .frame(height: 80) | ||||
|                                 .clipped() | ||||
|                                 .cornerRadius(8) | ||||
|                         } else if bannerImageLoader.errorMessage != nil { | ||||
|                             Rectangle() | ||||
|                                 .fill(Color.gray.opacity(0.3)) | ||||
|                                 .frame(height: 80) | ||||
|                                 .cornerRadius(8) | ||||
|                         } else { | ||||
|                             Rectangle() | ||||
|                                 .fill(Color.gray.opacity(0.3)) | ||||
|                                 .frame(height: 80) | ||||
|                                 .cornerRadius(8) | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     // Profile Picture | ||||
|                     HStack(spacing: 16) | ||||
|                     { | ||||
|                         if profileImageLoader.isLoading { | ||||
|                             ProgressView() | ||||
|                                 .frame(width: 60, height: 60) | ||||
|                         } else if let profileImage = profileImageLoader.image { | ||||
|                             profileImage | ||||
|                                 .resizable() | ||||
|                                 .frame(width: 60, height: 60) | ||||
|                                 .clipShape(Circle()) | ||||
|                         } else if profileImageLoader.errorMessage != nil { | ||||
|                             Circle() | ||||
|                                 .fill(Color.red.opacity(0.3)) | ||||
|                                 .frame(width: 60, height: 60) | ||||
|                                 .overlay( | ||||
|                                     Image(systemName: "exclamationmark.triangle") | ||||
|                                         .resizable() | ||||
|                                         .scaledToFit() | ||||
|                                         .foregroundColor(.red) | ||||
|                                 ) | ||||
|                         } else { | ||||
|                             Circle() | ||||
|                                 .fill(Color.gray.opacity(0.3)) | ||||
|                                 .frame(width: 60, height: 60) | ||||
|                                 .overlay( | ||||
|                                     Image(systemName: "person.circle.fill") | ||||
|                                         .resizable() | ||||
|                                         .scaledToFit() | ||||
|                                         .foregroundColor(.gray) | ||||
|                                 ) | ||||
|                         } | ||||
|                          | ||||
|                         // Username and Handle | ||||
|                         VStack(alignment: .leading) { | ||||
|                             Text(user.nick) | ||||
|                                 .font(.headline) | ||||
|                             Text("@\(user.name)") | ||||
|                                 .font(.caption) | ||||
|                                 .foregroundColor(.secondary) | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     // Status | ||||
|                     VStack(alignment: .leading, spacing: 8) { | ||||
|                         HStack { | ||||
|                             Text("Status") | ||||
|                                 .font(.subheadline) | ||||
|                                 .foregroundColor(.secondary) | ||||
|                             Spacer() | ||||
|                             if status?.isCustomized == true { | ||||
|                                 Button(action: { | ||||
|                                     showingClearConfirmation = true | ||||
|                                 }) { | ||||
|                                     ZStack { | ||||
|                                         Circle() | ||||
|                                             .fill(Color.red.opacity(0.1)) | ||||
|                                             .frame(width: 28, height: 28) | ||||
|                                         Image(systemName: "trash") | ||||
|                                             .foregroundColor(.red) | ||||
|                                     } | ||||
|                                 } | ||||
|                                 .buttonStyle(.plain) | ||||
|                                 .frame(width: 28, height: 28) | ||||
|                             } | ||||
|                             NavigationLink( | ||||
|                                 destination: StatusCreationView(initialStatus: status?.isCustomized == true ? status : nil) | ||||
|                                     .environmentObject(appState) | ||||
|                             ) { | ||||
|                                 ZStack { | ||||
|                                     Circle() | ||||
|                                         .fill(Color.blue.opacity(0.1)) | ||||
|                                         .frame(width: 28, height: 28) | ||||
|                                     Image(systemName: "pencil") | ||||
|                                         .foregroundColor(.blue) | ||||
|                                 } | ||||
|                             } | ||||
|                             .buttonStyle(.plain) | ||||
|                             .frame(width: 28, height: 28) | ||||
|                         } | ||||
|                          | ||||
|                         if let status = status { | ||||
|                             VStack(alignment: .leading, spacing: 4) { | ||||
|                                 HStack { | ||||
|                                     Circle() | ||||
|                                         .fill(status.isOnline ? Color.green : Color.gray) | ||||
|                                         .frame(width: 8, height: 8) | ||||
|                                     Text(status.label.isEmpty ? "No status" : status.label) | ||||
|                                         .font(.body) | ||||
|                                 } | ||||
|                                  | ||||
|                                 if status.isInvisible { | ||||
|                                     Text("Invisible") | ||||
|                                         .font(.caption) | ||||
|                                         .foregroundColor(.secondary) | ||||
|                                 } | ||||
|                                 if status.isNotDisturb { | ||||
|                                     Text("Do Not Disturb") | ||||
|                                         .font(.caption) | ||||
|                                         .foregroundColor(.secondary) | ||||
|                                 } | ||||
|                                 if let clearedAt = status.clearedAt { | ||||
|                                     Text("Clears: \(clearedAt.formatted(date: .abbreviated, time: .shortened))") | ||||
|                                         .font(.caption) | ||||
|                                         .foregroundColor(.secondary) | ||||
|                                 } | ||||
|                             } | ||||
|                         } else { | ||||
|                             Text("No status set") | ||||
|                                 .font(.body) | ||||
|                                 .foregroundColor(.secondary) | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     // Level and Progress | ||||
|                     VStack(alignment: .leading, spacing: 8) { | ||||
|                         Text("Level \(user.profile.level)") | ||||
|                             .font(.title3) | ||||
|                             .bold() | ||||
|                         ProgressView(value: user.profile.levelingProgress) | ||||
|                             .progressViewStyle(LinearProgressViewStyle()) | ||||
|                             .frame(height: 8) | ||||
|                         Text("Experience: \(user.profile.experience)") | ||||
|                             .font(.caption) | ||||
|                             .foregroundColor(.secondary) | ||||
|                     } | ||||
|                      | ||||
|                     // Bio | ||||
|                     if let bio = user.profile.bio, !bio.isEmpty { | ||||
|                         Text(bio) | ||||
|                             .font(.body) | ||||
|                             .multilineTextAlignment(.center) | ||||
|                             .foregroundColor(.secondary) | ||||
|                             .frame(alignment: .leading) | ||||
|                     } else { | ||||
|                         Text("No bio available") | ||||
|                             .font(.body) | ||||
|                             .foregroundColor(.secondary) | ||||
|                             .frame(alignment: .leading) | ||||
|                     } | ||||
|                      | ||||
|                     // Member since | ||||
|                     Text("Joined at \(user.createdAt.formatted(.dateTime.month(.abbreviated).year()))") | ||||
|                         .font(.caption) | ||||
|                         .foregroundColor(.secondary) | ||||
|                         .frame(alignment: .leading) | ||||
|                 } | ||||
|                 .padding() | ||||
|                 // Load images when user data is available | ||||
|                 .task(id: user.profile.picture?.id) { | ||||
|                     if let serverUrl = appState.serverUrl, let pictureId = user.profile.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token { | ||||
|                         await profileImageLoader.loadImage(from: imageUrl, token: token) | ||||
|                     } | ||||
|                 } | ||||
|                 .task(id: user.profile.background?.id) { | ||||
|                     if let serverUrl = appState.serverUrl, let backgroundId = user.profile.background?.id, let imageUrl = getAttachmentUrl(for: backgroundId, serverUrl: serverUrl), let token = appState.token { | ||||
|                         await bannerImageLoader.loadImage(from: imageUrl, token: token) | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 Text("No account data") | ||||
|                     .padding() | ||||
|             } | ||||
|         } | ||||
|         .navigationTitle("Account") | ||||
|         .confirmationDialog("Clear Status", isPresented: $showingClearConfirmation) { | ||||
|             Button("Clear Status", role: .destructive) { | ||||
|                 Task { | ||||
|                     await clearStatus() | ||||
|                 } | ||||
|             } | ||||
|             Button("Cancel", role: .cancel) {} | ||||
|         } message: { | ||||
|             Text("Are you sure you want to clear your status? This action cannot be undone.") | ||||
|         } | ||||
|         .onAppear { | ||||
|             Task.detached { | ||||
|                 await loadUserProfile() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func loadUserProfile() async { | ||||
|         guard let token = appState.token, let serverUrl = appState.serverUrl else { | ||||
|             error = NSError(domain: "AccountView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"]) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         isLoading = true | ||||
|         error = nil | ||||
|  | ||||
|         do { | ||||
|             user = try await networkService.fetchUserProfile(token: token, serverUrl: serverUrl) | ||||
|             status = try await networkService.fetchAccountStatus(token: token, serverUrl: serverUrl) | ||||
|         } catch { | ||||
|             self.error = error | ||||
|         } | ||||
|  | ||||
|         isLoading = false | ||||
|     } | ||||
|  | ||||
|     private func clearStatus() async { | ||||
|         guard let token = appState.token, let serverUrl = appState.serverUrl else { | ||||
|             error = NSError(domain: "AccountView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"]) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         do { | ||||
|             try await networkService.clearStatus(token: token, serverUrl: serverUrl) | ||||
|             // Refresh status after clearing | ||||
|             status = try await networkService.fetchAccountStatus(token: token, serverUrl: serverUrl) | ||||
|         } catch { | ||||
|             self.error = error | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     AccountView() | ||||
|         .environmentObject(AppState()) | ||||
| } | ||||
							
								
								
									
										86
									
								
								ios/WatchRunner Watch App/Views/ActivityListView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,86 @@ | ||||
| // | ||||
| //  ActivityListView.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| // MARK: - Views | ||||
|  | ||||
| struct ActivityListView: View { | ||||
|     @StateObject private var viewModel: ActivityViewModel | ||||
|     @EnvironmentObject var appState: AppState | ||||
|  | ||||
|     init(filter: String, mockActivities: [SnActivity]? = nil) { | ||||
|         _viewModel = StateObject(wrappedValue: ActivityViewModel(filter: filter, mockActivities: mockActivities)) | ||||
|     } | ||||
|  | ||||
|     var body: some View { | ||||
|         Group { | ||||
|             if viewModel.isLoading { | ||||
|                 ProgressView() | ||||
|             } else if let errorMessage = viewModel.errorMessage { | ||||
|                 VStack { | ||||
|                     Text("Error fetching data") | ||||
|                         .font(.headline) | ||||
|                     Text(errorMessage) | ||||
|                         .font(.caption) | ||||
|                         .lineLimit(nil) | ||||
|                 } | ||||
|                 .padding() | ||||
|             } else if viewModel.activities.isEmpty { | ||||
|                 Text("No activities found.") | ||||
|             } else { | ||||
|                 List { | ||||
|                     ForEach(viewModel.activities) { activity in | ||||
|                         switch activity.type { | ||||
|                         case "posts.new", "posts.new.replies": | ||||
|                             if case .post(let post) = activity.data { | ||||
|                                 NavigationLink( | ||||
|                                     destination: PostDetailView(post: post).environmentObject(appState) | ||||
|                                 ) { | ||||
|                                     PostRowView(post: post) | ||||
|                                 } | ||||
|                             } | ||||
|                         case "discovery": | ||||
|                             if case .discovery(let discoveryData) = activity.data { | ||||
|                                 DiscoveryView(discoveryData: discoveryData) | ||||
|                             } | ||||
|                         default: | ||||
|                             Text("Unknown activity type: \(activity.type)") | ||||
|                         } | ||||
|                     } | ||||
|                     if viewModel.hasMore { | ||||
|                         if viewModel.isLoadingMore { | ||||
|                             HStack { | ||||
|                                 Spacer() | ||||
|                                 ProgressView() | ||||
|                                 Spacer() | ||||
|                             } | ||||
|                         } else { | ||||
|                             Button("Load More") { | ||||
|                                 Task { | ||||
|                                     if let token = appState.token, let serverUrl = appState.serverUrl { | ||||
|                                         await viewModel.loadMoreActivities(token: token, serverUrl: serverUrl) | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .onAppear { | ||||
|             if appState.isReady, let token = appState.token, let serverUrl = appState.serverUrl { | ||||
|                 Task.detached { | ||||
|                     await viewModel.fetchActivities(token: token, serverUrl: serverUrl) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .navigationTitle(viewModel.filter) | ||||
|         .navigationBarTitleDisplayMode(.inline) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										62
									
								
								ios/WatchRunner Watch App/Views/AppInfoHeaderView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,62 @@ | ||||
| // | ||||
| //  AppInfoHeader.swift | ||||
| //  Runner | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/30. | ||||
| // | ||||
|  | ||||
| import Combine | ||||
| import SwiftUI | ||||
|  | ||||
| struct AppInfoHeaderView : View { | ||||
|     @EnvironmentObject var appState: AppState // Access AppState | ||||
|     @State private var webSocketConnectionState: WebSocketState = .disconnected // New state for WebSocket status | ||||
|     @State private var cancellables = Set<AnyCancellable>() // For managing subscriptions | ||||
|  | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading) { | ||||
|             HStack(spacing: 12) { | ||||
|                 Image("Logo") | ||||
|                     .resizable() | ||||
|                     .frame(width: 40, height: 40) | ||||
|                  | ||||
|                 VStack(alignment: .leading) { | ||||
|                     Text("Solian").font(.headline) | ||||
|                     Text("for Apple Watch").font(.system(size: 11)) | ||||
|                      | ||||
|                     // Display WebSocket connection status | ||||
|                     Text(webSocketStatusMessage) | ||||
|                         .font(.system(size: 10)) | ||||
|                         .foregroundColor(.secondary) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .onAppear { | ||||
|             setupWebSocketListeners() | ||||
|         } | ||||
|         .onDisappear { | ||||
|             cancellables.forEach { $0.cancel() } | ||||
|             cancellables.removeAll() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private var webSocketStatusMessage: String { | ||||
|         switch webSocketConnectionState { | ||||
|         case .connected: return "Connected" | ||||
|         case .connecting: return "Connecting..." | ||||
|         case .disconnected: return "Disconnected" | ||||
|         case .serverDown: return "Server Down" | ||||
|         case .duplicateDevice: return "Duplicate Device" | ||||
|         case .error(let msg): return "Error: \(msg)" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func setupWebSocketListeners() { | ||||
|         appState.networkService.stateStream | ||||
|             .receive(on: DispatchQueue.main) | ||||
|             .sink { state in | ||||
|                 webSocketConnectionState = state | ||||
|             } | ||||
|             .store(in: &cancellables) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										109
									
								
								ios/WatchRunner Watch App/Views/AttachmentView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,109 @@ | ||||
| // | ||||
| //  AttachmentImageView.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import AVKit | ||||
| import AVFoundation | ||||
|  | ||||
| struct AttachmentView: View { | ||||
|     let attachment: SnCloudFile | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @StateObject private var imageLoader = ImageLoader() | ||||
|  | ||||
|     var body: some View { | ||||
|         Group { | ||||
|             if let mimeType = attachment.mimeType { | ||||
|                 if mimeType.starts(with: "image") { | ||||
|                     if let serverUrl = appState.serverUrl, let imageUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) { | ||||
|                         NavigationLink( | ||||
|                             destination: ImageViewer(imageUrl: imageUrl).environmentObject(appState) | ||||
|                         ) { | ||||
|                             if imageLoader.isLoading { | ||||
|                                 ProgressView() | ||||
|                             } else if let image = imageLoader.image { | ||||
|                                 image | ||||
|                                     .resizable() | ||||
|                                     .aspectRatio(contentMode: .fit) | ||||
|                                     .frame(maxWidth: .infinity) | ||||
|                                     .cornerRadius(8) | ||||
|                             } else if let errorMessage = imageLoader.errorMessage { | ||||
|                                 Text("Failed to load attachment: \(errorMessage)") | ||||
|                                     .font(.caption) | ||||
|                                     .foregroundColor(.red) | ||||
|                                     .cornerRadius(8) | ||||
|                             } else { | ||||
|                                 Text("File: \(attachment.id)") | ||||
|                                     .cornerRadius(8) | ||||
|                             } | ||||
|                         } | ||||
|                         .buttonStyle(PlainButtonStyle()) | ||||
|                     } else { | ||||
|                         Text("Image URL not available.") | ||||
|                     } | ||||
|                 } else if mimeType.starts(with: "video") { | ||||
|                     if let serverUrl = appState.serverUrl, let videoUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) { | ||||
|                         NavigationLink(destination: VideoPlayerView(videoUrl: videoUrl)) { | ||||
|                             if imageLoader.isLoading { | ||||
|                                 ProgressView() | ||||
|                             } else if let image = imageLoader.image { | ||||
|                                 ZStack { | ||||
|                                     image | ||||
|                                         .resizable() | ||||
|                                         .aspectRatio(contentMode: .fit) | ||||
|                                         .frame(maxWidth: .infinity) | ||||
|                                         .cornerRadius(8) | ||||
|  | ||||
|                                     Image(systemName: "play.circle.fill") | ||||
|                                         .resizable() | ||||
|                                         .scaledToFit() | ||||
|                                         .frame(width: 36, height: 36) | ||||
|                                         .foregroundColor(.white) | ||||
|                                         .shadow(color: .black.opacity(0.6), radius: 4, x: 0, y: 2) | ||||
|                                 } | ||||
|                             } else if imageLoader.errorMessage != nil { | ||||
|                                 Image(systemName: "play.rectangle.fill") | ||||
|                                     .resizable() | ||||
|                                     .aspectRatio(contentMode: .fit) | ||||
|                                     .frame(maxWidth: .infinity) | ||||
|                                     .foregroundColor(.gray) | ||||
|                                     .cornerRadius(8) | ||||
|                             } else { | ||||
|                                 ProgressView() | ||||
|                                     .cornerRadius(8) | ||||
|                             } | ||||
|                         } | ||||
|                         .buttonStyle(PlainButtonStyle()) | ||||
|                     } else { | ||||
|                         Text("Video URL not available.") | ||||
|                     } | ||||
|                 } else if mimeType.starts(with: "audio") { | ||||
|                     if let serverUrl = appState.serverUrl, let audioUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) { | ||||
|                         AudioPlayerView(audioUrl: audioUrl) | ||||
|                     } else { | ||||
|                         Text("Cannot play audio: URL not available.") | ||||
|                     } | ||||
|                 } else { | ||||
|                     Text("Unsupported media type: \(mimeType)") | ||||
|                 } | ||||
|             } else { | ||||
|                 Text("File: \(attachment.id) (No MIME type)") | ||||
|             } | ||||
|         } | ||||
|         .task(id: attachment.id) { | ||||
|             if let serverUrl = appState.serverUrl, let attachmentUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl), let token = appState.token { | ||||
|                 if attachment.mimeType?.starts(with: "image") == true { | ||||
|                     await imageLoader.loadImage(from: attachmentUrl, token: token) | ||||
|                 } | ||||
|                 if attachment.mimeType?.starts(with: "video") == true { | ||||
|                     let thumbnailUrl = attachmentUrl | ||||
|                         .appending(queryItems: [URLQueryItem(name: "thumbnail", value: "true")]) // Construct thumbnail URL | ||||
|                     await imageLoader.loadImage(from: thumbnailUrl, token: token) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										47
									
								
								ios/WatchRunner Watch App/Views/AudioPlayerView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | ||||
|  | ||||
| // | ||||
| //  AudioPlayerView.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import AVFoundation | ||||
|  | ||||
| struct AudioPlayerView: View { | ||||
|     let audioUrl: URL | ||||
|     @State private var player: AVPlayer? | ||||
|     @State private var isPlaying: Bool = false | ||||
|  | ||||
|     var body: some View { | ||||
|         VStack { | ||||
|             if player != nil { | ||||
|                 Button(action: togglePlayPause) { | ||||
|                     Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill") | ||||
|                         .font(.largeTitle) | ||||
|                 } | ||||
|                 .buttonStyle(.plain) | ||||
|             } else { | ||||
|                 Text("Loading audio...") | ||||
|             } | ||||
|         } | ||||
|         .onAppear { | ||||
|             player = AVPlayer(url: audioUrl) | ||||
|         } | ||||
|         .onDisappear { | ||||
|             player?.pause() | ||||
|             player = nil | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func togglePlayPause() { | ||||
|         guard let player = player else { return } | ||||
|         if isPlaying { | ||||
|             player.pause() | ||||
|         } else { | ||||
|             player.play() | ||||
|         } | ||||
|         isPlaying.toggle() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										785
									
								
								ios/WatchRunner Watch App/Views/ChatViews.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,785 @@ | ||||
| // | ||||
| //  ChatView.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/30. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ChatView: View { | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @State private var selectedTab = 0 | ||||
|     @State private var chatRooms: [SnChatRoom] = [] | ||||
|     @State private var chatInvites: [SnChatMember] = [] | ||||
|     @State private var isLoading = false | ||||
|     @State private var error: Error? | ||||
|     @State private var showingInvites = false | ||||
|  | ||||
|     private let tabs = ["All", "Direct", "Group"] | ||||
|  | ||||
|     var body: some View { | ||||
|         TabView(selection: $selectedTab) { | ||||
|             ForEach(0..<tabs.count, id: \.self) { index in | ||||
|                 VStack { | ||||
|                     if isLoading { | ||||
|                         ProgressView() | ||||
|                     } else if error != nil { | ||||
|                         VStack { | ||||
|                             Text("Error loading chats") | ||||
|                                 .font(.caption) | ||||
|                             Button("Retry") { | ||||
|                                 Task { | ||||
|                                     await loadChatRooms() | ||||
|                                 } | ||||
|                             } | ||||
|                             .font(.caption2) | ||||
|                         } | ||||
|                     } else { | ||||
|                         ChatRoomListView( | ||||
|                             chatRooms: filteredChatRooms(for: index), | ||||
|                             selectedTab: index | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 .tabItem { | ||||
|                     Text(tabs[index]) | ||||
|                 } | ||||
|                 .tag(index) | ||||
|             } | ||||
|         } | ||||
|         .tabViewStyle(.page) | ||||
|         .navigationTitle("Chat") | ||||
|         .toolbar { | ||||
|             ToolbarItem(placement: .topBarTrailing) { | ||||
|                 Button { | ||||
|                     showingInvites = true | ||||
|                 } label: { | ||||
|                     ZStack { | ||||
|                         Image(systemName: "envelope") | ||||
|                         if !chatInvites.isEmpty { | ||||
|                             Circle() | ||||
|                                 .fill(Color.red) | ||||
|                                 .frame(width: 8, height: 8) | ||||
|                                 .offset(x: 8, y: -8) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .sheet(isPresented: $showingInvites) { | ||||
|             ChatInvitesView(invites: $chatInvites, appState: appState) | ||||
|         } | ||||
|         .onAppear { | ||||
|             Task.detached { | ||||
|                 await loadChatRooms() | ||||
|                 await loadChatInvites() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func filteredChatRooms(for tabIndex: Int) -> [SnChatRoom] { | ||||
|         switch tabIndex { | ||||
|         case 0: // All | ||||
|             return chatRooms | ||||
|         case 1: // Direct | ||||
|             return chatRooms.filter { $0.type == 1 } | ||||
|         case 2: // Group | ||||
|             return chatRooms.filter { $0.type != 1 } | ||||
|         default: | ||||
|             return chatRooms | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func loadChatRooms() async { | ||||
|         guard let token = appState.token, let serverUrl = appState.serverUrl else { return } | ||||
|  | ||||
|         isLoading = true | ||||
|         error = nil | ||||
|  | ||||
|         do { | ||||
|             let response = try await appState.networkService.fetchChatRooms(token: token, serverUrl: serverUrl) | ||||
|             chatRooms = response.rooms | ||||
|         } catch { | ||||
|             self.error = error | ||||
|         } | ||||
|  | ||||
|         isLoading = false | ||||
|     } | ||||
|  | ||||
|     private func loadChatInvites() async { | ||||
|         guard let token = appState.token, let serverUrl = appState.serverUrl else { return } | ||||
|  | ||||
|         do { | ||||
|             let response = try await appState.networkService.fetchChatInvites(token: token, serverUrl: serverUrl) | ||||
|             chatInvites = response.invites | ||||
|         } catch { | ||||
|             // Handle error silently for invites | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ChatRoomListView: View { | ||||
|     let chatRooms: [SnChatRoom] | ||||
|     let selectedTab: Int | ||||
|  | ||||
|     var body: some View { | ||||
|         if chatRooms.isEmpty { | ||||
|             VStack { | ||||
|                 Image(systemName: "message") | ||||
|                     .font(.largeTitle) | ||||
|                     .foregroundColor(.secondary) | ||||
|                 Text("No chats yet") | ||||
|                     .font(.caption) | ||||
|                     .foregroundColor(.secondary) | ||||
|             } | ||||
|         } else { | ||||
|             List(chatRooms) { room in | ||||
|                 ChatRoomListItem(room: room) | ||||
|             } | ||||
|             .listStyle(.plain) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ChatRoomListItem: View { | ||||
|     let room: SnChatRoom | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @StateObject private var avatarLoader = ImageLoader() | ||||
|  | ||||
|     private var displayName: String { | ||||
|         if room.type == 1, let members = room.members, !members.isEmpty { | ||||
|             // For direct messages, show the other member's name | ||||
|             return members[0].account.nick | ||||
|         } else { | ||||
|             // For group chats, show room name or fallback | ||||
|             return room.name ?? "Group Chat" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private var subtitle: String { | ||||
|         if room.type == 1, let members = room.members, members.count > 1 { | ||||
|             // For direct messages, show member usernames | ||||
|             return members.map { "@\($0.account.name)" }.joined(separator: ", ") | ||||
|         } else if let description = room.description { | ||||
|             // For group chats with description | ||||
|             return description | ||||
|         } else { | ||||
|             // Fallback | ||||
|             return "" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private var avatarPictureId: String? { | ||||
|         if room.type == 1, let members = room.members, !members.isEmpty { | ||||
|             // For direct messages, use the other member's avatar | ||||
|             return members[0].account.profile.picture?.id | ||||
|         } else { | ||||
|             // For group chats, use room picture | ||||
|             return room.picture?.id | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var body: some View { | ||||
|         NavigationLink( | ||||
|             destination: ChatRoomView(room: room) | ||||
|                 .environmentObject(appState) | ||||
|         ) { | ||||
|             HStack { | ||||
|                 // Avatar using ImageLoader pattern | ||||
|                 Group { | ||||
|                     if avatarLoader.isLoading { | ||||
|                         ProgressView() | ||||
|                             .frame(width: 32, height: 32) | ||||
|                     } else if let image = avatarLoader.image { | ||||
|                         image | ||||
|                             .resizable() | ||||
|                             .frame(width: 32, height: 32) | ||||
|                             .clipShape(Circle()) | ||||
|                     } else if avatarLoader.errorMessage != nil { | ||||
|                         // Error state - show fallback | ||||
|                         Circle() | ||||
|                             .fill(Color.gray.opacity(0.3)) | ||||
|                             .frame(width: 32, height: 32) | ||||
|                             .overlay( | ||||
|                                 Text(displayName.prefix(1).uppercased()) | ||||
|                                     .font(.system(size: 12, weight: .medium)) | ||||
|                                     .foregroundColor(.primary) | ||||
|                             ) | ||||
|                     } else { | ||||
|                         // No image available - show initial | ||||
|                         Circle() | ||||
|                             .fill(Color.gray.opacity(0.3)) | ||||
|                             .frame(width: 32, height: 32) | ||||
|                             .overlay( | ||||
|                                 Text(displayName.prefix(1).uppercased()) | ||||
|                                     .font(.system(size: 12, weight: .medium)) | ||||
|                                     .foregroundColor(.primary) | ||||
|                             ) | ||||
|                     } | ||||
|                 } | ||||
|                 .task(id: avatarPictureId) { | ||||
|                     if let serverUrl = appState.serverUrl, | ||||
|                        let pictureId = avatarPictureId, | ||||
|                        let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), | ||||
|                        let token = appState.token { | ||||
|                         await avatarLoader.loadImage(from: imageUrl, token: token) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 VStack(alignment: .leading, spacing: 2) { | ||||
|                     Text(displayName) | ||||
|                         .font(.system(size: 14, weight: .medium)) | ||||
|                         .lineLimit(1) | ||||
|  | ||||
|                     if !subtitle.isEmpty { | ||||
|                         Text(subtitle) | ||||
|                             .font(.system(size: 12)) | ||||
|                             .foregroundColor(.secondary) | ||||
|                             .lineLimit(1) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 Spacer() | ||||
|  | ||||
|                 // Unread count badge placeholder | ||||
|                 // In a full implementation, this would show unread count | ||||
|             } | ||||
|             .padding(.vertical, 4) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| import Combine | ||||
| import SwiftUI | ||||
|  | ||||
| struct ChatRoomView: View { | ||||
|     let room: SnChatRoom | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @State private var messages: [SnChatMessage] = [] | ||||
|     @State private var isLoading = false | ||||
|     @State private var error: Error? | ||||
|     @State private var wsState: WebSocketState = .disconnected // New state for WebSocket status | ||||
|     @State private var hasLoadedMessages = false // Track if messages have been loaded | ||||
|     @State private var messageText = "" // Text input for sending messages | ||||
|     @State private var isSending = false // Track sending state | ||||
|     @State private var isInputHidden = false // Track if input should be hidden during scrolling | ||||
|     @State private var scrollTimer: Timer? // Timer to show input after scrolling stops | ||||
|  | ||||
|     @State private var cancellables = Set<AnyCancellable>() // For managing subscriptions | ||||
|  | ||||
|     var body: some View { | ||||
|         VStack { | ||||
|             // Display WebSocket connection status | ||||
|             if (wsState != .connected) | ||||
|             { | ||||
|                 Text(webSocketStatusMessage) | ||||
|                     .font(.caption2) | ||||
|                     .foregroundColor(.secondary) | ||||
|                     .padding(.vertical, 2) | ||||
|                     .animation(.easeInOut, value: wsState) // Animate status changes | ||||
|                     .transition(.opacity) | ||||
|             } | ||||
|  | ||||
|             if isLoading { | ||||
|                 ProgressView() | ||||
|             } else if error != nil { | ||||
|                 VStack { | ||||
|                     Text("Error loading messages") | ||||
|                         .font(.caption) | ||||
|                     Button("Retry") { | ||||
|                         Task { | ||||
|                             await loadMessages() | ||||
|                         } | ||||
|                     } | ||||
|                     .font(.caption2) | ||||
|                 } | ||||
|             } else if messages.isEmpty { | ||||
|                 VStack { | ||||
|                     Image(systemName: "bubble.left") | ||||
|                         .font(.largeTitle) | ||||
|                         .foregroundColor(.secondary) | ||||
|                     Text("No messages yet") | ||||
|                         .font(.caption) | ||||
|                         .foregroundColor(.secondary) | ||||
|                 } | ||||
|             } else { | ||||
|                 ScrollViewReader { scrollView in | ||||
|                     ScrollView { | ||||
|                         LazyVStack(alignment: .leading, spacing: 8) { | ||||
|                             ForEach(messages) { message in | ||||
|                                 ChatMessageItem(message: message) | ||||
|                             } | ||||
|                         } | ||||
|                         .padding(.horizontal) | ||||
|                         .padding(.vertical, 8) | ||||
|                         .padding(.bottom, 8) | ||||
|                     } | ||||
|                     .onAppear { | ||||
|                         // Scroll to bottom when messages load | ||||
|                         if let lastMessage = messages.last { | ||||
|                             scrollView.scrollTo(lastMessage.id, anchor: .bottom) | ||||
|                         } | ||||
|                     } | ||||
|                     .onChange(of: messages.count) { _, _ in | ||||
|                         // Scroll to bottom when new messages arrive | ||||
|                         if let lastMessage = messages.last { | ||||
|                             withAnimation { | ||||
|                                 scrollView.scrollTo(lastMessage.id, anchor: .bottom) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     .onScrollPhaseChange { _, phase  in | ||||
|                         switch phase { | ||||
|                         case .interacting: | ||||
|                             if !isInputHidden { | ||||
|                                 withAnimation(.easeOut(duration: 0.2)) { | ||||
|                                     isInputHidden = true | ||||
|                                 } | ||||
|                             } | ||||
|                         case .idle: | ||||
|                             withAnimation(.easeIn(duration: 0.3)) { | ||||
|                                 isInputHidden = false | ||||
|                             } | ||||
|                         default: break | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Message input area | ||||
|             if !isInputHidden { | ||||
|                 HStack(spacing: 8) { | ||||
|                     TextField("Send message...", text: $messageText) | ||||
|                         .font(.system(size: 14)) | ||||
|                         .disabled(isSending) | ||||
|                         .frame(height: 40) | ||||
|  | ||||
|                     Button { | ||||
|                         Task { | ||||
|                             await sendMessage() | ||||
|                         } | ||||
|                     } label: { | ||||
|                         if isSending { | ||||
|                             ProgressView() | ||||
|                                 .frame(width: 20, height: 20) | ||||
|                         } else { | ||||
|                             Image(systemName: "arrow.up.circle.fill") | ||||
|                                 .resizable() | ||||
|                                 .frame(width: 20, height: 20) | ||||
|                         } | ||||
|                     } | ||||
|                     .labelStyle(.iconOnly) | ||||
|                     .buttonStyle(.automatic) | ||||
|                     .disabled(messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSending) | ||||
|                     .frame(width: 40, height: 40) | ||||
|                 } | ||||
|                 .padding(.horizontal) | ||||
|                 .padding(.top, 8) | ||||
|                 .transition(.move(edge: .bottom).combined(with: .opacity)) | ||||
|             } | ||||
|         } | ||||
|         .navigationTitle(room.name ?? "Chat") | ||||
|         .task { | ||||
|             await loadMessages() | ||||
|         } | ||||
|         .onAppear { | ||||
|             setupWebSocketListeners() | ||||
|         } | ||||
|         .onDisappear { | ||||
|             cancellables.forEach { $0.cancel() } | ||||
|             cancellables.removeAll() | ||||
|             scrollTimer?.invalidate() | ||||
|             scrollTimer = nil | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private var webSocketStatusMessage: String { | ||||
|         switch wsState { | ||||
|         case .connected: return "Connected" | ||||
|         case .connecting: return "Connecting..." | ||||
|         case .disconnected: return "Disconnected" | ||||
|         case .serverDown: return "Server Down" | ||||
|         case .duplicateDevice: return "Duplicate Device" | ||||
|         case .error(let msg): return "Error: \(msg)" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func loadMessages() async { | ||||
|         // Prevent reloading if already loaded | ||||
|         guard !hasLoadedMessages else { return } | ||||
|  | ||||
|         guard let token = appState.token, let serverUrl = appState.serverUrl else { | ||||
|             isLoading = false | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         isLoading = true | ||||
|         error = nil | ||||
|  | ||||
|         do { | ||||
|             let messages = try await appState.networkService.fetchChatMessages( | ||||
|                 chatRoomId: room.id, | ||||
|                 token: token, | ||||
|                 serverUrl: serverUrl | ||||
|             ) | ||||
|             // Sort with newest messages first (for flipped list, newest will appear at bottom) | ||||
|             self.messages = messages.sorted { $0.createdAt < $1.createdAt } | ||||
|             hasLoadedMessages = true | ||||
|         } catch { | ||||
|             print("[watchOS] Error loading messages: \(error.localizedDescription)") | ||||
|             self.error = error | ||||
|         } | ||||
|  | ||||
|         isLoading = false | ||||
|     } | ||||
|  | ||||
|     private func sendMessage() async { | ||||
|         let content = messageText.trimmingCharacters(in: .whitespacesAndNewlines) | ||||
|         guard !content.isEmpty, | ||||
|               let token = appState.token, | ||||
|               let serverUrl = appState.serverUrl else { return } | ||||
|  | ||||
|         isSending = true | ||||
|  | ||||
|         do { | ||||
|             // Generate a nonce for the message | ||||
|             let nonce = UUID().uuidString | ||||
|  | ||||
|             // Prepare the request data | ||||
|             let messageData: [String: Any] = [ | ||||
|                 "content": content, | ||||
|                 "attachments_id": [], // Empty for now, can be extended for attachments | ||||
|                 "meta": [:], | ||||
|                 "nonce": nonce | ||||
|             ] | ||||
|  | ||||
|             // Create the URL | ||||
|             guard let url = URL(string: "\(serverUrl)/sphere/chat/\(room.id)/messages") else { | ||||
|                 throw URLError(.badURL) | ||||
|             } | ||||
|  | ||||
|             // Create the request | ||||
|             var request = URLRequest(url: url) | ||||
|             request.httpMethod = "POST" | ||||
|             request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") | ||||
|             request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||||
|             request.httpBody = try JSONSerialization.data(withJSONObject: messageData, options: []) | ||||
|  | ||||
|             // Send the request | ||||
|             let (data, response) = try await URLSession.shared.data(for: request) | ||||
|  | ||||
|             guard let httpResponse = response as? HTTPURLResponse, | ||||
|                   (200...299).contains(httpResponse.statusCode) else { | ||||
|                 throw URLError(.badServerResponse) | ||||
|             } | ||||
|  | ||||
|             // Parse the response to get the sent message | ||||
|             let decoder = JSONDecoder() | ||||
|             decoder.dateDecodingStrategy = .iso8601 | ||||
|             decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|             let sentMessage = try decoder.decode(SnChatMessage.self, from: data) | ||||
|  | ||||
|             // Add the message to the local list | ||||
|             messages.append(sentMessage) | ||||
|  | ||||
|             // Clear the input | ||||
|             messageText = "" | ||||
|  | ||||
|         } catch { | ||||
|             print("[watchOS] Error sending message: \(error.localizedDescription)") | ||||
|             // Could show an error alert here | ||||
|         } | ||||
|  | ||||
|         isSending = false | ||||
|     } | ||||
|  | ||||
|     private func sendReadReceipt() { | ||||
|         let data: [String: Any] = ["chat_room_id": room.id] | ||||
|         let packet: [String: Any] = ["type": "messages.read", "data": data, "endpoint": "sphere"] | ||||
|         if let jsonData = try? JSONSerialization.data(withJSONObject: packet, options: []), | ||||
|            let jsonString = String(data: jsonData, encoding: .utf8) { | ||||
|             appState.networkService.sendWebSocketMessage(message: jsonString) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func setupWebSocketListeners() { | ||||
|         // Listen for WebSocket packets (new messages) | ||||
|         appState.networkService.packetStream | ||||
|             .receive(on: DispatchQueue.main) // Ensure UI updates on main thread | ||||
|             .sink(receiveCompletion: { completion in | ||||
|                 if case .failure(let err) = completion { | ||||
|                     print("[ChatRoomView] WebSocket packet stream error: \(err.localizedDescription)") | ||||
|                 } | ||||
|             }, receiveValue: { packet in | ||||
|                 if ["messages.new", "messages.update", "messages.delete"].contains(packet.type), | ||||
|                    let messageData = packet.data { | ||||
|                     do { | ||||
|                         let jsonData = try JSONSerialization.data(withJSONObject: messageData, options: []) | ||||
|                         let decoder = JSONDecoder() | ||||
|                         decoder.dateDecodingStrategy = .iso8601 | ||||
|                         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||
|                         let message = try decoder.decode(SnChatMessage.self, from: jsonData) | ||||
|  | ||||
|                         if message.chatRoomId == room.id { | ||||
|                             switch packet.type { | ||||
|                             case "messages.new": | ||||
|                                 if message.type.hasPrefix("call") { | ||||
|                                     // TODO: Handle ongoing call | ||||
|                                 } | ||||
|                                 if !messages.contains(where: { $0.id == message.id }) { | ||||
|                                     messages.append(message) | ||||
|                                 } | ||||
|                                 sendReadReceipt() | ||||
|                             case "messages.update": | ||||
|                                 if let index = messages.firstIndex(where: { $0.id == message.id }) { | ||||
|                                     messages[index] = message | ||||
|                                 } | ||||
|                             case "messages.delete": | ||||
|                                 messages.removeAll(where: { $0.id == message.id }) | ||||
|                             default: | ||||
|                                 break | ||||
|                             } | ||||
|                         } | ||||
|                     } catch { | ||||
|                         print("[ChatRoomView] Error decoding message from websocket: \(error.localizedDescription)") | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|             .store(in: &cancellables) | ||||
|  | ||||
|         // Listen for WebSocket connection state changes | ||||
|         appState.networkService.stateStream | ||||
|             .receive(on: DispatchQueue.main) // Ensure UI updates on main thread | ||||
|             .sink { state in | ||||
|                 wsState = state | ||||
|             } | ||||
|             .store(in: &cancellables) | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ChatMessageItem: View { | ||||
|     let message: SnChatMessage | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @StateObject private var avatarLoader = ImageLoader() | ||||
|  | ||||
|     private var avatarPictureId: String? { | ||||
|         message.sender.account.profile.picture?.id | ||||
|     } | ||||
|  | ||||
|     var body: some View { | ||||
|         HStack(alignment: .top, spacing: 8) { | ||||
|             // Avatar | ||||
|             Group { | ||||
|                 if avatarLoader.isLoading { | ||||
|                     ProgressView() | ||||
|                         .frame(width: 24, height: 24) | ||||
|                 } else if let image = avatarLoader.image { | ||||
|                     image | ||||
|                         .resizable() | ||||
|                         .frame(width: 24, height: 24) | ||||
|                         .clipShape(Circle()) | ||||
|                 } else { | ||||
|                     Circle() | ||||
|                         .fill(Color.gray.opacity(0.3)) | ||||
|                         .frame(width: 24, height: 24) | ||||
|                         .overlay( | ||||
|                             Text(message.sender.account.nick.prefix(1).uppercased()) | ||||
|                                 .font(.system(size: 10, weight: .medium)) | ||||
|                                 .foregroundColor(.primary) | ||||
|                         ) | ||||
|                 } | ||||
|             } | ||||
|             .task(id: avatarPictureId) { | ||||
|                 if let serverUrl = appState.serverUrl, | ||||
|                    let pictureId = avatarPictureId, | ||||
|                    let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), | ||||
|                    let token = appState.token { | ||||
|                     await avatarLoader.loadImage(from: imageUrl, token: token) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             VStack(alignment: .leading, spacing: 4) { | ||||
|                 HStack { | ||||
|                     Text(message.sender.account.nick) | ||||
|                         .font(.system(size: 12, weight: .medium)) | ||||
|                     Spacer() | ||||
|                     Text(message.createdAt, style: .time) | ||||
|                         .font(.system(size: 10)) | ||||
|                         .foregroundColor(.secondary) | ||||
|                 } | ||||
|  | ||||
|                 if let content = message.content, !content.isEmpty { | ||||
|                     Text(content) | ||||
|                         .font(.system(size: 14)) | ||||
|                         .lineLimit(nil) | ||||
|                         .fixedSize(horizontal: false, vertical: true) | ||||
|                 } | ||||
|  | ||||
|                 if !message.attachments.isEmpty { | ||||
|                     AttachmentView(attachment: message.attachments[0]) | ||||
|                     if message.attachments.count > 1 { | ||||
|                         HStack(spacing: 8) { | ||||
|                             Image(systemName: "paperclip.circle.fill") | ||||
|                                 .frame(width: 12, height: 12) | ||||
|                                 .foregroundStyle(.gray) | ||||
|                             Text("\(message.attachments.count - 1)+ attachments") | ||||
|                                 .font(.footnote) | ||||
|                                 .foregroundStyle(.gray) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .padding(.vertical, 4) | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ChatInvitesView: View { | ||||
|     @Binding var invites: [SnChatMember] | ||||
|     let appState: AppState | ||||
|     @Environment(\.dismiss) private var dismiss | ||||
|     @State private var isLoading = false | ||||
|  | ||||
|     var body: some View { | ||||
|         NavigationView { | ||||
|             VStack { | ||||
|                 if invites.isEmpty { | ||||
|                     VStack { | ||||
|                         Image(systemName: "envelope.open") | ||||
|                             .font(.largeTitle) | ||||
|                             .foregroundColor(.secondary) | ||||
|                         Text("No invites") | ||||
|                             .font(.caption) | ||||
|                             .foregroundColor(.secondary) | ||||
|                     } | ||||
|                 } else { | ||||
|                     List(invites) { invite in | ||||
|                         ChatInviteItem(invite: invite, appState: appState, invites: $invites) | ||||
|                     } | ||||
|                     .listStyle(.plain) | ||||
|                 } | ||||
|             } | ||||
|             .navigationTitle("Invites") | ||||
|             .navigationBarTitleDisplayMode(.inline) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ChatInviteItem: View { | ||||
|     let invite: SnChatMember | ||||
|     let appState: AppState | ||||
|     @Binding var invites: [SnChatMember] | ||||
|     @State private var isAccepting = false | ||||
|     @State private var isDeclining = false | ||||
|  | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 8) { | ||||
|             HStack { | ||||
|                 Circle() | ||||
|                     .fill(Color.gray.opacity(0.3)) | ||||
|                     .frame(width: 24, height: 24) | ||||
|                     .overlay( | ||||
|                         Text((invite.chatRoom?.name ?? "C").prefix(1).uppercased()) | ||||
|                             .font(.system(size: 10, weight: .medium)) | ||||
|                             .foregroundColor(.primary) | ||||
|                     ) | ||||
|  | ||||
|                 VStack(alignment: .leading, spacing: 2) { | ||||
|                     Text(invite.chatRoom?.name ?? "Unknown Chat") | ||||
|                         .font(.system(size: 14, weight: .medium)) | ||||
|                         .lineLimit(1) | ||||
|  | ||||
|                     HStack(spacing: 4) { | ||||
|                         Text(invite.role == 100 ? "Owner" : invite.role >= 50 ? "Moderator" : "Member") | ||||
|                             .font(.system(size: 12)) | ||||
|                             .foregroundColor(.secondary) | ||||
|  | ||||
|                         if invite.chatRoom?.type == 1 { | ||||
|                             Text("Direct") | ||||
|                                 .font(.system(size: 12)) | ||||
|                                 .foregroundColor(.blue) | ||||
|                                 .padding(.horizontal, 4) | ||||
|                                 .padding(.vertical, 2) | ||||
|                                 .background(Color.blue.opacity(0.1)) | ||||
|                                 .cornerRadius(4) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 Spacer() | ||||
|             } | ||||
|  | ||||
|             HStack(spacing: 8) { | ||||
|                 Button { | ||||
|                     Task { | ||||
|                         await acceptInvite() | ||||
|                     } | ||||
|                 } label: { | ||||
|                     if isAccepting { | ||||
|                         ProgressView() | ||||
|                             .frame(width: 20, height: 20) | ||||
|                     } else { | ||||
|                         Image(systemName: "checkmark") | ||||
|                             .frame(width: 20, height: 20) | ||||
|                     } | ||||
|                 } | ||||
|                 .disabled(isAccepting || isDeclining) | ||||
|  | ||||
|                 Button { | ||||
|                     Task { | ||||
|                         await declineInvite() | ||||
|                     } | ||||
|                 } label: { | ||||
|                     if isDeclining { | ||||
|                         ProgressView() | ||||
|                             .frame(width: 20, height: 20) | ||||
|                     } else { | ||||
|                         Image(systemName: "xmark") | ||||
|                             .frame(width: 20, height: 20) | ||||
|                     } | ||||
|                 } | ||||
|                 .disabled(isAccepting || isDeclining) | ||||
|             } | ||||
|         } | ||||
|         .padding(.vertical, 8) | ||||
|     } | ||||
|  | ||||
|     private func acceptInvite() async { | ||||
|         guard let token = appState.token, | ||||
|               let serverUrl = appState.serverUrl, | ||||
|               let chatRoomId = invite.chatRoom?.id else { return } | ||||
|  | ||||
|         isAccepting = true | ||||
|  | ||||
|         do { | ||||
|             try await appState.networkService.acceptChatInvite(chatRoomId: chatRoomId, token: token, serverUrl: serverUrl) | ||||
|             // Remove from invites list | ||||
|             invites.removeAll { $0.id == invite.id } | ||||
|         } catch { | ||||
|             // Handle error - could show alert | ||||
|             print("Failed to accept invite: \(error)") | ||||
|         } | ||||
|  | ||||
|         isAccepting = false | ||||
|     } | ||||
|  | ||||
|     private func declineInvite() async { | ||||
|         guard let token = appState.token, | ||||
|               let serverUrl = appState.serverUrl, | ||||
|               let chatRoomId = invite.chatRoom?.id else { return } | ||||
|  | ||||
|         isDeclining = true | ||||
|  | ||||
|         do { | ||||
|             try await appState.networkService.declineChatInvite(chatRoomId: chatRoomId, token: token, serverUrl: serverUrl) | ||||
|             // Remove from invites list | ||||
|             invites.removeAll { $0.id == invite.id } | ||||
|         } catch { | ||||
|             // Handle error - could show alert | ||||
|             print("Failed to decline invite: \(error)") | ||||
|         } | ||||
|  | ||||
|         isDeclining = false | ||||
|     } | ||||
| } | ||||
							
								
								
									
										53
									
								
								ios/WatchRunner Watch App/Views/ComposePostView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,53 @@ | ||||
| // | ||||
| //  ComposePostView.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ComposePostView: View { | ||||
|     @StateObject private var viewModel = ComposePostViewModel() | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @Environment(\.dismiss) private var dismiss | ||||
|  | ||||
|     var body: some View { | ||||
|         NavigationStack { | ||||
|             Form { | ||||
|                 TextField("Title", text: $viewModel.title) | ||||
|                 TextField("Content", text: $viewModel.content) | ||||
|             } | ||||
|             .navigationTitle("New Post") | ||||
|             .toolbar { | ||||
|                 ToolbarItem(placement: .cancellationAction) { | ||||
|                     Button("Cancel", systemImage: "xmark") { | ||||
|                         dismiss() | ||||
|                     } | ||||
|                     .labelStyle(.iconOnly) | ||||
|                 } | ||||
|                 ToolbarItem(placement: .confirmationAction) { | ||||
|                     Button("Post", systemImage: "square.and.arrow.up") { | ||||
|                         Task { | ||||
|                             if let token = appState.token, let serverUrl = appState.serverUrl { | ||||
|                                 await viewModel.createPost(token: token, serverUrl: serverUrl) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     .labelStyle(.iconOnly) | ||||
|                     .disabled(viewModel.isPosting) | ||||
|                 } | ||||
|             } | ||||
|             .onChange(of: viewModel.didPost) { | ||||
|                 if viewModel.didPost { | ||||
|                     dismiss() | ||||
|                 } | ||||
|             } | ||||
|             .alert("Error", isPresented: .constant(viewModel.errorMessage != nil), actions: { | ||||
|                 Button("OK") { viewModel.errorMessage = nil } | ||||
|             }, message: { | ||||
|                 Text(viewModel.errorMessage ?? "") | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										110
									
								
								ios/WatchRunner Watch App/Views/DiscoveryViews.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,110 @@ | ||||
| // | ||||
| //  DiscoveryViews.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct DiscoveryView: View { | ||||
|     let discoveryData: DiscoveryData | ||||
|  | ||||
|     var body: some View { | ||||
|         NavigationLink(destination: DiscoveryDetailView(discoveryData: discoveryData)) { | ||||
|             VStack(alignment: .leading) { | ||||
|                 Text("Discovery") | ||||
|                     .font(.headline) | ||||
|                 Text("\(discoveryData.items.count) new items to discover") | ||||
|                     .font(.subheadline) | ||||
|                     .foregroundColor(.secondary) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct DiscoveryDetailView: View { | ||||
|     let discoveryData: DiscoveryData | ||||
|  | ||||
|     var body: some View { | ||||
|         List(discoveryData.items) { item in | ||||
|             NavigationLink(destination: destinationView(for: item)) { | ||||
|                 itemView(for: item) | ||||
|             } | ||||
|         } | ||||
|         .navigationTitle("Discovery") | ||||
|     } | ||||
|  | ||||
|     @ViewBuilder | ||||
|     private func itemView(for item: DiscoveryItem) -> some View { | ||||
|         VStack(alignment: .leading) { | ||||
|             switch item.data { | ||||
|             case .realm(let realm): | ||||
|                 Text("Realm").font(.headline) | ||||
|                 Text(realm.name).foregroundColor(.secondary) | ||||
|             case .publisher(let publisher): | ||||
|                 Text("Publisher").font(.headline) | ||||
|                 Text(publisher.name).foregroundColor(.secondary) | ||||
|             case .article(let article): | ||||
|                 Text("Article").font(.headline) | ||||
|                 Text(article.title).foregroundColor(.secondary) | ||||
|             case .unknown: | ||||
|                 Text("Unknown item") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     @ViewBuilder | ||||
|     private func destinationView(for item: DiscoveryItem) -> some View { | ||||
|         switch item.data { | ||||
|         case .realm(let realm): | ||||
|             RealmDetailView(realm: realm) | ||||
|         case .publisher(let publisher): | ||||
|             PublisherDetailView(publisher: publisher) | ||||
|         case .article(let article): | ||||
|             ArticleDetailView(article: article) | ||||
|         case .unknown: | ||||
|             Text("Detail view not available") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct RealmDetailView: View { | ||||
|     let realm: SnRealm | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 8) { | ||||
|             Text(realm.name).font(.headline) | ||||
|             if let description = realm.description { | ||||
|                 Text(description).font(.body) | ||||
|             } | ||||
|         } | ||||
|         .navigationTitle("Realm") | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct PublisherDetailView: View { | ||||
|     let publisher: SnPublisher | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 8) { | ||||
|             Text(publisher.name).font(.headline) | ||||
|             if let description = publisher.description { | ||||
|                 Text(description).font(.body) | ||||
|             } | ||||
|         } | ||||
|         .navigationTitle("Publisher") | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ArticleDetailView: View { | ||||
|     let article: SnWebArticle | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 8) { | ||||
|             Text(article.title).font(.headline) | ||||
|             Text(article.url).font(.caption).foregroundColor(.secondary) | ||||
|         } | ||||
|         .navigationTitle("Article") | ||||
|     } | ||||
| } | ||||
							
								
								
									
										67
									
								
								ios/WatchRunner Watch App/Views/ExploreView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,67 @@ | ||||
| // | ||||
| //  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 { | ||||
|     @EnvironmentObject private var appState: AppState | ||||
|     @State private var isComposing = false | ||||
|     @State private var selectedTab: String = "Explore" | ||||
|  | ||||
|     var body: some View { | ||||
|         NavigationStack { | ||||
|             if appState.isReady { | ||||
|                 TabView(selection: $selectedTab) { | ||||
|                     ActivityListView(filter: "Explore") | ||||
|                         .tag("Explore") | ||||
|                         .tabItem { | ||||
|                             Label("Explore", systemImage: "safari") | ||||
|                         } | ||||
|                         .labelStyle(.titleOnly) | ||||
|  | ||||
|                     ActivityListView(filter: "Subscriptions") | ||||
|                         .tag("Subscriptions") | ||||
|                         .tabItem { | ||||
|                             Label("Subscriptions", systemImage: "star") | ||||
|                         } | ||||
|                         .labelStyle(.titleOnly) | ||||
|  | ||||
|                     ActivityListView(filter: "Friends") | ||||
|                         .tag("Friends") | ||||
|                         .tabItem { | ||||
|                             Label("Friends", systemImage: "person.2") | ||||
|                         } | ||||
|                         .labelStyle(.titleOnly) | ||||
|                 } | ||||
|                 .navigationTitle(selectedTab) | ||||
|                 .toolbar { | ||||
|                     ToolbarItem(placement: .primaryAction) { | ||||
|                         Button(action: { isComposing = true }) { | ||||
|                             Label("Compose", systemImage: "plus") | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 VStack { | ||||
|                     ProgressView { Text("Syncing...") } | ||||
|                     Button("Retry") { | ||||
|                         appState.requestData() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .sheet(isPresented: $isComposing) { | ||||
|             ComposePostView() | ||||
|         } | ||||
|         .alert("Error", isPresented: .constant(appState.errorMessage != nil), actions: { | ||||
|             Button("OK") { appState.errorMessage = nil } | ||||
|         }, message: { | ||||
|             Text(appState.errorMessage ?? "") | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								ios/WatchRunner Watch App/Views/ImageViewer.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | ||||
| import SwiftUI | ||||
|  | ||||
| struct ImageViewer: View { | ||||
|     let imageUrl: URL | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @StateObject private var imageLoader = ImageLoader() | ||||
|  | ||||
|     var body: some View { | ||||
|         Group { | ||||
|             if imageLoader.isLoading { | ||||
|                 ProgressView() | ||||
|             } else if let image = imageLoader.image { | ||||
|                 image | ||||
|                     .resizable() | ||||
|                     .aspectRatio(contentMode: .fit) | ||||
|                     .frame(maxWidth: .infinity, maxHeight: .infinity) | ||||
|                     .scaledToFit() | ||||
|             } else if let errorMessage = imageLoader.errorMessage { | ||||
|                 Text("Failed to load image: \(errorMessage)") | ||||
|                     .font(.caption) | ||||
|                     .foregroundColor(.red) | ||||
|             } else { | ||||
|                 Text("Failed to load image.") | ||||
|             } | ||||
|         } | ||||
|         .task(id: imageUrl) { | ||||
|             if let token = appState.token { | ||||
|                 await imageLoader.loadImage(from: imageUrl, token: token) | ||||
|             } | ||||
|         } | ||||
|         .navigationTitle("Image") | ||||
|         .navigationBarTitleDisplayMode(.inline) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										198
									
								
								ios/WatchRunner Watch App/Views/NotificationView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,198 @@ | ||||
|  | ||||
| // | ||||
| //  NotificationView.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Combine | ||||
|  | ||||
| @MainActor | ||||
| class NotificationViewModel: ObservableObject { | ||||
|     @Published var notifications = [SnNotification]() | ||||
|     @Published var isLoading = false | ||||
|     @Published var isLoadingMore = false | ||||
|     @Published var errorMessage: String? | ||||
|     @Published var hasMore = false | ||||
|  | ||||
|     private let networkService = NetworkService() | ||||
|     private var hasFetched = false | ||||
|     private var offset = 0 | ||||
|     private let pageSize = 20 | ||||
|  | ||||
|     func fetchNotifications(token: String, serverUrl: String) async { | ||||
|         if hasFetched { return } | ||||
|         guard !isLoading else { return } | ||||
|         isLoading = true | ||||
|         errorMessage = nil | ||||
|         hasFetched = true | ||||
|         offset = 0 | ||||
|  | ||||
|         do { | ||||
|             let response = try await networkService.fetchNotifications(offset: offset, take: pageSize, token: token, serverUrl: serverUrl) | ||||
|             self.notifications = response.notifications | ||||
|             self.hasMore = response.hasMore | ||||
|             offset += response.notifications.count | ||||
|         } catch { | ||||
|             self.errorMessage = error.localizedDescription | ||||
|             print("[watchOS] fetchNotifications failed with error: \(error)") | ||||
|             hasFetched = false | ||||
|         } | ||||
|  | ||||
|         isLoading = false | ||||
|     } | ||||
|  | ||||
|     func loadMoreNotifications(token: String, serverUrl: String) async { | ||||
|         guard !isLoadingMore && hasMore else { return } | ||||
|         isLoadingMore = true | ||||
|  | ||||
|         do { | ||||
|             let response = try await networkService.fetchNotifications(offset: offset, take: pageSize, token: token, serverUrl: serverUrl) | ||||
|             self.notifications.append(contentsOf: response.notifications) | ||||
|             self.hasMore = response.hasMore | ||||
|             offset += response.notifications.count | ||||
|         } catch { | ||||
|             self.errorMessage = error.localizedDescription | ||||
|             print("[watchOS] loadMoreNotifications failed with error: \(error)") | ||||
|         } | ||||
|  | ||||
|         isLoadingMore = false | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct NotificationView: View { | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @StateObject private var viewModel = NotificationViewModel() | ||||
|  | ||||
|     var body: some View { | ||||
|         Group { | ||||
|             if viewModel.isLoading { | ||||
|                 ProgressView() | ||||
|             } else if let errorMessage = viewModel.errorMessage { | ||||
|                 VStack { | ||||
|                     Text("Error") | ||||
|                         .font(.headline) | ||||
|                     Text(errorMessage) | ||||
|                         .font(.caption) | ||||
|                     Button("Retry") { | ||||
|                         Task { | ||||
|                             if let token = appState.token, let serverUrl = appState.serverUrl { | ||||
|                                 await viewModel.fetchNotifications(token: token, serverUrl: serverUrl) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .padding() | ||||
|             } else if viewModel.notifications.isEmpty { | ||||
|                 Text("No notifications") | ||||
|             } else { | ||||
|                 List { | ||||
|                     ForEach(viewModel.notifications) { notification in | ||||
|                         NavigationLink(destination: NotificationDetailView(notification: notification)) { | ||||
|                             VStack(alignment: .leading, spacing: 4) { | ||||
|                                 HStack { | ||||
|                                     Text(notification.title) | ||||
|                                         .font(.headline) | ||||
|                                     Spacer() | ||||
|                                     if notification.viewedAt == nil { | ||||
|                                         Circle() | ||||
|                                             .fill(Color.blue) | ||||
|                                             .frame(width: 8, height: 8) | ||||
|                                     } | ||||
|                                 } | ||||
|                                 if !notification.subtitle.isEmpty { | ||||
|                                     Text(notification.subtitle) | ||||
|                                         .font(.subheadline) | ||||
|                                         .foregroundColor(.secondary) | ||||
|                                 } | ||||
|                                 if notification.content.count > 100 { | ||||
|                                     Text(notification.content.prefix(100) + "...") | ||||
|                                         .font(.caption) | ||||
|                                         .foregroundColor(.gray) | ||||
|                                         .lineLimit(2) | ||||
|                                 } else { | ||||
|                                     Text(notification.content) | ||||
|                                         .font(.caption) | ||||
|                                         .foregroundColor(.gray) | ||||
|                                         .lineLimit(2) | ||||
|                                 } | ||||
|                                 Text(notification.createdAt, style: .relative) | ||||
|                                     .font(.caption2) | ||||
|                                     .foregroundColor(.gray) | ||||
|                             } | ||||
|                             .padding(.vertical, 8) | ||||
|                         } | ||||
|                     } | ||||
|                     if viewModel.hasMore { | ||||
|                         if viewModel.isLoadingMore { | ||||
|                             HStack { | ||||
|                                 Spacer() | ||||
|                                 ProgressView() | ||||
|                                 Spacer() | ||||
|                             } | ||||
|                         } else { | ||||
|                             Button("Load More") { | ||||
|                                 Task { | ||||
|                                     if let token = appState.token, let serverUrl = appState.serverUrl { | ||||
|                                         await viewModel.loadMoreNotifications(token: token, serverUrl: serverUrl) | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .onAppear { | ||||
|             if appState.isReady, let token = appState.token, let serverUrl = appState.serverUrl { | ||||
|                 Task.detached { | ||||
|                     await viewModel.fetchNotifications(token: token, serverUrl: serverUrl) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .navigationTitle("Notifications") | ||||
|         .navigationBarTitleDisplayMode(.inline) | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct NotificationDetailView: View { | ||||
|     let notification: SnNotification | ||||
|  | ||||
|     var body: some View { | ||||
|         ScrollView { | ||||
|             VStack(alignment: .leading, spacing: 16) { | ||||
|                 Text(notification.title) | ||||
|                     .font(.headline) | ||||
|                  | ||||
|                 if !notification.subtitle.isEmpty { | ||||
|                     Text(notification.subtitle) | ||||
|                         .font(.subheadline) | ||||
|                         .foregroundColor(.secondary) | ||||
|                 } | ||||
|                  | ||||
|                 Text(notification.content) | ||||
|                     .font(.body) | ||||
|                  | ||||
|                 HStack { | ||||
|                     Text(notification.createdAt, style: .date) | ||||
|                     Text("·") | ||||
|                     Text(notification.createdAt, style: .time) | ||||
|                 } | ||||
|                 .font(.caption) | ||||
|                 .foregroundColor(.gray) | ||||
|                  | ||||
|                 if notification.viewedAt == nil { | ||||
|                     Text("Unread") | ||||
|                         .font(.caption) | ||||
|                         .foregroundColor(.blue) | ||||
|                 } | ||||
|             } | ||||
|             .padding() | ||||
|         } | ||||
|         .navigationTitle("Notification") | ||||
|         .navigationBarTitleDisplayMode(.inline) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										151
									
								
								ios/WatchRunner Watch App/Views/PostViews.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,151 @@ | ||||
| // | ||||
| //  PostViews.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/29. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct PostRowView: View { | ||||
|     let post: SnPost | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @StateObject private var imageLoader = ImageLoader() // Instantiate ImageLoader | ||||
|  | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 4) { | ||||
|             HStack { | ||||
|                 if imageLoader.isLoading { | ||||
|                     ProgressView() | ||||
|                         .frame(width: 24, height: 24) | ||||
|                 } else if let image = imageLoader.image { | ||||
|                     image | ||||
|                         .resizable() | ||||
|                         .frame(width: 24, height: 24) | ||||
|                         .clipShape(Circle()) | ||||
|                 } else if let errorMessage = imageLoader.errorMessage { | ||||
|                     Text("Failed: \(errorMessage)") | ||||
|                         .font(.caption) | ||||
|                         .foregroundColor(.red) | ||||
|                         .frame(width: 24, height: 24) | ||||
|                 } else { | ||||
|                     // Placeholder if no image and not loading | ||||
|                     Image(systemName: "person.circle.fill") | ||||
|                         .resizable() | ||||
|                         .frame(width: 24, height: 24) | ||||
|                         .clipShape(Circle()) | ||||
|                         .foregroundColor(.gray) | ||||
|                 } | ||||
|                 Text(post.publisher.nick ?? post.publisher.name) | ||||
|                     .font(.subheadline) | ||||
|                     .bold() | ||||
|             } | ||||
|             .task(id: post.publisher.picture?.id) { // Use task(id:) to reload image when pictureId changes | ||||
|                 if let serverUrl = appState.serverUrl, let pictureId = post.publisher.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token { | ||||
|                     await imageLoader.loadImage(from: imageUrl, token: token) | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             if let title = post.title, !title.isEmpty { | ||||
|                 Text(title) | ||||
|                     .font(.headline) | ||||
|             } | ||||
|              | ||||
|             if let content = post.content, !content.isEmpty { | ||||
|                 Text(content) | ||||
|                     .font(.body) | ||||
|             } | ||||
|              | ||||
|             if !post.attachments.isEmpty { | ||||
|                 AttachmentView(attachment: post.attachments[0]) | ||||
|                 if post.attachments.count > 1 { | ||||
|                     HStack(spacing: 8) { | ||||
|                         Image(systemName: "paperclip.circle.fill") | ||||
|                             .frame(width: 12, height: 12) | ||||
|                             .foregroundStyle(.gray) | ||||
|                         Text("\(post.attachments.count - 1)+ attachments") | ||||
|                             .font(.footnote) | ||||
|                             .foregroundStyle(.gray) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }.padding(.vertical) | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct PostDetailView: View { | ||||
|     let post: SnPost | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @StateObject private var publisherImageLoader = ImageLoader() // Instantiate ImageLoader for publisher avatar | ||||
|  | ||||
|     var body: some View { | ||||
|         ScrollView { | ||||
|             VStack(alignment: .leading, spacing: 8) { | ||||
|                 HStack { | ||||
|                     if publisherImageLoader.isLoading { | ||||
|                         ProgressView() | ||||
|                             .frame(width: 32, height: 32) | ||||
|                     } else if let image = publisherImageLoader.image { | ||||
|                         image | ||||
|                             .resizable() | ||||
|                             .frame(width: 32, height: 32) | ||||
|                             .clipShape(Circle()) | ||||
|                     } else if let errorMessage = publisherImageLoader.errorMessage { | ||||
|                         Text("Failed: \(errorMessage)") | ||||
|                             .font(.caption) | ||||
|                             .foregroundColor(.red) | ||||
|                             .frame(width: 32, height: 32) | ||||
|                     } else { | ||||
|                         Image(systemName: "person.circle.fill") | ||||
|                             .resizable() | ||||
|                             .frame(width: 32, height: 32) | ||||
|                             .clipShape(Circle()) | ||||
|                             .foregroundColor(.gray) | ||||
|                     } | ||||
|                     Text("@\(post.publisher.name)") | ||||
|                         .font(.headline) | ||||
|                 } | ||||
|                 // Use task(id:) to reload image when pictureId changes | ||||
|                 .task(id: post.publisher.picture?.id) { | ||||
|                     if let serverUrl = appState.serverUrl, let pictureId = post.publisher.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token { | ||||
|                         await publisherImageLoader.loadImage(from: imageUrl, token: token) | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 if let title = post.title, !title.isEmpty { | ||||
|                     Text(title) | ||||
|                         .font(.title2) | ||||
|                         .bold() | ||||
|                 } | ||||
|                  | ||||
|                 if let content = post.content, !content.isEmpty { | ||||
|                     Text(content) | ||||
|                         .font(.body) | ||||
|                 } | ||||
|                  | ||||
|                 if !post.attachments.isEmpty { | ||||
|                     Text("Attachments").font(.headline) | ||||
|                     ForEach(post.attachments) { attachment in | ||||
|                         AttachmentView(attachment: attachment) | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 if !post.tags.isEmpty { | ||||
|                     Text("Tags").font(.headline) | ||||
|                     FlowLayout(alignment: .leading, spacing: 4) { | ||||
|                         ForEach(post.tags) { tag in | ||||
|                             Text("#\(tag.name ?? tag.slug)") | ||||
|                                 .font(.caption) | ||||
|                                 .padding(.horizontal, 8) | ||||
|                                 .padding(.vertical, 3) | ||||
|                                 .background(Capsule().fill(Color.accentColor.opacity(0.2))) | ||||
|                                 .cornerRadius(5) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             .padding() | ||||
|         } | ||||
|         .navigationTitle("Post") | ||||
|     } | ||||
| } | ||||
							
								
								
									
										132
									
								
								ios/WatchRunner Watch App/Views/StatusCreationView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,132 @@ | ||||
| // | ||||
| //  StatusCreationView.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/30. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct StatusCreationView: View { | ||||
|     @EnvironmentObject var appState: AppState | ||||
|     @Environment(\.dismiss) var dismiss | ||||
|      | ||||
|     let initialStatus: SnAccountStatus? | ||||
|      | ||||
|     @State private var attitude: Int | ||||
|     @State private var isInvisible: Bool | ||||
|     @State private var isNotDisturb: Bool | ||||
|     @State private var label: String | ||||
|     @State private var isSubmitting: Bool = false | ||||
|     @State private var error: Error? = nil | ||||
|      | ||||
|     private let networkService = NetworkService() | ||||
|      | ||||
|     init(initialStatus: SnAccountStatus? = nil) { | ||||
|         self.initialStatus = initialStatus | ||||
|         _attitude = State(initialValue: initialStatus?.attitude ?? 1) | ||||
|         _isInvisible = State(initialValue: initialStatus?.isInvisible ?? false) | ||||
|         _isNotDisturb = State(initialValue: initialStatus?.isNotDisturb ?? false) | ||||
|         _label = State(initialValue: initialStatus?.label ?? "") | ||||
|     } | ||||
|      | ||||
|     var body: some View { | ||||
|         ScrollView { | ||||
|             VStack(spacing: 16) { | ||||
|                 // Title | ||||
|                 Text("Set Status") | ||||
|                     .font(.headline) | ||||
|                     .padding(.top) | ||||
|                  | ||||
|                 // Label TextField | ||||
|                 TextField("Status label", text: $label) | ||||
|                     .textFieldStyle(.automatic) | ||||
|                     .padding(.horizontal) | ||||
|                  | ||||
|                 // Attitude Picker | ||||
|                 VStack(alignment: .leading, spacing: 8) { | ||||
|                     Text("Mood") | ||||
|                         .font(.subheadline) | ||||
|                         .foregroundColor(.secondary) | ||||
|                      | ||||
|                     Picker("Attitude", selection: $attitude) { | ||||
|                         Text("😊 Positive").tag(0) | ||||
|                         Text("😐 Neutral").tag(1) | ||||
|                         Text("😢 Negative").tag(2) | ||||
|                     } | ||||
|                     .pickerStyle(.wheel) | ||||
|                     .frame(height: 80) | ||||
|                 } | ||||
|                 .padding(.horizontal) | ||||
|                  | ||||
|                 // Toggles | ||||
|                 VStack(spacing: 12) { | ||||
|                     Toggle("Invisible", isOn: $isInvisible) | ||||
|                         .padding(.horizontal) | ||||
|                      | ||||
|                     Toggle("Do Not Disturb", isOn: $isNotDisturb) | ||||
|                         .padding(.horizontal) | ||||
|                 } | ||||
|                  | ||||
|                 // Error message | ||||
|                 if let error = error { | ||||
|                     Text("Error: \(error.localizedDescription)") | ||||
|                         .foregroundColor(.red) | ||||
|                         .font(.caption) | ||||
|                         .padding(.horizontal) | ||||
|                 } | ||||
|                  | ||||
|                 // Buttons | ||||
|                 HStack(spacing: 12) { | ||||
|                     Button("Cancel") { | ||||
|                         dismiss() | ||||
|                     } | ||||
|                     .buttonStyle(.automatic) | ||||
|                      | ||||
|                     Button(isSubmitting ? "Saving..." : "Save") { | ||||
|                         Task { | ||||
|                             await submitStatus() | ||||
|                         } | ||||
|                     } | ||||
|                     .buttonStyle(.automatic) | ||||
|                     .disabled(isSubmitting) | ||||
|                 } | ||||
|                 .padding(.horizontal) | ||||
|                 .padding(.bottom) | ||||
|             } | ||||
|         } | ||||
|         .navigationTitle("Status") | ||||
|         .navigationBarTitleDisplayMode(.inline) | ||||
|     } | ||||
|      | ||||
|     private func submitStatus() async { | ||||
|         guard let token = appState.token, let serverUrl = appState.serverUrl else { | ||||
|             error = NSError(domain: "StatusCreationView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"]) | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         isSubmitting = true | ||||
|         error = nil | ||||
|          | ||||
|         do { | ||||
|             _ = try await networkService.createOrUpdateStatus( | ||||
|                 attitude: attitude, | ||||
|                 isInvisible: isInvisible, | ||||
|                 isNotDisturb: isNotDisturb, | ||||
|                 label: label.isEmpty ? nil : label, | ||||
|                 token: token, | ||||
|                 serverUrl: serverUrl | ||||
|             ) | ||||
|             dismiss() | ||||
|         } catch { | ||||
|             self.error = error | ||||
|         } | ||||
|          | ||||
|         isSubmitting = false | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     StatusCreationView() | ||||
|         .environmentObject(AppState()) | ||||
| } | ||||
							
								
								
									
										12
									
								
								ios/WatchRunner Watch App/Views/VideoPlayerView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| import SwiftUI | ||||
| import AVKit | ||||
| import AVFoundation | ||||
|  | ||||
| struct VideoPlayerView: View { | ||||
|     let videoUrl: URL | ||||
|  | ||||
|     var body: some View { | ||||
|         VideoPlayer(player: AVPlayer(url: videoUrl)) | ||||
|             .edgesIgnoringSafeArea(.all) // Make it full screen | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								ios/WatchRunner Watch App/WatchRunnerApp.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  WatchRunnerApp.swift | ||||
| //  WatchRunner Watch App | ||||
| // | ||||
| //  Created by LittleSheep on 2025/10/28. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| @main | ||||
| struct WatchRunner_Watch_AppApp: App { | ||||
|     var body: some Scene { | ||||
|         WindowGroup { | ||||
|             ContentView() | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								ios/WatchRunner-Watch-App-Info.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
| <dict> | ||||
| 	<key>UIBackgroundModes</key> | ||||
| 	<array> | ||||
| 		<string>remote-notification</string> | ||||
| 	</array> | ||||
| </dict> | ||||
| </plist> | ||||
| @@ -1,6 +1,7 @@ | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:drift/wasm.dart'; | ||||
| import 'package:island/database/drift_db.dart'; | ||||
| import 'package:island/talker.dart'; | ||||
|  | ||||
| AppDatabase constructDb() { | ||||
|   return AppDatabase(connectOnWeb()); | ||||
| @@ -9,12 +10,17 @@ AppDatabase constructDb() { | ||||
| DatabaseConnection connectOnWeb() { | ||||
|   return DatabaseConnection.delayed( | ||||
|     Future(() async { | ||||
|       try { | ||||
|         final result = await WasmDatabase.open( | ||||
|           databaseName: 'solar_network_data', | ||||
|           sqlite3Uri: Uri.parse('sqlite3.wasm'), | ||||
|           driftWorkerUri: Uri.parse('drift_worker.dart.js'), | ||||
|         ); | ||||
|         return result.resolvedExecutor; | ||||
|       } catch (e, stackTrace) { | ||||
|         talker.error('Failed to open WASM database...', e, stackTrace); | ||||
|         rethrow; | ||||
|       } | ||||
|     }), | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -120,13 +120,24 @@ void main() async { | ||||
|       windowButtonVisibility: true, | ||||
|     ); | ||||
|     windowManager.waitUntilReadyToShow(windowOptions, () async { | ||||
|       final env = Platform.environment; | ||||
|       final isWayland = env.containsKey('WAYLAND_DISPLAY'); | ||||
|  | ||||
|       if (isWayland) { | ||||
|         try { | ||||
|           await windowManager.setAsFrameless(); | ||||
|         } catch (e) { | ||||
|           debugPrint('[Wayland] setAsFrameless failed: $e'); | ||||
|         } | ||||
|       } | ||||
|       await windowManager.setMinimumSize(defaultSize); | ||||
|       await windowManager.show(); | ||||
|       await windowManager.focus(); | ||||
|       final opacity = prefs.getDouble(kAppWindowOpacity) ?? 1.0; | ||||
|       await windowManager.setOpacity(opacity); | ||||
|       talker.info( | ||||
|         "[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}", | ||||
|         "[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}" | ||||
|         "${isWayland ? " (Wayland frameless fix applied)" : ""}", | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -55,6 +55,19 @@ class ProfileLinkConverter | ||||
|   } | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class UsernameColor with _$UsernameColor { | ||||
|   const factory UsernameColor({ | ||||
|     @Default('plain') String type, | ||||
|     String? value, | ||||
|     String? direction, | ||||
|     List<String>? colors, | ||||
|   }) = _UsernameColor; | ||||
|  | ||||
|   factory UsernameColor.fromJson(Map<String, dynamic> json) => | ||||
|       _$UsernameColorFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class SnAccountProfile with _$SnAccountProfile { | ||||
|   const factory SnAccountProfile({ | ||||
| @@ -79,6 +92,7 @@ sealed class SnAccountProfile with _$SnAccountProfile { | ||||
|     required SnCloudFile? picture, | ||||
|     required SnCloudFile? background, | ||||
|     required SnVerificationMark? verification, | ||||
|     UsernameColor? usernameColor, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|   | ||||
| @@ -622,10 +622,284 @@ as String, | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$UsernameColor { | ||||
|  | ||||
|  String get type; String? get value; String? get direction; List<String>? get colors; | ||||
| /// Create a copy of UsernameColor | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $UsernameColorCopyWith<UsernameColor> get copyWith => _$UsernameColorCopyWithImpl<UsernameColor>(this as UsernameColor, _$identity); | ||||
|  | ||||
|   /// Serializes this UsernameColor to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other.colors, colors)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(colors)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $UsernameColorCopyWith<$Res>  { | ||||
|   factory $UsernameColorCopyWith(UsernameColor value, $Res Function(UsernameColor) _then) = _$UsernameColorCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String type, String? value, String? direction, List<String>? colors | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$UsernameColorCopyWithImpl<$Res> | ||||
|     implements $UsernameColorCopyWith<$Res> { | ||||
|   _$UsernameColorCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final UsernameColor _self; | ||||
|   final $Res Function(UsernameColor) _then; | ||||
|  | ||||
| /// Create a copy of UsernameColor | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable | ||||
| as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable | ||||
| as String?,colors: freezed == colors ? _self.colors : colors // ignore: cast_nullable_to_non_nullable | ||||
| as List<String>?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [UsernameColor]. | ||||
| extension UsernameColorPatterns on UsernameColor { | ||||
| /// A variant of `map` that fallback to returning `orElse`. | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case final Subclass value: | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return orElse(); | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _UsernameColor value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _UsernameColor() when $default != null: | ||||
| return $default(_that);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| } | ||||
| /// A `switch`-like method, using callbacks. | ||||
| /// | ||||
| /// Callbacks receives the raw object, upcasted. | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case final Subclass value: | ||||
| ///     return ...; | ||||
| ///   case final Subclass2 value: | ||||
| ///     return ...; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _UsernameColor value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _UsernameColor(): | ||||
| return $default(_that);} | ||||
| } | ||||
| /// A variant of `map` that fallback to returning `null`. | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case final Subclass value: | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return null; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _UsernameColor value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _UsernameColor() when $default != null: | ||||
| return $default(_that);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
| /// A variant of `when` that fallback to an `orElse` callback. | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case Subclass(:final field): | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return orElse(); | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type,  String? value,  String? direction,  List<String>? colors)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _UsernameColor() when $default != null: | ||||
| return $default(_that.type,_that.value,_that.direction,_that.colors);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| } | ||||
| /// A `switch`-like method, using callbacks. | ||||
| /// | ||||
| /// As opposed to `map`, this offers destructuring. | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case Subclass(:final field): | ||||
| ///     return ...; | ||||
| ///   case Subclass2(:final field2): | ||||
| ///     return ...; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type,  String? value,  String? direction,  List<String>? colors)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _UsernameColor(): | ||||
| return $default(_that.type,_that.value,_that.direction,_that.colors);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case Subclass(:final field): | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return null; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type,  String? value,  String? direction,  List<String>? colors)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _UsernameColor() when $default != null: | ||||
| return $default(_that.type,_that.value,_that.direction,_that.colors);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _UsernameColor implements UsernameColor { | ||||
|   const _UsernameColor({this.type = 'plain', this.value, this.direction, final  List<String>? colors}): _colors = colors; | ||||
|   factory _UsernameColor.fromJson(Map<String, dynamic> json) => _$UsernameColorFromJson(json); | ||||
|  | ||||
| @override@JsonKey() final  String type; | ||||
| @override final  String? value; | ||||
| @override final  String? direction; | ||||
|  final  List<String>? _colors; | ||||
| @override List<String>? get colors { | ||||
|   final value = _colors; | ||||
|   if (value == null) return null; | ||||
|   if (_colors is EqualUnmodifiableListView) return _colors; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(value); | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Create a copy of UsernameColor | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$UsernameColorCopyWith<_UsernameColor> get copyWith => __$UsernameColorCopyWithImpl<_UsernameColor>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$UsernameColorToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other._colors, _colors)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(_colors)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$UsernameColorCopyWith<$Res> implements $UsernameColorCopyWith<$Res> { | ||||
|   factory _$UsernameColorCopyWith(_UsernameColor value, $Res Function(_UsernameColor) _then) = __$UsernameColorCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String type, String? value, String? direction, List<String>? colors | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$UsernameColorCopyWithImpl<$Res> | ||||
|     implements _$UsernameColorCopyWith<$Res> { | ||||
|   __$UsernameColorCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _UsernameColor _self; | ||||
|   final $Res Function(_UsernameColor) _then; | ||||
|  | ||||
| /// Create a copy of UsernameColor | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) { | ||||
|   return _then(_UsernameColor( | ||||
| type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable | ||||
| as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable | ||||
| as String?,colors: freezed == colors ? _self._colors : colors // ignore: cast_nullable_to_non_nullable | ||||
| as List<String>?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnAccountProfile { | ||||
|  | ||||
|  String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
|  String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; UsernameColor? get usernameColor; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
| /// Create a copy of SnAccountProfile | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -638,16 +912,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]); | ||||
| int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
|   return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -658,11 +932,11 @@ abstract mixin class $SnAccountProfileCopyWith<$Res>  { | ||||
|   factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| $SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification; | ||||
| $SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;$UsernameColorCopyWith<$Res>? get usernameColor; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -675,7 +949,7 @@ class _$SnAccountProfileCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnAccountProfile | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable | ||||
| @@ -698,7 +972,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev | ||||
| as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable | ||||
| as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable | ||||
| as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable | ||||
| as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable | ||||
| as UsernameColor?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
| @@ -752,6 +1027,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification { | ||||
|   return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { | ||||
|     return _then(_self.copyWith(verification: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnAccountProfile | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $UsernameColorCopyWith<$Res>? get usernameColor { | ||||
|     if (_self.usernameColor == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) { | ||||
|     return _then(_self.copyWith(usernameColor: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
| @@ -831,10 +1118,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double socialCredits,  int socialCreditsLevel,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double socialCredits,  int socialCreditsLevel,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  UsernameColor? usernameColor,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountProfile() when $default != null: | ||||
| return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -852,10 +1139,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double socialCredits,  int socialCreditsLevel,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double socialCredits,  int socialCreditsLevel,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  UsernameColor? usernameColor,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountProfile(): | ||||
| return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -869,10 +1156,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double socialCredits,  int socialCreditsLevel,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double socialCredits,  int socialCreditsLevel,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  UsernameColor? usernameColor,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountProfile() when $default != null: | ||||
| return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -884,7 +1171,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnAccountProfile implements SnAccountProfile { | ||||
|   const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final  List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links; | ||||
|   const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final  List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, this.usernameColor, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links; | ||||
|   factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @@ -914,6 +1201,7 @@ class _SnAccountProfile implements SnAccountProfile { | ||||
| @override final  SnCloudFile? picture; | ||||
| @override final  SnCloudFile? background; | ||||
| @override final  SnVerificationMark? verification; | ||||
| @override final  UsernameColor? usernameColor; | ||||
| @override final  DateTime createdAt; | ||||
| @override final  DateTime updatedAt; | ||||
| @override final  DateTime? deletedAt; | ||||
| @@ -931,16 +1219,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]); | ||||
| int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
|   return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -951,11 +1239,11 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi | ||||
|   factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification; | ||||
| @override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;@override $UsernameColorCopyWith<$Res>? get usernameColor; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -968,7 +1256,7 @@ class __$SnAccountProfileCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnAccountProfile | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
|   return _then(_SnAccountProfile( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable | ||||
| @@ -991,7 +1279,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev | ||||
| as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable | ||||
| as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable | ||||
| as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable | ||||
| as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable | ||||
| as UsernameColor?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
| @@ -1046,6 +1335,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification { | ||||
|   return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { | ||||
|     return _then(_self.copyWith(verification: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnAccountProfile | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $UsernameColorCopyWith<$Res>? get usernameColor { | ||||
|     if (_self.usernameColor == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) { | ||||
|     return _then(_self.copyWith(usernameColor: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|   | ||||