Compare commits
10 Commits
3.3.0+145
...
refactor/w
| Author | SHA1 | Date | |
|---|---|---|---|
| 0622498f4e | |||
| 844efcda1a | |||
| 98e39cce6a | |||
| 0c459bf7e3 | |||
| a2576abee0 | |||
| f4b28c3fa2 | |||
|
43d767bc03
|
|||
|
0910be88ef
|
|||
|
e96b1fd9d4
|
|||
|
3f83bbc1d8
|
@@ -62,9 +62,3 @@ If you want to build the release version, use the flutter build command. Learn m
|
|||||||
```bash
|
```bash
|
||||||
flutter build <platform>
|
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.
|
|
||||||
@@ -43,16 +43,6 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- App protocol -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
|
||||||
<data android:scheme="solian" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- Deeplinking -->
|
<!-- Deeplinking -->
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|||||||
@@ -162,8 +162,6 @@
|
|||||||
"accountConnectionProviderGithub": "GitHub",
|
"accountConnectionProviderGithub": "GitHub",
|
||||||
"accountConnectionProviderDiscord": "Discord",
|
"accountConnectionProviderDiscord": "Discord",
|
||||||
"accountConnectionProviderAfdian": "Afdian",
|
"accountConnectionProviderAfdian": "Afdian",
|
||||||
"accountConnectionProviderSpotify": "Spotify",
|
|
||||||
"accountConnectionProviderSteam": "Steam",
|
|
||||||
"checkIn": "Check In",
|
"checkIn": "Check In",
|
||||||
"checkInNone": "Not checked-in yet",
|
"checkInNone": "Not checked-in yet",
|
||||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||||
@@ -471,7 +469,6 @@
|
|||||||
"pronouns": "Pronouns",
|
"pronouns": "Pronouns",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"timeZone": "Time Zone",
|
"timeZone": "Time Zone",
|
||||||
"timezoneNotFound": "Time zone not found",
|
|
||||||
"birthday": "Birthday",
|
"birthday": "Birthday",
|
||||||
"selectADate": "Select a date",
|
"selectADate": "Select a date",
|
||||||
"checkInResultT0": "Worst",
|
"checkInResultT0": "Worst",
|
||||||
@@ -874,7 +871,6 @@
|
|||||||
"pollShortTextAnswerPreview": "Short text answer (preview)",
|
"pollShortTextAnswerPreview": "Short text answer (preview)",
|
||||||
"award": "Award",
|
"award": "Award",
|
||||||
"awardPost": "Award Post",
|
"awardPost": "Award Post",
|
||||||
"awardPoints": "Awarded {} points",
|
|
||||||
"awardMessage": "Message",
|
"awardMessage": "Message",
|
||||||
"awardMessageHint": "Enter your award message...",
|
"awardMessageHint": "Enter your award message...",
|
||||||
"awardAttitude": "Attitude",
|
"awardAttitude": "Attitude",
|
||||||
@@ -1256,69 +1252,5 @@
|
|||||||
"availableWithYourPlan": "Available with your plan",
|
"availableWithYourPlan": "Available with your plan",
|
||||||
"upgradeRequired": "Upgrade required",
|
"upgradeRequired": "Upgrade required",
|
||||||
"settingsDisableAnimation": "Disable Animation",
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
"addTag": "Add Tag",
|
"addTag": "Add Tag"
|
||||||
"postFeaturedOn": "Post featured on {}",
|
|
||||||
"messageSentAt": "Sent at {}",
|
|
||||||
"myTickets": "My Tickets",
|
|
||||||
"drawHistory": "Draw History",
|
|
||||||
"lottery": "Lottery",
|
|
||||||
"noLotteryTickets": "No lottery tickets yet",
|
|
||||||
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
|
||||||
"buyTicket": "Buy Ticket",
|
|
||||||
"ticketNumbers": "Numbers: {}, Special: {}",
|
|
||||||
"cost": "Cost",
|
|
||||||
"multiplier": "Multiplier",
|
|
||||||
"prizeWon": "Prize Won",
|
|
||||||
"pending": "Pending",
|
|
||||||
"drawn": "Drawn",
|
|
||||||
"won": "Won",
|
|
||||||
"lost": "Lost",
|
|
||||||
"noDrawHistory": "No draw history yet",
|
|
||||||
"buyLotteryTicket": "Buy Lottery Ticket",
|
|
||||||
"selectNumbers": "Select Numbers",
|
|
||||||
"select5UniqueNumbers": "Select 5 unique numbers",
|
|
||||||
"selectSpecialNumber": "Select Special Number",
|
|
||||||
"selectMultiplier": "Select Multiplier",
|
|
||||||
"baseCost": "Base Cost",
|
|
||||||
"totalCost": "Total Cost",
|
|
||||||
"prizeStructure": "Prize Structure",
|
|
||||||
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
|
||||||
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
|
||||||
"winningNumbers": "Winning Numbers",
|
|
||||||
"specialNumber": "Special Number",
|
|
||||||
"totalTickets": "Total Tickets",
|
|
||||||
"totalWinners": "Total Winners",
|
|
||||||
"prizePool": "Prize Pool",
|
|
||||||
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
|
||||||
"purchase": "Purchase",
|
|
||||||
"multiplierLabel": "Multiplier",
|
|
||||||
"specialOnly": "Special Only",
|
|
||||||
"matches": "Matches",
|
|
||||||
"thoughtDefaultTopic": "Reflection",
|
|
||||||
"thoughtAiName": "SN-chan",
|
|
||||||
"thoughtUserName": "You",
|
|
||||||
"thoughtStreamingHint": "Sn-chan is thinking...",
|
|
||||||
"thoughtInputHint": "Ask sn-chan anything...",
|
|
||||||
"thoughtNewConversation": "Start New Conversation",
|
|
||||||
"thoughtParseError": "Failed to parse AI response",
|
|
||||||
"thoughtFunctionCall": "Function Call",
|
|
||||||
"aiThought": "AI Thought",
|
|
||||||
"aiThoughtTitle": "Let sn-chan think",
|
|
||||||
"postReferenceUnavailable": "Referenced post is unavailable",
|
|
||||||
"fabLocation": "FAB Location",
|
|
||||||
"activities": "Activities",
|
|
||||||
"presenceTypeGaming": "Playing",
|
|
||||||
"presenceTypeMusic": "Listening to Music",
|
|
||||||
"presenceTypeWorkout": "Working out",
|
|
||||||
"articleCompose": "Compose Article",
|
|
||||||
"backToHub": "Back to Hub",
|
|
||||||
"advancedFilters": "Advanced Filters",
|
|
||||||
"searchPosts": "Search Posts",
|
|
||||||
"sortBy": "Sort by",
|
|
||||||
"fromDate": "From Date",
|
|
||||||
"toDate": "To Date",
|
|
||||||
"popularity": "Popularity",
|
|
||||||
"descendingOrder": "Descending Order",
|
|
||||||
"selectDate": "Select Date",
|
|
||||||
"pinnedPosts": "Pinned Posts"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,11 +158,11 @@
|
|||||||
"checkIn": "签到",
|
"checkIn": "签到",
|
||||||
"checkInNone": "尚未签到",
|
"checkInNone": "尚未签到",
|
||||||
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
|
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
|
||||||
"checkInResultLevel0": "大凶",
|
"checkInResultLevel0": "最差运气",
|
||||||
"checkInResultLevel1": "凶",
|
"checkInResultLevel1": "坏运气",
|
||||||
"checkInResultLevel2": "中平",
|
"checkInResultLevel2": "一个普通的日常",
|
||||||
"checkInResultLevel3": "吉",
|
"checkInResultLevel3": "好运",
|
||||||
"checkInResultLevel4": "大吉",
|
"checkInResultLevel4": "最佳运气",
|
||||||
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
||||||
"eventCalander": "活动日历",
|
"eventCalander": "活动日历",
|
||||||
"eventCalanderEmpty": "该日无活动。",
|
"eventCalanderEmpty": "该日无活动。",
|
||||||
@@ -344,7 +344,7 @@
|
|||||||
"accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。",
|
"accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。",
|
||||||
"unauthorized": "未授权",
|
"unauthorized": "未授权",
|
||||||
"unauthorizedHint": "您未登录或会话已过期,请重新登录。",
|
"unauthorizedHint": "您未登录或会话已过期,请重新登录。",
|
||||||
"publisherBelongsTo": "属于 {}",
|
"publisherBelongsTo": "属于",
|
||||||
"postContent": "内容",
|
"postContent": "内容",
|
||||||
"postSettings": "设置",
|
"postSettings": "设置",
|
||||||
"postPublisherUnselected": "未指定发布者",
|
"postPublisherUnselected": "未指定发布者",
|
||||||
@@ -1081,14 +1081,5 @@
|
|||||||
"postPublish": "发布帖子",
|
"postPublish": "发布帖子",
|
||||||
"restoreDraftTitle": "恢复草稿",
|
"restoreDraftTitle": "恢复草稿",
|
||||||
"restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?",
|
"restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?",
|
||||||
"draft": "草稿",
|
"draft": "草稿"
|
||||||
"thoughtDefaultTopic": "寻思",
|
|
||||||
"thoughtAiName": "SN 酱",
|
|
||||||
"thoughtUserName": "您",
|
|
||||||
"thoughtStreamingHint": "SN 酱正在思考...",
|
|
||||||
"thoughtInputHint": "问 SN 酱任何问题...",
|
|
||||||
"thoughtNewConversation": "开始新对话",
|
|
||||||
"thoughtParseError": "解析 AI 响应失败",
|
|
||||||
"aiThought": "寻思",
|
|
||||||
"aiThoughtTitle": "让 SN 酱寻思寻思"
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg width="2471" height="2500" viewBox="0 0 256 259" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M127.779 0C60.42 0 5.24 52.412 0 119.014l68.724 28.674a35.812 35.812 0 0 1 20.426-6.366c.682 0 1.356.019 2.02.056l30.566-44.71v-.626c0-26.903 21.69-48.796 48.353-48.796 26.662 0 48.352 21.893 48.352 48.796 0 26.902-21.69 48.804-48.352 48.804-.37 0-.73-.009-1.098-.018l-43.593 31.377c.028.582.046 1.163.046 1.735 0 20.204-16.283 36.636-36.294 36.636-17.566 0-32.263-12.658-35.584-29.412L4.41 164.654c15.223 54.313 64.673 94.132 123.369 94.132 70.818 0 128.221-57.938 128.221-129.393C256 57.93 198.597 0 127.779 0zM80.352 196.332l-15.749-6.568c2.787 5.867 7.621 10.775 14.033 13.47 13.857 5.83 29.836-.803 35.612-14.799a27.555 27.555 0 0 0 .046-21.035c-2.768-6.79-7.999-12.086-14.706-14.909-6.67-2.795-13.811-2.694-20.085-.304l16.275 6.79c10.222 4.3 15.056 16.145 10.794 26.46-4.253 10.314-15.998 15.195-26.22 10.895zm121.957-100.29c0-17.925-14.457-32.52-32.217-32.52-17.769 0-32.226 14.595-32.226 32.52 0 17.926 14.457 32.512 32.226 32.512 17.76 0 32.217-14.586 32.217-32.512zm-56.37-.055c0-13.488 10.84-24.42 24.2-24.42 13.368 0 24.208 10.932 24.208 24.42 0 13.488-10.84 24.421-24.209 24.421-13.359 0-24.2-10.933-24.2-24.42z" fill="#1A1918"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
15
ios/Podfile
@@ -1,3 +1,4 @@
|
|||||||
|
# Uncomment this line to define a global platform for your project
|
||||||
platform :ios, '15.0'
|
platform :ios, '15.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
@@ -31,8 +32,6 @@ target 'Runner' do
|
|||||||
use_modular_headers!
|
use_modular_headers!
|
||||||
|
|
||||||
pod 'Alamofire'
|
pod 'Alamofire'
|
||||||
pod 'Kingfisher', '~> 8.0'
|
|
||||||
pod 'KingfisherWebP'
|
|
||||||
|
|
||||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
|
||||||
@@ -42,6 +41,8 @@ target 'Runner' do
|
|||||||
|
|
||||||
target 'SolianNotificationService' do
|
target 'SolianNotificationService' do
|
||||||
inherit! :search_paths
|
inherit! :search_paths
|
||||||
|
pod 'Kingfisher', '~> 8.0'
|
||||||
|
pod 'Alamofire'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'SolianShareExtension' do
|
target 'SolianShareExtension' do
|
||||||
@@ -49,16 +50,6 @@ target 'Runner' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'Solian Watch App' do
|
|
||||||
platform :watchos, '11.0'
|
|
||||||
|
|
||||||
use_frameworks!
|
|
||||||
use_modular_headers!
|
|
||||||
|
|
||||||
pod 'Kingfisher', '~> 8.0'
|
|
||||||
pod 'KingfisherWebP'
|
|
||||||
end
|
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
flutter_additional_ios_build_settings(target)
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ PODS:
|
|||||||
- Alamofire (5.10.2)
|
- Alamofire (5.10.2)
|
||||||
- app_links (6.4.1):
|
- app_links (6.4.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
@@ -218,26 +216,7 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.6.1)
|
- Kingfisher (8.6.0)
|
||||||
- KingfisherWebP (1.7.2):
|
|
||||||
- Kingfisher (~> 8.0)
|
|
||||||
- libwebp (>= 1.1.0)
|
|
||||||
- libwebp (1.5.0):
|
|
||||||
- libwebp/demux (= 1.5.0)
|
|
||||||
- libwebp/mux (= 1.5.0)
|
|
||||||
- libwebp/sharpyuv (= 1.5.0)
|
|
||||||
- libwebp/webp (= 1.5.0)
|
|
||||||
- libwebp/demux (1.5.0):
|
|
||||||
- libwebp/webp
|
|
||||||
- libwebp/mux (1.5.0):
|
|
||||||
- libwebp/demux
|
|
||||||
- libwebp/sharpyuv (1.5.0)
|
|
||||||
- libwebp/webp (1.5.0):
|
|
||||||
- libwebp/sharpyuv
|
|
||||||
- livekit_client (2.5.3):
|
|
||||||
- Flutter
|
|
||||||
- flutter_webrtc
|
|
||||||
- WebRTC-SDK (= 137.7151.04)
|
|
||||||
- local_auth_darwin (0.0.1):
|
- local_auth_darwin (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -324,7 +303,6 @@ PODS:
|
|||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
|
||||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
@@ -348,8 +326,6 @@ DEPENDENCIES:
|
|||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
- KingfisherWebP
|
|
||||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
|
||||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||||
@@ -391,8 +367,6 @@ SPEC REPOS:
|
|||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- KingfisherWebP
|
|
||||||
- libwebp
|
|
||||||
- nanopb
|
- nanopb
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
@@ -406,8 +380,6 @@ SPEC REPOS:
|
|||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_links:
|
app_links:
|
||||||
:path: ".symlinks/plugins/app_links/ios"
|
:path: ".symlinks/plugins/app_links/ios"
|
||||||
connectivity_plus:
|
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
|
||||||
croppy:
|
croppy:
|
||||||
:path: ".symlinks/plugins/croppy/ios"
|
:path: ".symlinks/plugins/croppy/ios"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
@@ -452,8 +424,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
irondash_engine_context:
|
irondash_engine_context:
|
||||||
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
||||||
livekit_client:
|
|
||||||
:path: ".symlinks/plugins/livekit_client/ios"
|
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||||
media_kit_libs_ios_video:
|
media_kit_libs_ios_video:
|
||||||
@@ -498,7 +468,6 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
|
||||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
@@ -535,12 +504,9 @@ SPEC CHECKSUMS:
|
|||||||
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
|
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
Kingfisher: 7ac7a7288653787a54206b11a3c74f49ab650f1f
|
Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0
|
||||||
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
|
||||||
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
|
||||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
@@ -549,8 +515,8 @@ SPEC CHECKSUMS:
|
|||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
@@ -558,7 +524,7 @@ SPEC CHECKSUMS:
|
|||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
|
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
|
||||||
@@ -566,11 +532,11 @@ SPEC CHECKSUMS:
|
|||||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||||
|
|
||||||
PODFILE CHECKSUM: 585198f58dca90ac6492607c83a8d17045ab3852
|
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
5D8143680678FCD1D1827271 /* Pods_Solian_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */; };
|
|
||||||
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
|
||||||
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
|
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
|
||||||
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
@@ -60,17 +58,6 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
7310A7DE2EB10963002C0FD3 /* Embed Watch Content */ = {
|
|
||||||
isa = PBXCopyFilesBuildPhase;
|
|
||||||
buildActionMask = 12;
|
|
||||||
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
|
|
||||||
dstSubfolderSpec = 16;
|
|
||||||
files = (
|
|
||||||
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */,
|
|
||||||
);
|
|
||||||
name = "Embed Watch Content";
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = {
|
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -97,8 +84,6 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
0ECC3D56D018DD87FC342699 /* Pods-Solian Watch App.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Solian Watch App.profile.xcconfig"; path = "Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App.profile.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.profile.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.profile.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
@@ -106,18 +91,15 @@
|
|||||||
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
1C14F71D23E4371602065522 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
1C14F71D23E4371602065522 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
2440CEDEAAD6D51FDA95FA62 /* Pods-Solian Watch App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Solian Watch App.release.xcconfig"; path = "Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App.release.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.release.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.release.xcconfig"; sourceTree = "<group>"; };
|
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.release.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Solian Watch App.debug.xcconfig"; path = "Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App.debug.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Solian Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
|
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
|
||||||
@@ -129,7 +111,6 @@
|
|||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.debug.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.debug.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -139,12 +120,10 @@
|
|||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||||
A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.release.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.release.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.release.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.release.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Solian_Watch_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
F6D834CA86410B09796B312B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
F6D834CA86410B09796B312B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.debug.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.debug.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
@@ -183,13 +162,6 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = "Solian Watch App";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
73268D272DEB012A0076E970 /* Services */ = {
|
73268D272DEB012A0076E970 /* Services */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
@@ -233,14 +205,6 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
7310A7D12EB10962002C0FD3 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
5D8143680678FCD1D1827271 /* Pods_Solian_Watch_App.framework in Frameworks */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
73ACDFA82E3D0E6100B63535 /* Frameworks */ = {
|
73ACDFA82E3D0E6100B63535 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -294,7 +258,6 @@
|
|||||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
|
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
|
||||||
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
|
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
|
||||||
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
|
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
|
||||||
C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */,
|
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -317,12 +280,6 @@
|
|||||||
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */,
|
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */,
|
||||||
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */,
|
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */,
|
||||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */,
|
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */,
|
||||||
86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */,
|
|
||||||
A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */,
|
|
||||||
103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */,
|
|
||||||
31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */,
|
|
||||||
2440CEDEAAD6D51FDA95FA62 /* Pods-Solian Watch App.release.xcconfig */,
|
|
||||||
0ECC3D56D018DD87FC342699 /* Pods-Solian Watch App.profile.xcconfig */,
|
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -346,7 +303,6 @@
|
|||||||
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
||||||
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
||||||
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */,
|
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
91E124CE95BCB4DCD890160D /* Pods */,
|
91E124CE95BCB4DCD890160D /* Pods */,
|
||||||
@@ -363,7 +319,6 @@
|
|||||||
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
|
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
|
||||||
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
||||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
|
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
|
||||||
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */,
|
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -408,28 +363,6 @@
|
|||||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
7310A7D32EB10962002C0FD3 /* Solian Watch App */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */;
|
|
||||||
buildPhases = (
|
|
||||||
DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */,
|
|
||||||
7310A7D02EB10962002C0FD3 /* Sources */,
|
|
||||||
7310A7D12EB10962002C0FD3 /* Frameworks */,
|
|
||||||
7310A7D22EB10962002C0FD3 /* Resources */,
|
|
||||||
E29ECA5954168075BDB000DC /* [CP] Embed Pods Frameworks */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */,
|
|
||||||
);
|
|
||||||
name = "Solian Watch App";
|
|
||||||
productName = "WatchRunner Watch App";
|
|
||||||
productReference = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = {
|
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */;
|
buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */;
|
||||||
@@ -501,7 +434,6 @@
|
|||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
|
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
|
||||||
7310A7DE2EB10963002C0FD3 /* Embed Watch Content */,
|
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
@@ -531,7 +463,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 2600;
|
LastSwiftUpdateCheck = 1640;
|
||||||
LastUpgradeCheck = 1510;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
@@ -539,9 +471,6 @@
|
|||||||
CreatedOnToolsVersion = 14.0;
|
CreatedOnToolsVersion = 14.0;
|
||||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
};
|
};
|
||||||
7310A7D32EB10962002C0FD3 = {
|
|
||||||
CreatedOnToolsVersion = 26.0.1;
|
|
||||||
};
|
|
||||||
73ACDFAA2E3D0E6100B63535 = {
|
73ACDFAA2E3D0E6100B63535 = {
|
||||||
CreatedOnToolsVersion = 16.4;
|
CreatedOnToolsVersion = 16.4;
|
||||||
};
|
};
|
||||||
@@ -575,7 +504,6 @@
|
|||||||
73CDD6792DEC00480059D95D /* SolianNotificationService */,
|
73CDD6792DEC00480059D95D /* SolianNotificationService */,
|
||||||
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
||||||
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||||
7310A7D32EB10962002C0FD3 /* Solian Watch App */,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -588,13 +516,6 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
7310A7D22EB10962002C0FD3 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
73ACDFA92E3D0E6100B63535 /* Resources */ = {
|
73ACDFA92E3D0E6100B63535 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -762,45 +683,6 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
};
|
};
|
||||||
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-Solian 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-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
|
||||||
outputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
|
E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -852,13 +734,6 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
7310A7D02EB10962002C0FD3 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
73ACDFA72E3D0E6100B63535 /* Sources */ = {
|
73ACDFA72E3D0E6100B63535 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -998,7 +873,6 @@
|
|||||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
EXCLUDED_SOURCE_FILE_NAMES = "";
|
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
@@ -1009,12 +883,10 @@
|
|||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
};
|
};
|
||||||
@@ -1022,7 +894,6 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */;
|
baseConfigurationReference = 14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -1031,8 +902,6 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SUPPORTS_MACCATALYST = YES;
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -1044,7 +913,6 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */;
|
baseConfigurationReference = 14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -1053,8 +921,6 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SUPPORTS_MACCATALYST = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
};
|
};
|
||||||
@@ -1064,7 +930,6 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */;
|
baseConfigurationReference = E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -1073,162 +938,11 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SUPPORTS_MACCATALYST = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
};
|
};
|
||||||
7310A7E02EB10963002C0FD3 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
baseConfigurationReference = 31EA49B10397BD4145AD765E /* Pods-Solian 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 = 2440CEDEAAD6D51FDA95FA62 /* Pods-Solian 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 = 0ECC3D56D018DD87FC342699 /* Pods-Solian 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 */ = {
|
73ACDFC42E3D0E6100B63535 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -1262,7 +976,6 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -1303,7 +1016,6 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@@ -1342,7 +1054,6 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@@ -1384,7 +1095,6 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
@@ -1428,7 +1138,6 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -1470,7 +1179,6 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -1720,7 +1428,6 @@
|
|||||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
EXCLUDED_SOURCE_FILE_NAMES = "";
|
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
@@ -1736,7 +1443,6 @@
|
|||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -1751,7 +1457,6 @@
|
|||||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
EXCLUDED_SOURCE_FILE_NAMES = "";
|
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
@@ -1760,15 +1465,12 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
ONLY_ACTIVE_ARCH = NO;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -1785,16 +1487,6 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
7310A7E02EB10963002C0FD3 /* Debug */,
|
|
||||||
7310A7E12EB10963002C0FD3 /* Release */,
|
|
||||||
7310A7E22EB10963002C0FD3 /* Profile */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = {
|
73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -20,20 +20,6 @@
|
|||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
|
|
||||||
BuildableName = "Solian Watch App.app"
|
|
||||||
BlueprintName = "Solian Watch App"
|
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
import WatchConnectivity
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
let notifyDelegate = NotifyDelegate()
|
let notifyDelegate = NotifyDelegate()
|
||||||
private static var sharedWatchConnectivityService: WatchConnectivityService?
|
|
||||||
|
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
@@ -25,85 +23,11 @@ import WatchConnectivity
|
|||||||
intentIdentifiers: [],
|
intentIdentifiers: [],
|
||||||
options: []
|
options: []
|
||||||
)
|
)
|
||||||
|
|
||||||
UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
|
UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
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)
|
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,334 +1 @@
|
|||||||
{
|
{"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 762 B |
@@ -1,13 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>BUNDLE_ID</key>
|
<key>BUNDLE_ID</key>
|
||||||
<string>dev.solsynth.solian</string>
|
<string>dev.solsynth.solian</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@@ -38,9 +38,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Viewer</string>
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string></string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>solian</string>
|
<string>solian</string>
|
||||||
@@ -52,16 +50,15 @@
|
|||||||
<key>CLIENT_ID</key>
|
<key>CLIENT_ID</key>
|
||||||
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false />
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>NSCalendarsUsageDescription</key>
|
<key>NSCalendarsUsageDescription</key>
|
||||||
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue
|
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
||||||
your action quickly.</string>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
@@ -78,7 +75,7 @@
|
|||||||
<key>REVERSED_CLIENT_ID</key>
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
@@ -91,15 +88,13 @@
|
|||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<false />
|
<false/>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
</array>
|
</array>
|
||||||
<key>WKCompanionAppBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
@@ -107,5 +102,5 @@
|
|||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
func getAttachmentUrl(for identifier: String) -> String {
|
func getAttachmentUrl(for identifier: String) -> String {
|
||||||
let serverBaseUrl = UserDefaults.standard.getServerUrl()
|
let serverBaseUrl = "https://api.solian.app"
|
||||||
|
|
||||||
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
|
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ extension UserDefaults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getServerUrl(forKey key: String = "app_server_url") -> String {
|
func getServerUrl(forKey key: String = "app_server_url") -> String {
|
||||||
return self.getFlutterValue(forKey: key) ?? "https://api.solian.app"
|
return self.getFlutterValue(forKey: key) ?? "https://nt.solian.app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"platform" : "universal",
|
|
||||||
"reference" : "systemIndigoColor"
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 473 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "icon.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 70 KiB |
@@ -1,50 +0,0 @@
|
|||||||
//
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
// 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
//
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,643 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
|
|
||||||
init() {
|
|
||||||
let config = URLSessionConfiguration.ephemeral
|
|
||||||
config.waitsForConnectivity = true
|
|
||||||
session = URLSession(configuration: config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
//
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
//
|
|
||||||
// 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())
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
//
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,785 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 ?? "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
//
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 ?? "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
|
|
||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
//
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
//
|
|
||||||
// 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())
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ import UserNotifications
|
|||||||
import Intents
|
import Intents
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import KingfisherWebP
|
|
||||||
|
|
||||||
enum ParseNotificationPayloadError: Error {
|
enum ParseNotificationPayloadError: Error {
|
||||||
case missingMetadata(String)
|
case missingMetadata(String)
|
||||||
@@ -25,11 +24,6 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
_ request: UNNotificationRequest,
|
_ request: UNNotificationRequest,
|
||||||
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
||||||
) {
|
) {
|
||||||
KingfisherManager.shared.defaultOptions += [
|
|
||||||
.processor(WebPProcessor.default),
|
|
||||||
.cacheSerializer(WebPSerializer.default)
|
|
||||||
]
|
|
||||||
|
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
||||||
contentHandler(request.content)
|
contentHandler(request.content)
|
||||||
@@ -65,44 +59,14 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let pfpIdentifier = meta["pfp"] as? String
|
let pfpIdentifier = meta["pfp"] as? String
|
||||||
|
|
||||||
let metaCopy = meta as? [String: Any] ?? [:]
|
let metaCopy = meta as? [String: Any] ?? [:]
|
||||||
let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil
|
let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil
|
||||||
|
|
||||||
let handle = INPersonHandle(value: "\(metaCopy["user_id"] ?? "")", type: .unknown)
|
|
||||||
|
|
||||||
let completeNotificationProcessing: (Data?) -> Void = { imageData in
|
|
||||||
let sender = INPerson(
|
|
||||||
personHandle: handle,
|
|
||||||
nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"),
|
|
||||||
displayName: content.title,
|
|
||||||
image: imageData == nil ? nil : INImage(imageData: imageData!),
|
|
||||||
contactIdentifier: nil,
|
|
||||||
customIdentifier: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
|
||||||
self.donateInteraction(for: intent)
|
|
||||||
|
|
||||||
if let updatedContent = try? request.content.updating(from: intent) {
|
|
||||||
if let mutableContent = updatedContent.mutableCopy() as? UNMutableNotificationContent {
|
|
||||||
mutableContent.categoryIdentifier = "CHAT_MESSAGE"
|
|
||||||
self.contentHandler?(mutableContent)
|
|
||||||
} else {
|
|
||||||
self.contentHandler?(updatedContent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content.categoryIdentifier = "CHAT_MESSAGE"
|
|
||||||
self.contentHandler?(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let pfpUrl = pfpUrl, let url = URL(string: pfpUrl) {
|
|
||||||
let targetSize = 512
|
let targetSize = 512
|
||||||
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
||||||
|
|
||||||
KingfisherManager.shared.retrieveImage(with: url, options: [
|
KingfisherManager.shared.retrieveImage(with: URL(string: pfpUrl!)!, options: [.processor(scaleProcessor)], completionHandler: { result in
|
||||||
.processor(scaleProcessor)
|
|
||||||
], completionHandler: { result in
|
|
||||||
var image: Data?
|
var image: Data?
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let value):
|
case .success(let value):
|
||||||
@@ -110,11 +74,27 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("Unable to get pfp url: \(error)")
|
print("Unable to get pfp url: \(error)")
|
||||||
}
|
}
|
||||||
completeNotificationProcessing(image)
|
|
||||||
})
|
let handle = INPersonHandle(value: "\(metaCopy["user_id"] ?? "")", type: .unknown)
|
||||||
|
let sender = INPerson(
|
||||||
|
personHandle: handle,
|
||||||
|
nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"),
|
||||||
|
displayName: content.title,
|
||||||
|
image: image == nil ? nil : INImage(imageData: image!),
|
||||||
|
contactIdentifier: nil,
|
||||||
|
customIdentifier: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
||||||
|
self.donateInteraction(for: intent)
|
||||||
|
let updatedContent = try? request.content.updating(from: intent)
|
||||||
|
content.categoryIdentifier = "CHAT_MESSAGE"
|
||||||
|
if let updatedContent = updatedContent {
|
||||||
|
self.contentHandler?(updatedContent)
|
||||||
} else {
|
} else {
|
||||||
completeNotificationProcessing(nil)
|
self.contentHandler?(content)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleDefaultNotification(content: UNMutableNotificationContent) throws {
|
private func handleDefaultNotification(content: UNMutableNotificationContent) throws {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -30,7 +30,6 @@ import 'package:talker_flutter/talker_flutter.dart';
|
|||||||
import 'package:talker_riverpod_logger/talker_riverpod_logger.dart';
|
import 'package:talker_riverpod_logger/talker_riverpod_logger.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'package:protocol_handler/protocol_handler.dart';
|
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
@@ -51,12 +50,6 @@ void main() async {
|
|||||||
GoRouter.optionURLReflectsImperativeAPIs = true;
|
GoRouter.optionURLReflectsImperativeAPIs = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows)) {
|
|
||||||
talker.info("[SplashScreen] Initializing desktop window manager...");
|
|
||||||
await protocolHandler.register('myprotocol');
|
|
||||||
talker.info("[SplashScreen] Desktop window manager is ready!");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ sealed class SnNotableDay with _$SnNotableDay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class SnTimelineEvent with _$SnTimelineEvent {
|
sealed class SnActivity with _$SnActivity {
|
||||||
const factory SnTimelineEvent({
|
const factory SnActivity({
|
||||||
required String id,
|
required String id,
|
||||||
required String type,
|
required String type,
|
||||||
required String resourceIdentifier,
|
required String resourceIdentifier,
|
||||||
@@ -28,10 +28,10 @@ sealed class SnTimelineEvent with _$SnTimelineEvent {
|
|||||||
required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
}) = _SnTimelineEvent;
|
}) = _SnActivity;
|
||||||
|
|
||||||
factory SnTimelineEvent.fromJson(Map<String, dynamic> json) =>
|
factory SnActivity.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnTimelineEventFromJson(json);
|
_$SnActivityFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@@ -74,29 +74,3 @@ sealed class SnEventCalendarEntry with _$SnEventCalendarEntry {
|
|||||||
factory SnEventCalendarEntry.fromJson(Map<String, dynamic> json) =>
|
factory SnEventCalendarEntry.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnEventCalendarEntryFromJson(json);
|
_$SnEventCalendarEntryFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
|
||||||
sealed class SnPresenceActivity with _$SnPresenceActivity {
|
|
||||||
const factory SnPresenceActivity({
|
|
||||||
required String id,
|
|
||||||
required int type,
|
|
||||||
required String? manualId,
|
|
||||||
required String? title,
|
|
||||||
required String? subtitle,
|
|
||||||
required String? caption,
|
|
||||||
required String? titleUrl,
|
|
||||||
required String? subtitleUrl,
|
|
||||||
required String? smallImage,
|
|
||||||
required String? largeImage,
|
|
||||||
required Map<String, dynamic>? meta,
|
|
||||||
required int leaseMinutes,
|
|
||||||
required DateTime leaseExpiresAt,
|
|
||||||
required String accountId,
|
|
||||||
required DateTime createdAt,
|
|
||||||
required DateTime updatedAt,
|
|
||||||
required DateTime? deletedAt,
|
|
||||||
}) = _SnPresenceActivity;
|
|
||||||
|
|
||||||
factory SnPresenceActivity.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SnPresenceActivityFromJson(json);
|
|
||||||
}
|
|
||||||
|
|||||||