Compare commits
236 Commits
a2cc55696f
...
v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
504322c2dd
|
|||
|
a07ec3ca36
|
|||
| d96691e920 | |||
|
6273b2d917
|
|||
|
ab90d244b5
|
|||
|
dc6af6d9e5
|
|||
|
0ca801d963
|
|||
|
3edcdd72af
|
|||
|
402bb3fe04
|
|||
|
8ba55eb1be
|
|||
|
983ae2a1fc
|
|||
|
6fc94001b3
|
|||
|
44dbcfdc94
|
|||
|
b57caf56db
|
|||
|
dbcd1b6d36
|
|||
|
a8055de910
|
|||
|
49b15e7674
|
|||
|
e2369c40db
|
|||
|
44c5d91620
|
|||
|
7a5a2407b7
|
|||
|
234434f102
|
|||
|
9c3b228d02
|
|||
|
82682cae9a
|
|||
|
fcbd5fe680
|
|||
|
ad91b17af7
|
|||
|
24fa637329
|
|||
|
926ae5402f
|
|||
|
1a37d384e6
|
|||
|
d4cf598f69
|
|||
|
0106c08891
|
|||
|
9697def808
|
|||
|
6572875229
|
|||
|
66590b9079
|
|||
|
08b9604b55
|
|||
|
0602bbd277
|
|||
|
76e7ba7898
|
|||
|
6e6616b236
|
|||
|
071d51b25e
|
|||
|
a958362461
|
|||
|
6749bb00fe
|
|||
|
11fb20c673
|
|||
|
a7990f83db
|
|||
|
5f4cdf7937
|
|||
|
3330ca14dd
|
|||
|
1719b1c8fe
|
|||
|
3c2c51bfaf
|
|||
|
239d6750ff
|
|||
|
8b0c91977a
|
|||
|
f74cca8464
|
|||
|
08091d51bf
|
|||
|
481190811b
|
|||
|
4b32b65d1c
|
|||
|
50ac7109bb
|
|||
|
62da279c71
|
|||
|
fde6dbf891
|
|||
|
613bf4fb42
|
|||
|
00ae586016
|
|||
|
ea0d132dce
|
|||
|
aa2df1e847
|
|||
|
50672795f3
|
|||
|
383de9568d
|
|||
|
01fa228e45
|
|||
|
1e71ad33a6
|
|||
|
92c0260ecd
|
|||
|
0a161ad255
|
|||
|
c003f27b9a
|
|||
|
19db8309c4
|
|||
|
aa72ce08e8
|
|||
|
4639b00b86
|
|||
|
cc5460ea55
|
|||
|
eafac811e6
|
|||
|
e3be691596
|
|||
|
aa180a1358
|
|||
|
c2707b8af1
|
|||
|
62fd0500f3
|
|||
|
eeae865cc8
|
|||
|
cdf1413fe0
|
|||
|
327b4c04f1
|
|||
|
bd903ce29c
|
|||
|
1b8ecb15ce
|
|||
|
d4e380a97a
|
|||
|
126048b4fa
|
|||
|
8bec18813d
|
|||
|
1ae81794b1
|
|||
|
2a7d12de48
|
|||
|
64c60ead48
|
|||
| 001549b190 | |||
| 4595865ad3 | |||
|
|
1834643167 | ||
|
|
0e816eaa3e | ||
|
|
7c1f24b824 | ||
|
c6594ea2ce
|
|||
|
3bec6e683e
|
|||
|
83e92e2eed
|
|||
|
|
b7d44d96ba | ||
|
a83b929d42
|
|||
|
9423affa75
|
|||
|
cda23db609
|
|||
|
61074bc5a3
|
|||
|
5feafa9255
|
|||
|
e604577c1f
|
|||
|
af0ddd1273
|
|||
|
8a6bb34808
|
|||
|
4ef8445c77
|
|||
|
ec39ad6ca3
|
|||
|
eabb3154f1
|
|||
|
910bf20eef
|
|||
|
5efa9b2ae8
|
|||
|
dd3e39e891
|
|||
|
b6896ded23
|
|||
|
f28a73ff9c
|
|||
|
a014b64235
|
|||
|
7e0e7c20d7
|
|||
|
389fa515ba
|
|||
|
681ead02eb
|
|||
|
8d1c145b0b
|
|||
|
51b4754182
|
|||
|
8a2b321701
|
|||
|
f685a7a249
|
|||
|
76009147e9
|
|||
|
ce12f28e56
|
|||
|
3604373a1e
|
|||
|
9704a4c2c7
|
|||
|
67def56ad1
|
|||
|
1be33916af
|
|||
|
e8ff1bfd22
|
|||
|
3ae56f3d89
|
|||
|
707143e998
|
|||
|
1fd34eb2a3
|
|||
|
d7ca41e946
|
|||
|
ad9fb0719a
|
|||
|
e2d315afd4
|
|||
|
6124dbfd79
|
|||
|
5327f04ec0
|
|||
|
41c56a2319
|
|||
|
f9d033542e
|
|||
|
91784e65e6
|
|||
|
9d39c6a825
|
|||
|
537e49f1a4
|
|||
|
75bbd4df71
|
|||
|
6ef4580d93
|
|||
|
6ffd498761
|
|||
|
27157e7cc1
|
|||
|
bbb07d574a
|
|||
|
c660a419e2
|
|||
|
c3f61467c8
|
|||
|
9bc47df452
|
|||
|
9ef8ca4d45
|
|||
|
b55cbd08d1
|
|||
|
8c6bd0feaa
|
|||
|
7dd4b20628
|
|||
|
fec0cb7640
|
|||
|
75deb04a2b
|
|||
|
7c7ed21a96
|
|||
|
a201f20793
|
|||
|
598c51bc1a
|
|||
|
e1ea61c5f1
|
|||
|
ac424bde36
|
|||
|
b43b70df3f
|
|||
|
4321aa621a
|
|||
|
d5d275fb43
|
|||
|
6bb3307144
|
|||
|
391604d4a2
|
|||
|
1d9361c12f
|
|||
|
a129b9cdd0
|
|||
|
3bf815ac61
|
|||
|
77bae4d6fd
|
|||
|
0a301c4c9b
|
|||
|
27b390a51c
|
|||
|
018386d14e
|
|||
|
3825d7c6c7
|
|||
|
bf930291e4
|
|||
|
a8c4988790
|
|||
|
28dd204b1a
|
|||
|
3cbc1a59a7
|
|||
|
277e9ae3d1
|
|||
|
27b3ca25b7
|
|||
|
f871cd3b62
|
|||
|
a8a59ee30c
|
|||
|
2cd1416a13
|
|||
|
6be7dfbc61
|
|||
|
1abbd85614
|
|||
|
31ac5ad07c
|
|||
|
ae2ba495e9
|
|||
|
637aa44548
|
|||
|
44dbfc36d9
|
|||
|
5dbe7371cb
|
|||
|
6c91093198
|
|||
|
3f640b7898
|
|||
|
7db164fda6
|
|||
|
6df1d96cc9
|
|||
|
122a796f8c
|
|||
|
fbc7812a16
|
|||
|
0b1a23e81a
|
|||
|
c87e6cfe07
|
|||
|
53d51b8a0e
|
|||
|
337ae39e08
|
|||
|
8fe3a664a6
|
|||
|
3bfc0b8181
|
|||
| ac2951479b | |||
| 2bfd13d843 | |||
| 28db6f9f01 | |||
|
a4f7b8415d
|
|||
|
2255d3d591
|
|||
|
97792ae734
|
|||
|
a5d13250cc
|
|||
|
de9e235d0c
|
|||
|
56fb5451cd
|
|||
|
870de961f5
|
|||
|
22bf6d1c33
|
|||
|
5b62f89531
|
|||
|
b1326d8f04
|
|||
|
fffca4a78c
|
|||
|
42bd7f97cb
|
|||
| 6377856ae0 | |||
| 0f1c52b9e3 | |||
| 6ed6f60fbc | |||
| e65a414065 | |||
| 214d5c4a53 | |||
| fe33931304 | |||
| 113309257e | |||
| b95a8b2ed2 | |||
|
|
e922971a5e | ||
| 9d5b71bead | |||
| 890efa2efb | |||
| 674097e425 | |||
|
3379dcb7f3
|
|||
| eb5a849e1f | |||
|
4981a23e8e
|
|||
|
c64d4bacb6
|
|||
|
838d18013b
|
|||
|
3f7902e463
|
|||
|
54560ad5d8
|
|||
|
0c729db639
|
|||
|
1fbaac8d88
|
|||
|
b9dc724f0b
|
@@ -14,13 +14,13 @@ The backend of the Solar Network is written in Go and is a microservices app. Th
|
|||||||
|
|
||||||
## Commit Messages
|
## Commit Messages
|
||||||
|
|
||||||
We're using the gitmoji to clarify the reason and changes of the commit. To learn more about gitmoji, visit https://gitmoji.dev
|
We're using the gitmoji to clarify the reason and changes of the commit. To learn more about gitmoji, visit <https://gitmoji.dev>
|
||||||
|
|
||||||
All the commit message should follow `:[gitmoji]: <commit message>` syntax
|
All the commit message should follow `:[gitmoji]: <commit message>` syntax
|
||||||
|
|
||||||
## Translations & Localization
|
## Translations & Localization
|
||||||
|
|
||||||
We're not accepting translation and localization improvements, or fixes on the GitHub or Solsynth Git Repository. If you want to contribute to those, please head to our Crowdin project: https://crowdin.com/project/solian
|
We're not accepting translation and localization improvements, or fixes on the GitHub or Solsynth Git Repository. If you want to contribute to those, please head to our Crowdin project: <https://crowdin.com/project/solian>
|
||||||
|
|
||||||
## New Features
|
## New Features
|
||||||
|
|
||||||
@@ -30,7 +30,12 @@ To contribute new features, please create an issue or mention the feature you wa
|
|||||||
|
|
||||||
Read the error message, check for the update (including pre-releases), and wiki before creating an issue. At the same time, be respectful and don't argue with our developers and contributors in the development chat or GitHub issue. Otherwise your issue may got deleted and your Solar Network Account may got a strike.
|
Read the error message, check for the update (including pre-releases), and wiki before creating an issue. At the same time, be respectful and don't argue with our developers and contributors in the development chat or GitHub issue. Otherwise your issue may got deleted and your Solar Network Account may got a strike.
|
||||||
|
|
||||||
|
## Styles of Code
|
||||||
|
|
||||||
|
Before you create a Pull Request, make sure your code has pass the `flutter analyze` check, if there is any notes, fix as much as possible, if there is no way to fix, do ignore.
|
||||||
|
|
||||||
|
When the code contains comments, use English. We do not any other language of comments existing in the codebase. It might confuse future contributors, cause the code hard to understand and maintaiance.
|
||||||
|
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
We appreciate every single commit you contributed. Let's work together and create a better Solar Network!
|
We appreciate every single commit you contributed. Let's work together and create a better Solar Network!
|
||||||
|
|
||||||
|
|||||||
@@ -75,3 +75,4 @@ dependencies {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,12 @@
|
|||||||
<data android:scheme="http" android:host="solian.app" />
|
<data android:scheme="http" android:host="solian.app" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="solian" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Share Intent Filters -->
|
<!-- Share Intent Filters -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|||||||
@@ -48,6 +48,8 @@
|
|||||||
"deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
|
"deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
|
||||||
"deletePost": "Delete Post",
|
"deletePost": "Delete Post",
|
||||||
"deletePostHint": "Are you sure to delete this post?",
|
"deletePostHint": "Are you sure to delete this post?",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
"copyLink": "Copy Link",
|
"copyLink": "Copy Link",
|
||||||
"postCreateAccountTitle": "Thanks for joining!",
|
"postCreateAccountTitle": "Thanks for joining!",
|
||||||
"postCreateAccountNext": "What's next?",
|
"postCreateAccountNext": "What's next?",
|
||||||
@@ -133,6 +135,11 @@
|
|||||||
"reactionPositive": "Postive",
|
"reactionPositive": "Postive",
|
||||||
"reactionNegative": "Negative",
|
"reactionNegative": "Negative",
|
||||||
"reactionNeutral": "Neutral",
|
"reactionNeutral": "Neutral",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
"connectionConnected": "Connected",
|
"connectionConnected": "Connected",
|
||||||
"connectionDisconnected": "Disconnected",
|
"connectionDisconnected": "Disconnected",
|
||||||
"connectionReconnecting": "Reconnecting",
|
"connectionReconnecting": "Reconnecting",
|
||||||
@@ -164,8 +171,8 @@
|
|||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "Good Luck",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "Best Luck",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
||||||
"eventCalander": "Event Calander",
|
"eventCalendar": "Event Calendar",
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
@@ -251,11 +258,16 @@
|
|||||||
"translatorBadgeName": "Translator",
|
"translatorBadgeName": "Translator",
|
||||||
"translatorBadgeDescription": "Helping translate Solar Network into different languages",
|
"translatorBadgeDescription": "Helping translate Solar Network into different languages",
|
||||||
"wallet": "Wallet",
|
"wallet": "Wallet",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
"walletCurrencyPoints": "New Solar Points",
|
"walletCurrencyPoints": "New Solar Points",
|
||||||
"walletCurrencyShortPoints": "NSP",
|
"walletCurrencyShortPoints": "NSP",
|
||||||
"walletCurrencyGolds": "The Solar Dollars",
|
"walletCurrencyGolds": "The Solar Dollars",
|
||||||
"walletCurrencyShortGolds": "NSD",
|
"walletCurrencyShortGolds": "NSD",
|
||||||
"retry": "Retry",
|
|
||||||
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
|
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
|
||||||
"relationships": "Relationships",
|
"relationships": "Relationships",
|
||||||
"addFriend": "Send a Friend Request",
|
"addFriend": "Send a Friend Request",
|
||||||
@@ -306,6 +318,8 @@
|
|||||||
"settingsBackgroundImageClear": "Clear Background Image",
|
"settingsBackgroundImageClear": "Clear Background Image",
|
||||||
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
|
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
|
||||||
"messageNone": "No content to display",
|
"messageNone": "No content to display",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
"unreadMessages": {
|
"unreadMessages": {
|
||||||
"one": "{} unread message",
|
"one": "{} unread message",
|
||||||
"other": "{} unread messages"
|
"other": "{} unread messages"
|
||||||
@@ -319,6 +333,7 @@
|
|||||||
"settingsAprilFoolFeatures": "April Fool Features",
|
"settingsAprilFoolFeatures": "April Fool Features",
|
||||||
"settingsEnterToSend": "Enter to Send",
|
"settingsEnterToSend": "Enter to Send",
|
||||||
"settingsTransparentAppBar": "Transparent App Bar",
|
"settingsTransparentAppBar": "Transparent App Bar",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
"settingsCustomFonts": "Custom Fonts",
|
"settingsCustomFonts": "Custom Fonts",
|
||||||
"settingsCustomFontsHint": "Custom fonts will be used for all text in the app. Make sure it is installed on your device.",
|
"settingsCustomFontsHint": "Custom fonts will be used for all text in the app. Make sure it is installed on your device.",
|
||||||
"settingsColorScheme": "Color Scheme",
|
"settingsColorScheme": "Color Scheme",
|
||||||
@@ -366,7 +381,6 @@
|
|||||||
"authFactorSecretHint": "Create an secret for this factor.",
|
"authFactorSecretHint": "Create an secret for this factor.",
|
||||||
"authFactorQrCodeScan": "Scan this QR code with your authenticator app to set up TOTP authentication",
|
"authFactorQrCodeScan": "Scan this QR code with your authenticator app to set up TOTP authentication",
|
||||||
"authFactorNoQrCode": "No QR code available for this authentication factor",
|
"authFactorNoQrCode": "No QR code available for this authentication factor",
|
||||||
"cancel": "Cancel",
|
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"authFactorAdditional": "One more step",
|
"authFactorAdditional": "One more step",
|
||||||
"authFactorHint": "Contact method hint",
|
"authFactorHint": "Contact method hint",
|
||||||
@@ -391,6 +405,10 @@
|
|||||||
"other": "{} are typing..."
|
"other": "{} are typing..."
|
||||||
},
|
},
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
"settingsServer": "Server",
|
"settingsServer": "Server",
|
||||||
"settingsBehavior": "Behavior",
|
"settingsBehavior": "Behavior",
|
||||||
"settingsDesktop": "Desktop",
|
"settingsDesktop": "Desktop",
|
||||||
@@ -461,7 +479,6 @@
|
|||||||
"accountProfileView": "View Profile",
|
"accountProfileView": "View Profile",
|
||||||
"unspecified": "Unspecified",
|
"unspecified": "Unspecified",
|
||||||
"added": "Added",
|
"added": "Added",
|
||||||
"preview": "Preview",
|
|
||||||
"togglePreview": "Toggle Preview",
|
"togglePreview": "Toggle Preview",
|
||||||
"subscribe": "Subscribe",
|
"subscribe": "Subscribe",
|
||||||
"unsubscribe": "Unsubscribe",
|
"unsubscribe": "Unsubscribe",
|
||||||
@@ -472,6 +489,7 @@
|
|||||||
"pinCode": "PIN Code",
|
"pinCode": "PIN Code",
|
||||||
"biometric": "Biometric",
|
"biometric": "Biometric",
|
||||||
"enterPinToConfirm": "Enter your 6-digit PIN to confirm payment",
|
"enterPinToConfirm": "Enter your 6-digit PIN to confirm payment",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
"clearPin": "Clear PIN",
|
"clearPin": "Clear PIN",
|
||||||
"useBiometricToConfirm": "Use biometric authentication to confirm payment",
|
"useBiometricToConfirm": "Use biometric authentication to confirm payment",
|
||||||
"touchSensorToAuthenticate": "Touch the sensor to authenticate",
|
"touchSensorToAuthenticate": "Touch the sensor to authenticate",
|
||||||
@@ -621,6 +639,10 @@
|
|||||||
"chatNotJoined": "You have not joined this chat yet.",
|
"chatNotJoined": "You have not joined this chat yet.",
|
||||||
"chatUnableJoin": "You can't join this chat due to it's access control settings.",
|
"chatUnableJoin": "You can't join this chat due to it's access control settings.",
|
||||||
"chatJoin": "Join the Chat",
|
"chatJoin": "Join the Chat",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
"realmJoin": "Join the Realm",
|
"realmJoin": "Join the Realm",
|
||||||
"realmJoinSuccess": "Successfully joined the realm.",
|
"realmJoinSuccess": "Successfully joined the realm.",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
@@ -849,6 +871,7 @@
|
|||||||
"pollShortTextAnswerPreview": "Short text answer (preview)",
|
"pollShortTextAnswerPreview": "Short text answer (preview)",
|
||||||
"award": "Award",
|
"award": "Award",
|
||||||
"awardPost": "Award Post",
|
"awardPost": "Award Post",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
"awardMessage": "Message",
|
"awardMessage": "Message",
|
||||||
"awardMessageHint": "Enter your award message...",
|
"awardMessageHint": "Enter your award message...",
|
||||||
"awardAttitude": "Attitude",
|
"awardAttitude": "Attitude",
|
||||||
@@ -1005,6 +1028,10 @@
|
|||||||
"searchLinks": "Links",
|
"searchLinks": "Links",
|
||||||
"searchAttachments": "Attachments",
|
"searchAttachments": "Attachments",
|
||||||
"noMessagesFound": "No messages found",
|
"noMessagesFound": "No messages found",
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"retry": "Retry",
|
||||||
"openInBrowser": "Open in Browser",
|
"openInBrowser": "Open in Browser",
|
||||||
"highlightPost": "Highlight Post",
|
"highlightPost": "Highlight Post",
|
||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
@@ -1043,6 +1070,7 @@
|
|||||||
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||||
"parseIframe": "Parse Iframe",
|
"parseIframe": "Parse Iframe",
|
||||||
"messageActions": "Message Actions",
|
"messageActions": "Message Actions",
|
||||||
|
"messageContent": "Message Content",
|
||||||
"viewEmbedLoadHint": "Tap to load",
|
"viewEmbedLoadHint": "Tap to load",
|
||||||
"levelingStage1": "Novice",
|
"levelingStage1": "Novice",
|
||||||
"levelingStage2": "Apprentice",
|
"levelingStage2": "Apprentice",
|
||||||
@@ -1076,5 +1104,203 @@
|
|||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload"
|
"upload": "Upload",
|
||||||
|
"updateAvailable": "Update available",
|
||||||
|
"noChangelogProvided": "No changelog provided.",
|
||||||
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
|
"installUpdate": "Install update",
|
||||||
|
"openReleasePage": "Open release page",
|
||||||
|
"postCompose": "Compose Post",
|
||||||
|
"postPublish": "Publish Post",
|
||||||
|
"restoreDraftTitle": "Restore Draft",
|
||||||
|
"restoreDraftMessage": "A draft was found. Do you want to restore it?",
|
||||||
|
"draft": "Draft",
|
||||||
|
"purchaseGift": "Purchase Gift",
|
||||||
|
"selectRecipient": "Select Recipient",
|
||||||
|
"changeRecipient": "Change Recipient",
|
||||||
|
"addMessage": "Add Message",
|
||||||
|
"skipRecipient": "Skip Recipient",
|
||||||
|
"giftSubscriptions": "Gift Subscriptions",
|
||||||
|
"purchaseAGift": "Purchase a Gift",
|
||||||
|
"redeemAGift": "Redeem a Gift",
|
||||||
|
"giftHistory": "Gift History",
|
||||||
|
"sentGifts": "Sent Gifts",
|
||||||
|
"receivedGifts": "Received Gifts",
|
||||||
|
"noSentGifts": "No sent gifts",
|
||||||
|
"noReceivedGifts": "No received gifts",
|
||||||
|
"stellarGift": "Stellar Gift",
|
||||||
|
"novaGift": "Nova Gift",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"preview": "Preview",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Function Call",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"fabLocation": "FAB Location"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -940,7 +940,7 @@
|
|||||||
"editBot": "编辑机器人",
|
"editBot": "编辑机器人",
|
||||||
"botAutomatedBy": "由 {} 自动化",
|
"botAutomatedBy": "由 {} 自动化",
|
||||||
"botDetails": "机器人详情",
|
"botDetails": "机器人详情",
|
||||||
"overview": "总揽",
|
"overview": "总览",
|
||||||
"keys": "密钥",
|
"keys": "密钥",
|
||||||
"botNotFound": "机器人未找到。",
|
"botNotFound": "机器人未找到。",
|
||||||
"newBotKey": "新建密钥",
|
"newBotKey": "新建密钥",
|
||||||
@@ -1060,7 +1060,7 @@
|
|||||||
"selectPool": "选择储存池",
|
"selectPool": "选择储存池",
|
||||||
"choosePool": "选择一个储存池",
|
"choosePool": "选择一个储存池",
|
||||||
"errorLoadingPools": "加载池时出错",
|
"errorLoadingPools": "加载池时出错",
|
||||||
"quotaCostInfo": "此上传将消耗{} 配额点",
|
"quotaCostInfo": "此上传将消耗 {} 配额点",
|
||||||
"uploadConstraints": "上传限制",
|
"uploadConstraints": "上传限制",
|
||||||
"fileSizeExceeded": "文件大小超过了 {} 的最大限制",
|
"fileSizeExceeded": "文件大小超过了 {} 的最大限制",
|
||||||
"fileTypeNotAccepted": "此储存池不接受该文件类型",
|
"fileTypeNotAccepted": "此储存池不接受该文件类型",
|
||||||
@@ -1075,5 +1075,20 @@
|
|||||||
"deleteRecycledFiles": "删除被回收的文件",
|
"deleteRecycledFiles": "删除被回收的文件",
|
||||||
"recycledFilesDeleted": "被回收文件成功删除",
|
"recycledFilesDeleted": "被回收文件成功删除",
|
||||||
"failedToDeleteRecycledFiles": "删除被回收文件失败",
|
"failedToDeleteRecycledFiles": "删除被回收文件失败",
|
||||||
"upload": "上传"
|
"upload": "上传",
|
||||||
|
"systemWallet": "中央统筹",
|
||||||
|
"postCompose": "撰写帖子",
|
||||||
|
"postPublish": "发布帖子",
|
||||||
|
"restoreDraftTitle": "恢复草稿",
|
||||||
|
"restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?",
|
||||||
|
"draft": "草稿",
|
||||||
|
"thoughtDefaultTopic": "寻思",
|
||||||
|
"thoughtAiName": "SN 酱",
|
||||||
|
"thoughtUserName": "您",
|
||||||
|
"thoughtStreamingHint": "SN 酱正在思考...",
|
||||||
|
"thoughtInputHint": "问 SN 酱任何问题...",
|
||||||
|
"thoughtNewConversation": "开始新对话",
|
||||||
|
"thoughtParseError": "解析 AI 响应失败",
|
||||||
|
"aiThought": "寻思",
|
||||||
|
"aiThoughtTitle": "让 SN 酱寻思寻思"
|
||||||
}
|
}
|
||||||
@@ -1,169 +1,169 @@
|
|||||||
{
|
{
|
||||||
"login": "Login",
|
"login": "登入",
|
||||||
"loginDescription": "Existing user? We're welcome you back!",
|
"loginDescription": "常客乎?僅盼榮歸!",
|
||||||
"forgotPassword": "Forgot password",
|
"forgotPassword": "密語遺乎",
|
||||||
"loginPickFactor": "Pick a factor",
|
"loginPickFactor": "擇一信物以証",
|
||||||
"loginMultiFactor": {
|
"loginMultiFactor": {
|
||||||
"one": "{} step left",
|
"one": "尚餘 {} 步",
|
||||||
"other": "{} steps left"
|
"other": "尚餘 {} 步"
|
||||||
},
|
},
|
||||||
"loginEnterPassword": "Enter the code",
|
"loginEnterPassword": "請輸入驗證碼",
|
||||||
"loginSuccess": "Logged in as {}",
|
"loginSuccess": "{},恭迎尊駕",
|
||||||
"loginGreeting": "Welcome back!",
|
"loginGreeting": "欣見再臨!",
|
||||||
"loginOr": "Or login with\nthird parties",
|
"loginOr": "或借第三方登入",
|
||||||
"loginInProgress": "Logging you in...",
|
"loginInProgress": "引君入內……",
|
||||||
"username": "Username",
|
"username": "用戶名",
|
||||||
"usernameCannotChangeHint": "Username cannot be updated after created.",
|
"usernameCannotChangeHint": "用户名立,则如石刻,不可改也。",
|
||||||
"usernameLookupHint": "We also take your email address.",
|
"usernameLookupHint": "另需電郵地址,以便尺素往来。",
|
||||||
"unknown": "Unknown",
|
"unknown": "不詳",
|
||||||
"termAcceptNextWithAgree": "By continuing, you agree to our terms of services and other terms and conditions.",
|
"termAcceptNextWithAgree": "若續行,則示為閣下已允諾服務之約與諸般條件。",
|
||||||
"termAcceptLink": "Check them out",
|
"termAcceptLink": "敬請參閱",
|
||||||
"loginResetPasswordHint": "Provide your username to receive a password reset link.",
|
"loginResetPasswordHint": "請賜示尊號,當奉密鑰重置之途徑。",
|
||||||
"password": "Password",
|
"password": "密語",
|
||||||
"next": "Next",
|
"next": "進",
|
||||||
"createAccount": "Create an Account",
|
"createAccount": "開立新戶",
|
||||||
"createAccountDescription": "New to here? We got you covered!",
|
"createAccountDescription": "初臨寶地?無須多慮,自有安排!",
|
||||||
"nickname": "Nickname",
|
"nickname": "別號",
|
||||||
"email": "Email",
|
"email": "電郵地址",
|
||||||
"bio": "Bio",
|
"bio": "自述",
|
||||||
"fieldCannotBeEmpty": "This field cannot be empty.",
|
"fieldCannotBeEmpty": "此域空空如也,請填補之。",
|
||||||
"fieldEmailAddressMustBeValid": "The email address must be valid.",
|
"fieldEmailAddressMustBeValid": "電郵地址務必有效。",
|
||||||
"logout": "Logout",
|
"logout": "離",
|
||||||
"updateYourProfile": "Profile Settings",
|
"updateYourProfile": "個人檔案設置",
|
||||||
"accountBasicInfo": "Basic Info",
|
"accountBasicInfo": "基本資料",
|
||||||
"accountProfile": "Your Profile",
|
"accountProfile": "君之檔案",
|
||||||
"saveChanges": "Save Changes",
|
"saveChanges": "落定",
|
||||||
"publishers": "Publishers",
|
"publishers": "發布者",
|
||||||
"managedPublisher": "Managed Publishers",
|
"managedPublisher": "轄下發布者",
|
||||||
"createPublisher": "Create a Publisher",
|
"createPublisher": "創立發布者",
|
||||||
"createPublisherHint": "To create posts, collections, etc.",
|
"createPublisherHint": "司掌帖文、纂輯之務。",
|
||||||
"editPublisher": "Edit Publisher",
|
"editPublisher": "修訂發布者",
|
||||||
"syncPublisher": "Use Account Data",
|
"syncPublisher": "取資於戶",
|
||||||
"syncPublisherRealm": "Use Realm Data",
|
"syncPublisherRealm": "動用界域資料",
|
||||||
"create": "Create",
|
"create": "创建",
|
||||||
"update": "Update",
|
"update": "革",
|
||||||
"edit": "Edit",
|
"edit": "訂",
|
||||||
"delete": "Delete",
|
"delete": "革去",
|
||||||
"deletePublisher": "Delete Publisher",
|
"deletePublisher": "革除發布者",
|
||||||
"deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
|
"deletePublisherHint": "確乎?革除此發佈者,則其一切文翰結集,皆付之一炬。",
|
||||||
"deletePost": "Delete Post",
|
"deletePost": "焚稿",
|
||||||
"deletePostHint": "Are you sure to delete this post?",
|
"deletePostHint": "爾果欲焚此稿耶?",
|
||||||
"copyLink": "Copy Link",
|
"copyLink": "抄錄鏈接",
|
||||||
"postCreateAccountTitle": "Thanks for joining!",
|
"postCreateAccountTitle": "蒙君惠然肯來,不勝感激!",
|
||||||
"postCreateAccountNext": "What's next?",
|
"postCreateAccountNext": "其後欲行何事?",
|
||||||
"postCreateAccountNext1": "Go to your email inbox and receive the account activation email.",
|
"postCreateAccountNext1": "請歸於電郵信匣,取賬戶激活之尺素。",
|
||||||
"postCreateAccountNext2": "Log in to your account and start exploring the Solar Network!",
|
"postCreateAccountNext2": "請登入賬戶,暢遊 Solar Network之浩瀚!",
|
||||||
"postPlaceholder": "What's on your mind?",
|
"postPlaceholder": "心緒何方?",
|
||||||
"publishersEmpty": "No publishers yet",
|
"publishersEmpty": "尚無發布者",
|
||||||
"publishersEmptyDescription": "You can need to create a publisher to start publishing your posts.",
|
"publishersEmptyDescription": "君需先創立發布者,方能開始發表文章。",
|
||||||
"authFactorPassword": "Password",
|
"authFactorPassword": "密語",
|
||||||
"authFactorPasswordDescription": "The password you set when you registered.",
|
"authFactorPasswordDescription": "此乃君註冊時所設之密語。",
|
||||||
"authFactorEmail": "Email verification code",
|
"authFactorEmail": "電郵驗證符",
|
||||||
"authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
|
"authFactorEmailDescription": "此一次性符令,已發於君註冊時所用之電郵地址之途。",
|
||||||
"authFactorTOTP": "Time-based OTP",
|
"authFactorTOTP": "動態一次性符令",
|
||||||
"authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
|
"authFactorTOTPDescription": "此動態一次性符令,乃由 TOTP 信物如 Google Authenticator 或 Authy 所生成。",
|
||||||
"authFactorInAppNotify": "In-app notification",
|
"authFactorInAppNotify": "應用內通告",
|
||||||
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
|
"authFactorInAppNotifyDescription": "此動態一次性符令,經由應用內通告發送。",
|
||||||
"authFactorPin": "Pin Code",
|
"authFactorPin": "定長密語",
|
||||||
"authFactorPinDescription": "It consists of 6 digits. It cannot be used to log in. When performing some dangerous operations, the system will ask you to enter this PIN for confirmation.",
|
"authFactorPinDescription": "此物凡六位數,不可用以登入。若行險要之舉,系統將請君輸入此定長數符以驗明正身。",
|
||||||
"realms": "Realms",
|
"realms": "界域",
|
||||||
"createRealm": "Create a Realm",
|
"createRealm": "始創一界域",
|
||||||
"createRealmHint": "Meet friends with same interests, build communities, and more.",
|
"createRealmHint": "結交同道,共建社羣,樂趣無窮。",
|
||||||
"editRealm": "Edit Realm",
|
"editRealm": "修訂界域訊息",
|
||||||
"deleteRealm": "Delete Realm",
|
"deleteRealm": "革去此界域",
|
||||||
"deleteRealmHint": "Are you sure to delete this realm? This will also deleted all the channels, publishers, and posts under this realm.",
|
"deleteRealmHint": "確定革去此界域乎?其下所有通道、發布者及文章亦將同歸於盡。",
|
||||||
"explore": "Explore",
|
"explore": "探索",
|
||||||
"exploreFilterSubscriptions": "Subscriptions",
|
"exploreFilterSubscriptions": "訂閱",
|
||||||
"exploreFilterFriends": "Friends",
|
"exploreFilterFriends": "知交",
|
||||||
"account": "Account",
|
"account": "賬戶",
|
||||||
"name": "Name",
|
"name": "名",
|
||||||
"slug": "Slug",
|
"slug": "別號",
|
||||||
"slugHint": "The slug will be used in the URL to access this resource, it should be unique and URL safe.",
|
"slugHint": "此別稱乃資源門徑之樞要,當為世間獨有,且不違網址安全之法度。",
|
||||||
"createChatRoom": "Create a Room",
|
"createChatRoom": "始創一談筵",
|
||||||
"editChatRoom": "Edit Room",
|
"editChatRoom": "修訂談筵",
|
||||||
"deleteChatRoom": "Delete Room",
|
"deleteChatRoom": "革去談筵",
|
||||||
"deleteChatRoomHint": "Are you sure to delete this room? This action cannot be undone.",
|
"deleteChatRoomHint": "確乎欲革去此聊天室?此舉萬劫不復。",
|
||||||
"chat": "Chat",
|
"chat": "暢談",
|
||||||
"chatTabAll": "All",
|
"chatTabAll": "總覽",
|
||||||
"chatTabDirect": "Direct Messages",
|
"chatTabDirect": "私信",
|
||||||
"chatTabGroup": "Group Chats",
|
"chatTabGroup": "群談",
|
||||||
"chatMessageHint": "Message in {}",
|
"chatMessageHint": "{} 中之訊息",
|
||||||
"chatDirectMessageHint": "Message to {}",
|
"chatDirectMessageHint": "致 {} 之訊",
|
||||||
"directMessage": "Direct Message",
|
"directMessage": "私信",
|
||||||
"loading": "Loading...",
|
"loading": "載入中……",
|
||||||
"descriptionNone": "No description yet.",
|
"descriptionNone": "未著一字。",
|
||||||
"invites": "Invites",
|
"invites": "邀函",
|
||||||
"invitesEmpty": "No invites yet, such a lonely person...",
|
"invitesEmpty": "空無邀函,形單影隻,何等寂寥……",
|
||||||
"members": {
|
"members": {
|
||||||
"one": "{} member",
|
"one": "{} 位成員",
|
||||||
"other": "{} members"
|
"other": "{} 位成員"
|
||||||
},
|
},
|
||||||
"permissionOwner": "Owner",
|
"permissionOwner": "宗主",
|
||||||
"permissionModerator": "Moderator",
|
"permissionModerator": "版正",
|
||||||
"permissionMember": "Member",
|
"permissionMember": "成員",
|
||||||
"reply": "Reply",
|
"reply": "回復",
|
||||||
"repliesCount": {
|
"repliesCount": {
|
||||||
"zero": "No reply",
|
"zero": "闃寂",
|
||||||
"one": "{} reply",
|
"one": "{} 個回復",
|
||||||
"other": "{} replies"
|
"other": "{} 個回復"
|
||||||
},
|
},
|
||||||
"forward": "Forward",
|
"forward": "傳檄",
|
||||||
"repliedTo": "Replied to",
|
"repliedTo": "已回覆",
|
||||||
"forwarded": "Forwarded",
|
"forwarded": "已轉",
|
||||||
"hasAttachments": {
|
"hasAttachments": {
|
||||||
"one": "{} attachment",
|
"one": "{} 個附件",
|
||||||
"other": "{} attachments"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
"postHasAttachments": {
|
"postHasAttachments": {
|
||||||
"one": "{} attachment",
|
"one": "{} 個附件",
|
||||||
"other": "{} attachments"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
"edited": "Edited",
|
"edited": "已修訂",
|
||||||
"addVideo": "Add video",
|
"addVideo": "附視訊",
|
||||||
"addPhoto": "Add photo",
|
"addPhoto": "附圖像",
|
||||||
"addFile": "Add file",
|
"addFile": "附卷宗",
|
||||||
"createDirectMessage": "Send new DM",
|
"createDirectMessage": "發新密訊",
|
||||||
"gotoDirectMessage": "Go to DM",
|
"gotoDirectMessage": "赴密訊",
|
||||||
"react": "React",
|
"react": "感應",
|
||||||
"reactions": {
|
"reactions": {
|
||||||
"zero": "Reactions",
|
"zero": "感應",
|
||||||
"one": "{} reaction",
|
"one": "{} 個感應",
|
||||||
"other": "{} reactions"
|
"other": "{} 個感應"
|
||||||
},
|
},
|
||||||
"reactionPositive": "Postive",
|
"reactionPositive": "嘉應",
|
||||||
"reactionNegative": "Negative",
|
"reactionNegative": "咎應",
|
||||||
"reactionNeutral": "Neutral",
|
"reactionNeutral": "中和",
|
||||||
"connectionConnected": "Connected",
|
"connectionConnected": "已聯",
|
||||||
"connectionDisconnected": "Disconnected",
|
"connectionDisconnected": "已絕",
|
||||||
"connectionReconnecting": "Reconnecting",
|
"connectionReconnecting": "復聯中",
|
||||||
"accountConnections": "Account Connections",
|
"accountConnections": "賬戶接續",
|
||||||
"accountConnectionsDescription": "Manage your external account connections",
|
"accountConnectionsDescription": "統御君之域外賬戶接續",
|
||||||
"accountConnectionAdd": "Add Connection",
|
"accountConnectionAdd": "始創一接續",
|
||||||
"accountConnectionDelete": "Delete Connection",
|
"accountConnectionDelete": "革去接續",
|
||||||
"accountConnectionDeleteHint": "Are you sure you want to delete this connection? This action cannot be undone.",
|
"accountConnectionDeleteHint": "確乎欲革去此接續?革去靈犀相接則永逝矣,不可挽回。",
|
||||||
"accountConnectionsEmpty": "No connections found. Add a connection to get started.",
|
"accountConnectionsEmpty": "未見接續。請始創一接續以啟用之。",
|
||||||
"accountConnectionProvider": "Provider",
|
"accountConnectionProvider": "供應者",
|
||||||
"accountConnectionProviderHint": "Enter provider name",
|
"accountConnectionProviderHint": "請輸入供應者名號",
|
||||||
"accountConnectionIdentifier": "Identifier",
|
"accountConnectionIdentifier": "標識符",
|
||||||
"accountConnectionIdentifierHint": "Enter your identifier for this provider",
|
"accountConnectionIdentifierHint": "請輸入君於此供應者之標識",
|
||||||
"accountConnectionDescription": "Add a connection to link your account with external services.",
|
"accountConnectionDescription": "締結靈契,以通聯君之戶牘與域外服務。",
|
||||||
"accountConnectionAddSuccess": "Connection added successfully.",
|
"accountConnectionAddSuccess": "靈犀已締。",
|
||||||
"accountConnectionAddError": "Unable to setup connection.",
|
"accountConnectionAddError": "靈犀難通。",
|
||||||
"accountConnectionProviderApple": "Apple",
|
"accountConnectionProviderApple": "蘋果",
|
||||||
"accountConnectionProviderMicrosoft": "Microsoft",
|
"accountConnectionProviderMicrosoft": "微軟",
|
||||||
"accountConnectionProviderGoogle": "Google",
|
"accountConnectionProviderGoogle": "谷歌",
|
||||||
"accountConnectionProviderGithub": "GitHub",
|
"accountConnectionProviderGithub": "Git Hub",
|
||||||
"accountConnectionProviderDiscord": "Discord",
|
"accountConnectionProviderDiscord": "Discord",
|
||||||
"accountConnectionProviderAfdian": "Afdian",
|
"accountConnectionProviderAfdian": "愛發電",
|
||||||
"checkIn": "Check In",
|
"checkIn": "簽到",
|
||||||
"checkInNone": "Not checked-in yet",
|
"checkInNone": "尚未簽到",
|
||||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
"checkInNoneHint": "簽到以獲吉運籤文日祿。",
|
||||||
"checkInResultLevel0": "Wrost Luck",
|
"checkInResultLevel0": "大凶",
|
||||||
"checkInResultLevel1": "Bad Luck",
|
"checkInResultLevel1": "凶",
|
||||||
"checkInResultLevel2": "A Normal Day",
|
"checkInResultLevel2": "中平",
|
||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "吉",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "大吉",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} 於 {} 簽到,獲 {} 籤",
|
||||||
"eventCalander": "Event Calander",
|
"eventCalander": "Event Calander",
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
"eventCalanderEmpty": "No events on that day.",
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
},
|
},
|
||||||
"postHasAttachments": {
|
"postHasAttachments": {
|
||||||
"one": "{} 個附件",
|
"one": "{} 個附件",
|
||||||
"other": "{}個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
"edited": "已編輯",
|
"edited": "已編輯",
|
||||||
"addVideo": "添加視頻",
|
"addVideo": "添加視頻",
|
||||||
@@ -753,19 +753,19 @@
|
|||||||
"markAsSensitive": "標記為敏感",
|
"markAsSensitive": "標記為敏感",
|
||||||
"fileName": "文件名",
|
"fileName": "文件名",
|
||||||
"sensitiveCategories": {
|
"sensitiveCategories": {
|
||||||
"language": "Language",
|
"language": "語言",
|
||||||
"sexualContent": "Sexual Content",
|
"sexualContent": "色情內容",
|
||||||
"violence": "Violence",
|
"violence": "暴力",
|
||||||
"profanity": "Profanity",
|
"profanity": "褻瀆",
|
||||||
"hateSpeech": "Hate Speech",
|
"hateSpeech": "仇恨言論",
|
||||||
"racism": "Racism",
|
"racism": "種族主義",
|
||||||
"adultContent": "Adult Content",
|
"adultContent": "成人內容",
|
||||||
"drugAbuse": "Drug Abuse",
|
"drugAbuse": "藥物濫用",
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
"alcoholAbuse": "酗酒",
|
||||||
"gambling": "Gambling",
|
"gambling": "賭博",
|
||||||
"selfHarm": "Self-harm",
|
"selfHarm": "自殘",
|
||||||
"childAbuse": "Child Abuse",
|
"childAbuse": "虐待兒童",
|
||||||
"other": "Other"
|
"other": "其他"
|
||||||
},
|
},
|
||||||
"poll": "投票",
|
"poll": "投票",
|
||||||
"pollsRecent": "最近投票",
|
"pollsRecent": "最近投票",
|
||||||
@@ -809,159 +809,159 @@
|
|||||||
"one": "+{} 個文件被摺疊",
|
"one": "+{} 個文件被摺疊",
|
||||||
"other": "+{} 個文件被摺疊"
|
"other": "+{} 個文件被摺疊"
|
||||||
},
|
},
|
||||||
"pollQuestions": "Questions",
|
"pollQuestions": "問題",
|
||||||
"pollAnswerSubmitted": "Poll answer has been submitted.",
|
"pollAnswerSubmitted": "投票答案已提交。",
|
||||||
"modifyAnswers": "Modify Answers",
|
"modifyAnswers": "修改答案",
|
||||||
"back": "Back",
|
"back": "返回",
|
||||||
"submit": "Submit",
|
"submit": "提交",
|
||||||
"pollOptionDefaultLabel": "Option 1",
|
"pollOptionDefaultLabel": "選項1",
|
||||||
"pollUpdated": "Poll updated.",
|
"pollUpdated": "投票已更新。",
|
||||||
"pollCreated": "Poll created.",
|
"pollCreated": "投票已創建。",
|
||||||
"pollCreate": "Create Poll",
|
"pollCreate": "創建投票",
|
||||||
"pollEdit": "Edit Poll",
|
"pollEdit": "編輯投票",
|
||||||
"pollPreviewJsonDebug": "Debug Preview",
|
"pollPreviewJsonDebug": "調試預覽",
|
||||||
"pollTitleRequired": "Title is required",
|
"pollTitleRequired": "標題不可為空",
|
||||||
"pollEndDateOptional": "End date & time (optional)",
|
"pollEndDateOptional": "結束日期和時間 (可選)",
|
||||||
"notSet": "Not set",
|
"notSet": "未設定",
|
||||||
"pick": "Pick",
|
"pick": "選擇",
|
||||||
"clear": "Clear",
|
"clear": "清除",
|
||||||
"questions": "Questions",
|
"questions": "問題",
|
||||||
"pollAddQuestion": "Add question",
|
"pollAddQuestion": "添加問題",
|
||||||
"pollQuestionTypeSingleChoice": "Single choice",
|
"pollQuestionTypeSingleChoice": "單選框",
|
||||||
"pollQuestionTypeMultipleChoice": "Multiple choice",
|
"pollQuestionTypeMultipleChoice": "多選框",
|
||||||
"pollQuestionTypeFreeText": "Free text",
|
"pollQuestionTypeFreeText": "自由文本",
|
||||||
"pollQuestionTypeYesNo": "Yes / No",
|
"pollQuestionTypeYesNo": "是 / 不是",
|
||||||
"pollQuestionTypeRating": "Rating",
|
"pollQuestionTypeRating": "評分",
|
||||||
"pollNoQuestionsYet": "No questions yet",
|
"pollNoQuestionsYet": "尚未有問題",
|
||||||
"pollNoQuestionsHint": "Use \"Add question\" to start building your poll.",
|
"pollNoQuestionsHint": "使用「添加問題」開始建立您的投票。",
|
||||||
"pollDebugPreview": "Debug Preview",
|
"pollDebugPreview": "調試預覽",
|
||||||
"pollUntitledQuestion": "Untitled question",
|
"pollUntitledQuestion": "無標題問題",
|
||||||
"moveUp": "Move up",
|
"moveUp": "往上移動",
|
||||||
"moveDown": "Move down",
|
"moveDown": "往下移動",
|
||||||
"required": "Required",
|
"required": "必需的",
|
||||||
"pollQuestionTitle": "Question title",
|
"pollQuestionTitle": "問題標題",
|
||||||
"pollQuestionTitleRequired": "Question title is required",
|
"pollQuestionTitleRequired": "問題標題是必需的",
|
||||||
"pollQuestionDescriptionOptional": "Question description (optional)",
|
"pollQuestionDescriptionOptional": "問題描述(選填)",
|
||||||
"options": "Options",
|
"options": "選項",
|
||||||
"pollAddOption": "Add option",
|
"pollAddOption": "添加選項",
|
||||||
"pollOptionLabel": "Option label",
|
"pollOptionLabel": "選項標籤",
|
||||||
"pollLongTextAnswerPreview": "Long text answer (preview)",
|
"pollLongTextAnswerPreview": "長文本答案 (預覽)",
|
||||||
"pollShortTextAnswerPreview": "Short text answer (preview)",
|
"pollShortTextAnswerPreview": "短文本答案 (預覽)",
|
||||||
"award": "Award",
|
"award": "讚賞",
|
||||||
"awardPost": "Award Post",
|
"awardPost": "讚賞帖子",
|
||||||
"awardMessage": "Message",
|
"awardMessage": "消息",
|
||||||
"awardMessageHint": "Enter your award message...",
|
"awardMessageHint": "輸入您的讚賞消息...",
|
||||||
"awardAttitude": "Attitude",
|
"awardAttitude": "態度",
|
||||||
"awardAttitudePositive": "Positive",
|
"awardAttitudePositive": "積極",
|
||||||
"awardAttitudeNegative": "Negative",
|
"awardAttitudeNegative": "消极",
|
||||||
"awardAmount": "Amount",
|
"awardAmount": "金額",
|
||||||
"awardAmountHint": "Enter amount...",
|
"awardAmountHint": "輸入金額……",
|
||||||
"awardAmountRequired": "Amount is required",
|
"awardAmountRequired": "「金額」為必填字段",
|
||||||
"awardAmountInvalid": "Please enter a valid amount",
|
"awardAmountInvalid": "請輸入有效金額",
|
||||||
"awardMessageTooLong": "Message is too long (max 4096 characters)",
|
"awardMessageTooLong": "消息太長(最多4096個字符)",
|
||||||
"awardSuccess": "Award sent successfully!",
|
"awardSuccess": "獎勵已成功發送!",
|
||||||
"awardSubmit": "Award",
|
"awardSubmit": "讚賞",
|
||||||
"awardPostPreview": "Post Preview",
|
"awardPostPreview": "帖子預覽",
|
||||||
"awardNoContent": "No content available",
|
"awardNoContent": "暫無內容",
|
||||||
"awardByPublisher": "By {}",
|
"awardByPublisher": "由 {} 發表",
|
||||||
"awardBenefits": "Award Benefits",
|
"awardBenefits": "讚賞福利",
|
||||||
"awardBenefitsDescription": "Awarding this post increases its value and visibility. Higher valued posts have a better chance of being featured and highlighted in the community.",
|
"awardBenefitsDescription": "為該帖子授予獎勵可以提升其價值和曝光度。價值更高的帖子更有可能在社區中被推薦和突出顯示。",
|
||||||
"checkInResultLevel5": "Happy Birthday 🥳",
|
"checkInResultLevel5": "生日快樂 🥳",
|
||||||
"region": "Region",
|
"region": "區域",
|
||||||
"accountRegionHint": "This region will be used for content delivery and localization.",
|
"accountRegionHint": "這個區域將用於內容傳遞和本地化。",
|
||||||
"settingsCustomFontsHelper": "Use comma to seprate.",
|
"settingsCustomFontsHelper": "使用逗號分隔。",
|
||||||
"settingsBackgroundImageEnable": "顯示背景圖片",
|
"settingsBackgroundImageEnable": "顯示背景圖片",
|
||||||
"settingsDataSavingMode": "低數據模式",
|
"settingsDataSavingMode": "低數據模式",
|
||||||
"dataSavingHint": "低數據模式",
|
"dataSavingHint": "低數據模式",
|
||||||
"postTypePost": "Post",
|
"postTypePost": "帖子",
|
||||||
"searchDrafts": "Search drafts...",
|
"searchDrafts": "搜尋草稿……",
|
||||||
"noSearchResults": "No search results",
|
"noSearchResults": "無搜尋結果",
|
||||||
"contactMethodMakePublic": "Make Public",
|
"contactMethodMakePublic": "設為公開",
|
||||||
"contactMethodMakePrivate": "Make Private",
|
"contactMethodMakePrivate": "設定為僅自己可見",
|
||||||
"contactMethodPublic": "Public",
|
"contactMethodPublic": "公開",
|
||||||
"contactMethodPrivate": "Private",
|
"contactMethodPrivate": "私密",
|
||||||
"discoverRealms": "發現領域",
|
"discoverRealms": "發現領域",
|
||||||
"discoverPublishers": "發現發佈者",
|
"discoverPublishers": "發現發佈者",
|
||||||
"discoverShuffledPost": "Random Posts",
|
"discoverShuffledPost": "隨機帖子",
|
||||||
"projects": "Projects",
|
"projects": "項目",
|
||||||
"noProjects": "No projects found.",
|
"noProjects": "未找到項目。",
|
||||||
"deleteProject": "Delete Project",
|
"deleteProject": "刪除項目",
|
||||||
"deleteProjectHint": "Are you sure you want to delete this project? This action cannot be undone.",
|
"deleteProjectHint": "確定要刪除此項目嗎?此操作無法撤銷。",
|
||||||
"createProject": "Create Project",
|
"createProject": "新建專案",
|
||||||
"editProject": "Edit Project",
|
"editProject": "編輯項目",
|
||||||
"projectDetails": "Project Details",
|
"projectDetails": "專案描述",
|
||||||
"createBot": "Create Bot",
|
"createBot": "創建機器人",
|
||||||
"bots": "Bots",
|
"bots": "機器人",
|
||||||
"noBots": "No bots yet.",
|
"noBots": "還沒有機器人。",
|
||||||
"deleteBotHint": "Are you sure you want to delete this bot? This action cannot be undone.",
|
"deleteBotHint": "您確定要刪除這個機器人嗎?此操作無法撤銷。",
|
||||||
"deleteBot": "Delete Bot",
|
"deleteBot": "刪除機器人",
|
||||||
"discoverWebArticles": "來自站外的文章",
|
"discoverWebArticles": "來自站外的文章",
|
||||||
"messageJumpNotLoaded": "The referenced message was not loaded, unable to jump to it.",
|
"messageJumpNotLoaded": "引用的訊息未加載,無法跳轉到該訊息。",
|
||||||
"postUnlinkRealm": "No linked realm",
|
"postUnlinkRealm": "未連結到領域",
|
||||||
"postSlug": "Slug",
|
"postSlug": "別名",
|
||||||
"postSlugHint": "The slug can be used to access your post via URL in the webpage, it should be publisher-wide unique.",
|
"postSlugHint": "這個別名可以用於在網頁通過 URL 瀏覽到你的帖子,它應該在同一發布者中是唯一。",
|
||||||
"attachmentOnDevice": "On-device",
|
"attachmentOnDevice": "離線",
|
||||||
"attachmentOnCloud": "On-cloud",
|
"attachmentOnCloud": "在線",
|
||||||
"attachments": "Attachments",
|
"attachments": "附件",
|
||||||
"publisherCollabInvitation": "Collabration invitations",
|
"publisherCollabInvitation": "協作邀請",
|
||||||
"publisherCollabInvitationCount": {
|
"publisherCollabInvitationCount": {
|
||||||
"zero": "No invitation",
|
"zero": "無邀請",
|
||||||
"one": "{} available invitation",
|
"one": "{} 個可用邀請",
|
||||||
"other": "{} available invitations"
|
"other": "{} 個可用邀請"
|
||||||
},
|
},
|
||||||
"failedToLoadUserInfo": "Failed to load user info",
|
"failedToLoadUserInfo": "無法加載用戶資訊",
|
||||||
"failedToLoadUserInfoNetwork": "It seems be network issue, you can tap the button below to try again.",
|
"failedToLoadUserInfoNetwork": "看起來是網絡問題,您可以點擊下面的按鈕再試一次。",
|
||||||
"failedToLoadUserInfoUnauthorized": "It seems your session has been logged out or not available anymore, you can still try agian to fetch the user info if you want.",
|
"failedToLoadUserInfoUnauthorized": "看起來您的會話已經登出或不再可用,如果您想的話,您仍然可以嘗試再次獲取用戶資訊。",
|
||||||
"okay": "Okay",
|
"okay": "好的",
|
||||||
"postDetail": "Post Detail",
|
"postDetail": "帖子詳情",
|
||||||
"postCount": {
|
"postCount": {
|
||||||
"zero": "No posts",
|
"zero": "沒有帖子",
|
||||||
"one": "{} post",
|
"one": "{} 帖子",
|
||||||
"other": "{} posts"
|
"other": "{} 帖子"
|
||||||
},
|
},
|
||||||
"mimeType": "MIME Type",
|
"mimeType": "類型",
|
||||||
"fileSize": "File Size",
|
"fileSize": "文件大小",
|
||||||
"fileHash": "File Hash",
|
"fileHash": "文件哈希",
|
||||||
"exifData": "EXIF Data",
|
"exifData": "EXIF 數據",
|
||||||
"postShuffle": "Shuffle Posts",
|
"postShuffle": "隨機帖子",
|
||||||
"leveling": "Leveling",
|
"leveling": "等級",
|
||||||
"levelingHistory": "Leveling History",
|
"levelingHistory": "經驗記錄",
|
||||||
"stellarProgram": "Stellar Program",
|
"stellarProgram": "恆星計畫",
|
||||||
"socialCredits": "Social Credits",
|
"socialCredits": "社會信用點",
|
||||||
"credits": "Credits",
|
"credits": "信用",
|
||||||
"creditsStatus": "Credits Status",
|
"creditsStatus": "積分狀態",
|
||||||
"socialCreditsDescription": "Social Credit is a way for Solar Network to evaluate users. It is calculated based on their behavior and interactions. With a base score of 100, higher scores indicate a user's credibility within the community. Scores change over time to reflect a user's recent behavior. Users with higher credit ratings enjoy more benefits, while users with lower credit ratings may have some functionality restricted.",
|
"socialCreditsDescription": "社會信用是 Solar Network 評價用戶的一種方式。它基於用戶的行為和互動來計算。以 100 分為基準,分數越高表示用戶在社區中的信譽越好。分數會隨著時間的推移而變化,反映用戶的最新行為。信用等級高的用戶可以享受到更多的福利,反之的用戶部分功能可能受到限制。",
|
||||||
"socialCreditsLevelPoor": "Poor",
|
"socialCreditsLevelPoor": "糟糕",
|
||||||
"socialCreditsLevelNormal": "Normal",
|
"socialCreditsLevelNormal": "正常",
|
||||||
"socialCreditsLevelGood": "Good",
|
"socialCreditsLevelGood": "良好",
|
||||||
"socialCreditsLevelExcellent": "Excellent",
|
"socialCreditsLevelExcellent": "優秀",
|
||||||
"orderByPopularity": "Sort by popularity",
|
"orderByPopularity": "按熱度排序",
|
||||||
"orderByReleaseDate": "Sort by release date",
|
"orderByReleaseDate": "按發佈日期排序",
|
||||||
"editBot": "Edit Bot",
|
"editBot": "編輯機器人",
|
||||||
"botAutomatedBy": "Automated by {}",
|
"botAutomatedBy": "由 {} 自動化",
|
||||||
"botDetails": "Bot Details",
|
"botDetails": "機器人描述",
|
||||||
"overview": "Overview",
|
"overview": "概述",
|
||||||
"keys": "Keys",
|
"keys": "密鑰",
|
||||||
"botNotFound": "Bot not found.",
|
"botNotFound": "機器人未找到。",
|
||||||
"newBotKey": "New Bot Key",
|
"newBotKey": "新建密鑰",
|
||||||
"newBotKeyHint": "Enter a name for your new key. The key will be shown only once.",
|
"newBotKeyHint": "輸入新密鑰的名稱,密鑰只會顯示一次。",
|
||||||
"revokeBotKey": "Revoke Bot Key",
|
"revokeBotKey": "撤銷密鑰",
|
||||||
"revokeBotKeyHint": "Are you sure you want to revoke this key? This action cannot be undone and any application using this key will stop working.",
|
"revokeBotKeyHint": "你確定要撤銷這個密鑰?這個操作無法撤回,所有使用該密鑰的應用程式會停止工作。",
|
||||||
"noBotKeys": "No bot keys yet.",
|
"noBotKeys": "機器人未找到。",
|
||||||
"revoke": "Revoke",
|
"revoke": "撤銷",
|
||||||
"keyName": "Key Name",
|
"keyName": "密鑰名稱",
|
||||||
"newKeyGenerated": "New Key Generated",
|
"newKeyGenerated": "新密鑰已生成",
|
||||||
"copyKeyHint": "Please copy this key and store it somewhere safe. You will not be able to see it again.",
|
"copyKeyHint": "請安全地保存該密鑰,你不會再次看到它。",
|
||||||
"rotateKey": "Rotate Key",
|
"rotateKey": "旋轉密鑰",
|
||||||
"rotateBotKey": "Rotate Bot Key",
|
"rotateBotKey": "旋轉密鑰",
|
||||||
"rotateBotKeyHint": "Are you sure you want to rotate this key? The old key will become invalid immediately. This action cannot be undone.",
|
"rotateBotKeyHint": "你確認要旋轉這個密鑰?久的密鑰會立即失效,該操作無法撤銷。",
|
||||||
"webFeedArticleCount": {
|
"webFeedArticleCount": {
|
||||||
"zero": "No articles",
|
"zero": "無文章",
|
||||||
"one": "{} article",
|
"one": "{} 文章",
|
||||||
"other": "{} articles"
|
"other": "{} 文章"
|
||||||
},
|
},
|
||||||
"webFeedSubscribed": "The feed has been subscribed",
|
"webFeedSubscribed": "你已經訂閱了這個來源",
|
||||||
"webFeedUnsubscribed": "The feed has been unsubscribed",
|
"webFeedUnsubscribed": "你已經取消訂閱這個來源",
|
||||||
"appDetails": "應用程式詳情",
|
"appDetails": "應用程式詳情",
|
||||||
"secrets": "密鑰",
|
"secrets": "密鑰",
|
||||||
"appNotFound": "找不到應用程式。",
|
"appNotFound": "找不到應用程式。",
|
||||||
@@ -974,106 +974,108 @@
|
|||||||
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
|
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
|
||||||
"expiresIn": "過期時間(秒)",
|
"expiresIn": "過期時間(秒)",
|
||||||
"isOidc": "OIDC 相容",
|
"isOidc": "OIDC 相容",
|
||||||
"pinPost": "Pin Post",
|
"pinPost": "置頂帖子",
|
||||||
"unpinPost": "Unpin Post",
|
"unpinPost": "取消置頂",
|
||||||
"pinnedPost": "Pinned",
|
"pinnedPost": "已置顶",
|
||||||
"publisherPage": "Publisher Page",
|
"publisherPage": "發布者頁面",
|
||||||
"realmPage": "Realm Page",
|
"realmPage": "領域頁面",
|
||||||
"replyPage": "Reply Page",
|
"replyPage": "回覆頁面",
|
||||||
"pinPostPublisherHint": "Pin this post to your publisher page",
|
"pinPostPublisherHint": "將這篇文章置顶到您的發佈者頁面",
|
||||||
"pinPostRealmHint": "Pin this post to the realm page",
|
"pinPostRealmHint": "將這篇文章置顶到領域頁面",
|
||||||
"pinPostRealmDisabledHint": "This post doesn't belong to any realm",
|
"pinPostRealmDisabledHint": "這個帖子不屬於任何領域",
|
||||||
"pinPostReplyHint": "Pin this post to the reply page",
|
"pinPostReplyHint": "將這篇文章置顶到回覆頁面",
|
||||||
"pinPostReplyDisabledHint": "This post is not a reply",
|
"pinPostReplyDisabledHint": "這篇帖子不是回覆",
|
||||||
"pin": "Pin",
|
"pin": "置顶",
|
||||||
"unpinPostHint": "Are you sure you want to unpin this post?",
|
"unpinPostHint": "你確定要取消置顶這篇帖子嗎?",
|
||||||
"all": "All",
|
"all": "所有",
|
||||||
"statusPresent": "Present",
|
"statusPresent": "至今",
|
||||||
"accountAutomated": "Automated",
|
"accountAutomated": "機器人",
|
||||||
"chatBreakClearButton": "Clear",
|
"chatBreakClearButton": "清除",
|
||||||
"chatBreak5m": "5m",
|
"chatBreak5m": "5 分鐘",
|
||||||
"chatBreak10m": "10m",
|
"chatBreak10m": "10 分鐘",
|
||||||
"chatBreak15m": "15m",
|
"chatBreak15m": "15 分鐘",
|
||||||
"chatBreak30m": "30m",
|
"chatBreak30m": "30 分鐘",
|
||||||
"chatBreakCustomMinutes": "Custom (minutes)",
|
"chatBreakCustomMinutes": "自訂(分鐘)",
|
||||||
"errorGeneric": "Error: {}",
|
"errorGeneric": "錯誤:{}",
|
||||||
"searchMessages": "Search Messages",
|
"searchMessages": "搜尋消息",
|
||||||
"messagesCount": "{} messages",
|
"messagesCount": "{} 消息",
|
||||||
"dotSeparator": "·",
|
"dotSeparator": ".",
|
||||||
"roleValidationHint": "Role must be between 0 and 100",
|
"roleValidationHint": "成員角色必須設置在0到100之間",
|
||||||
"searchMessagesHint": "Search messages...",
|
"searchMessagesHint": "搜尋消息…",
|
||||||
"searchLinks": "Links",
|
"searchLinks": "連結",
|
||||||
"searchAttachments": "Attachments",
|
"searchAttachments": "附件",
|
||||||
"noMessagesFound": "No messages found",
|
"noMessagesFound": "未找到消息",
|
||||||
"openInBrowser": "Open in Browser",
|
"openInBrowser": "在瀏覽器打開",
|
||||||
"highlightPost": "Highlight Post",
|
"highlightPost": "精選帖子",
|
||||||
"filters": "Filters",
|
"filters": "過濾器",
|
||||||
"apply": "Apply",
|
"apply": "應用",
|
||||||
"pubName": "Pub Name",
|
"pubName": "題目名稱",
|
||||||
"realm": "Realm",
|
"realm": "領域",
|
||||||
"shuffle": "Shuffle",
|
"shuffle": "隨機",
|
||||||
"pinned": "Pinned",
|
"pinned": "已置顶",
|
||||||
"noResultsFound": "No results found",
|
"noResultsFound": "未找到結果",
|
||||||
"toggleFilters": "Toggle filters",
|
"toggleFilters": "切換篩檢器",
|
||||||
"notableDayNext": "{} is in",
|
"notableDayNext": "距離 {} 還有",
|
||||||
"expandPoll": "Expand Poll",
|
"expandPoll": "展開投票",
|
||||||
"collapsePoll": "Collapse Poll",
|
"collapsePoll": "摺叠投票",
|
||||||
"embedView": "Embed View",
|
"embedView": "嵌入視圖",
|
||||||
"embedUri": "Embed URI",
|
"embedUri": "嵌入URL",
|
||||||
"aspectRatio": "Aspect Ratio",
|
"aspectRatio": "縱橫比",
|
||||||
"renderer": "Renderer",
|
"renderer": "渲染器",
|
||||||
"addEmbed": "Add Embed",
|
"addEmbed": "添加嵌入",
|
||||||
"editEmbed": "Edit Embed",
|
"editEmbed": "編輯嵌入",
|
||||||
"deleteEmbed": "Delete Embed",
|
"deleteEmbed": "刪除嵌入",
|
||||||
"deleteEmbedConfirm": "Are you sure you want to delete this embed?",
|
"deleteEmbedConfirm": "您確定要刪除這個嵌入嗎?",
|
||||||
"currentEmbed": "Current Embed",
|
"currentEmbed": "當前嵌入",
|
||||||
"noEmbed": "No embed yet",
|
"noEmbed": "尚未嵌入",
|
||||||
"save": "Save",
|
"save": "保存",
|
||||||
"webView": "Web View",
|
"webView": "網頁視圖",
|
||||||
"settingsDefaultPool": "Default file pool",
|
"settingsDefaultPool": "預設檔案池",
|
||||||
"settingsDefaultPoolHelper": "Select the default storage pool for file uploads",
|
"settingsDefaultPoolHelper": "選擇文件上傳的默認儲存池",
|
||||||
"uploadFile": "Upload File",
|
"uploadFile": "上傳檔案",
|
||||||
"authDeviceChallenges": "Device Usage",
|
"authDeviceChallenges": "設備活動",
|
||||||
"authDeviceHint": "Swipe left to edit label, swipe right to logout device.",
|
"authDeviceHint": "向左滑動以編輯標籤,向右滑動以登出設備。",
|
||||||
"settingsMessageDisplayStyle": "Message Display Style",
|
"settingsMessageDisplayStyle": "訊息顯示樣式",
|
||||||
"auto": "Auto",
|
"auto": "自動",
|
||||||
"manual": "Manual",
|
"manual": "手動",
|
||||||
"iframeCode": "Iframe Code",
|
"iframeCode": "Iframe 代碼",
|
||||||
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||||
"parseIframe": "Parse Iframe",
|
"parseIframe": "解析 Iframe",
|
||||||
"messageActions": "Message Actions",
|
"messageActions": "消息選項",
|
||||||
"viewEmbedLoadHint": "Tap to load",
|
"viewEmbedLoadHint": "點擊以載入",
|
||||||
"levelingStage1": "Novice",
|
"levelingStage1": "新手",
|
||||||
"levelingStage2": "Apprentice",
|
"levelingStage2": "學徒",
|
||||||
"levelingStage3": "Journeyman",
|
"levelingStage3": "學徒工",
|
||||||
"levelingStage4": "Adept",
|
"levelingStage4": "熟練",
|
||||||
"levelingStage5": "Expert",
|
"levelingStage5": "專家",
|
||||||
"levelingStage6": "Master",
|
"levelingStage6": "大師",
|
||||||
"levelingStage7": "Grandmaster",
|
"levelingStage7": "宗師",
|
||||||
"levelingStage8": "Legend",
|
"levelingStage8": "傳說",
|
||||||
"levelingStage9": "Myth",
|
"levelingStage9": "神話",
|
||||||
"levelingStage10": "Immortal",
|
"levelingStage10": "不朽",
|
||||||
"levelingStage11": "Divine",
|
"levelingStage11": "神聖",
|
||||||
"levelingStage12": "Transcendent",
|
"levelingStage12": "超凡",
|
||||||
"uploadAttachment": "Upload Attachment",
|
"uploadAttachment": "上傳附件",
|
||||||
"attachmentPreview": "Attachment Preview",
|
"attachmentPreview": "附件預覽",
|
||||||
"selectPool": "Select Pool",
|
"selectPool": "選擇檔案池",
|
||||||
"choosePool": "Choose a pool",
|
"choosePool": "選擇一個檔案池",
|
||||||
"errorLoadingPools": "Error loading pools",
|
"errorLoadingPools": "加載池時出錯",
|
||||||
"quotaCostInfo": "This upload will cost {} quota points",
|
"quotaCostInfo": "這次上傳將消耗 {} 配額點",
|
||||||
"uploadConstraints": "Upload Constraints",
|
"uploadConstraints": "上傳限制",
|
||||||
"fileSizeExceeded": "File size exceeds the maximum limit of {}",
|
"fileSizeExceeded": "檔案大小超過了 {} 的最大限制",
|
||||||
"fileTypeNotAccepted": "File type is not accepted by this pool",
|
"fileTypeNotAccepted": "該文件類型不被此池接受",
|
||||||
"files": "Files",
|
"files": "附件",
|
||||||
"confirmDeleteFile": "Are you sure you want to delete this file?",
|
"confirmDeleteFile": "你確定要刪除這個文件嗎?",
|
||||||
"deleteFile": "Delete File",
|
"deleteFile": "刪除文件",
|
||||||
"failedToDeleteFile": "Failed to delete file",
|
"failedToDeleteFile": "刪除文件失敗",
|
||||||
"drive": "Drive",
|
"drive": "雲盤",
|
||||||
"allPools": "All Pools",
|
"allPools": "全部的池",
|
||||||
"includeRecycled": "Include Recycled",
|
"includeRecycled": "包含已回收文件",
|
||||||
"confirmDeleteRecycledFiles": "Are you sure you want to delete all recycled files?",
|
"confirmDeleteRecycledFiles": "您確定要刪除所有回收的檔案嗎?",
|
||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "刪除已回收檔案",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "已回收檔案刪除成功",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "已回收檔案刪除失敗",
|
||||||
"upload": "Upload"
|
"upload": "上傳",
|
||||||
|
"postCompose": "撰寫帖子",
|
||||||
|
"postPublish": "發佈帖子"
|
||||||
}
|
}
|
||||||
11
ios/Podfile
@@ -1,4 +1,3 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
|
||||||
platform :ios, '15.0'
|
platform :ios, '15.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
@@ -50,6 +49,16 @@ target 'Runner' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
target 'WatchRunner Watch App' do
|
||||||
|
platform :watchos, '11.0'
|
||||||
|
|
||||||
|
use_frameworks!
|
||||||
|
use_modular_headers!
|
||||||
|
|
||||||
|
pod 'Kingfisher', '~> 8.0'
|
||||||
|
pod 'KingfisherWebP'
|
||||||
|
end
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
flutter_additional_ios_build_settings(target)
|
||||||
|
|||||||
193
ios/Podfile.lock
@@ -1,5 +1,7 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.10.2)
|
- Alamofire (5.10.2)
|
||||||
|
- app_links (6.4.1):
|
||||||
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
@@ -42,83 +44,83 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- file_saver (0.0.1):
|
- file_saver (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Firebase/CoreOnly (12.2.0):
|
- Firebase/CoreOnly (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- Firebase/Crashlytics (12.2.0):
|
- Firebase/Crashlytics (12.4.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseCrashlytics (~> 12.2.0)
|
- FirebaseCrashlytics (~> 12.4.0)
|
||||||
- Firebase/Messaging (12.2.0):
|
- Firebase/Messaging (12.4.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 12.2.0)
|
- FirebaseMessaging (~> 12.4.0)
|
||||||
- firebase_analytics (12.0.2):
|
- firebase_analytics (12.0.3):
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FirebaseAnalytics (= 12.2.0)
|
- FirebaseAnalytics (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (4.1.1):
|
- firebase_core (4.2.0):
|
||||||
- Firebase/CoreOnly (= 12.2.0)
|
- Firebase/CoreOnly (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_crashlytics (5.0.2):
|
- firebase_crashlytics (5.0.3):
|
||||||
- Firebase/Crashlytics (= 12.2.0)
|
- Firebase/Crashlytics (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (16.0.2):
|
- firebase_messaging (16.0.3):
|
||||||
- Firebase/Messaging (= 12.2.0)
|
- Firebase/Messaging (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseAnalytics (12.2.0):
|
- FirebaseAnalytics (12.4.0):
|
||||||
- FirebaseAnalytics/Default (= 12.2.0)
|
- FirebaseAnalytics/Default (= 12.4.0)
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseAnalytics/Default (12.2.0):
|
- FirebaseAnalytics/Default (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- GoogleAppMeasurement/Default (= 12.2.0)
|
- GoogleAppMeasurement/Default (= 12.4.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseCore (12.2.0):
|
- FirebaseCore (12.4.0):
|
||||||
- FirebaseCoreInternal (~> 12.2.0)
|
- FirebaseCoreInternal (~> 12.4.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- FirebaseCoreExtension (12.2.0):
|
- FirebaseCoreExtension (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseCoreInternal (12.2.0):
|
- FirebaseCoreInternal (12.4.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- FirebaseCrashlytics (12.2.0):
|
- FirebaseCrashlytics (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- FirebaseRemoteConfigInterop (~> 12.2.0)
|
- FirebaseRemoteConfigInterop (~> 12.4.0)
|
||||||
- FirebaseSessions (~> 12.2.0)
|
- FirebaseSessions (~> 12.4.0)
|
||||||
- GoogleDataTransport (~> 10.1)
|
- GoogleDataTransport (~> 10.1)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseInstallations (12.2.0):
|
- FirebaseInstallations (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseMessaging (12.2.0):
|
- FirebaseMessaging (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- GoogleDataTransport (~> 10.1)
|
- GoogleDataTransport (~> 10.1)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Reachability (~> 8.1)
|
- GoogleUtilities/Reachability (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseRemoteConfigInterop (12.2.0)
|
- FirebaseRemoteConfigInterop (12.4.0)
|
||||||
- FirebaseSessions (12.2.0):
|
- FirebaseSessions (12.4.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.4.0)
|
||||||
- FirebaseCoreExtension (~> 12.2.0)
|
- FirebaseCoreExtension (~> 12.4.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.4.0)
|
||||||
- GoogleDataTransport (~> 10.1)
|
- GoogleDataTransport (~> 10.1)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
@@ -155,27 +157,28 @@ PODS:
|
|||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAdsOnDeviceConversion (2.3.0):
|
- GoogleAdsOnDeviceConversion (3.1.0):
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/Core (12.2.0):
|
- GoogleAppMeasurement/Core (12.4.0):
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/Default (12.2.0):
|
- GoogleAppMeasurement/Default (12.4.0):
|
||||||
- GoogleAdsOnDeviceConversion (= 2.3.0)
|
- GoogleAdsOnDeviceConversion (~> 3.1.0)
|
||||||
- GoogleAppMeasurement/Core (= 12.2.0)
|
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||||
- GoogleAppMeasurement/IdentitySupport (= 12.2.0)
|
- GoogleAppMeasurement/IdentitySupport (= 12.4.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/IdentitySupport (12.2.0):
|
- GoogleAppMeasurement/IdentitySupport (12.4.0):
|
||||||
- GoogleAppMeasurement/Core (= 12.2.0)
|
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
@@ -215,8 +218,23 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.5.0)
|
- Kingfisher (8.6.1)
|
||||||
- livekit_client (2.5.0):
|
- KingfisherWebP (1.7.2):
|
||||||
|
- Kingfisher (~> 8.0)
|
||||||
|
- libwebp (>= 1.1.0)
|
||||||
|
- libwebp (1.5.0):
|
||||||
|
- libwebp/demux (= 1.5.0)
|
||||||
|
- libwebp/mux (= 1.5.0)
|
||||||
|
- libwebp/sharpyuv (= 1.5.0)
|
||||||
|
- libwebp/webp (= 1.5.0)
|
||||||
|
- libwebp/demux (1.5.0):
|
||||||
|
- libwebp/webp
|
||||||
|
- libwebp/mux (1.5.0):
|
||||||
|
- libwebp/demux
|
||||||
|
- libwebp/sharpyuv (1.5.0)
|
||||||
|
- libwebp/webp (1.5.0):
|
||||||
|
- libwebp/sharpyuv
|
||||||
|
- livekit_client (2.5.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- WebRTC-SDK (= 137.7151.04)
|
- WebRTC-SDK (= 137.7151.04)
|
||||||
@@ -252,9 +270,9 @@ PODS:
|
|||||||
- record_ios (1.1.0):
|
- record_ios (1.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- SDWebImage (5.21.2):
|
- SDWebImage (5.21.3):
|
||||||
- SDWebImage/Core (= 5.21.2)
|
- SDWebImage/Core (= 5.21.3)
|
||||||
- SDWebImage/Core (5.21.2)
|
- SDWebImage/Core (5.21.3)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@@ -293,6 +311,8 @@ PODS:
|
|||||||
- super_native_extensions (0.0.1):
|
- super_native_extensions (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SwiftyGif (5.4.5)
|
- SwiftyGif (5.4.5)
|
||||||
|
- syncfusion_flutter_pdfviewer (0.0.1):
|
||||||
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- volume_controller (0.0.1):
|
- volume_controller (0.0.1):
|
||||||
@@ -303,6 +323,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
|
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/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`)
|
||||||
@@ -327,6 +348,7 @@ DEPENDENCIES:
|
|||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
|
- KingfisherWebP
|
||||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
@@ -344,6 +366,7 @@ DEPENDENCIES:
|
|||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||||
|
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
@@ -368,6 +391,8 @@ SPEC REPOS:
|
|||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
|
- KingfisherWebP
|
||||||
|
- libwebp
|
||||||
- nanopb
|
- nanopb
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
@@ -379,6 +404,8 @@ SPEC REPOS:
|
|||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
app_links:
|
||||||
|
:path: ".symlinks/plugins/app_links/ios"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
croppy:
|
croppy:
|
||||||
@@ -459,6 +486,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||||
super_native_extensions:
|
super_native_extensions:
|
||||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||||
|
syncfusion_flutter_pdfviewer:
|
||||||
|
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
volume_controller:
|
volume_controller:
|
||||||
@@ -468,6 +497,7 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||||
|
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
@@ -475,20 +505,20 @@ SPEC CHECKSUMS:
|
|||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||||
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
|
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||||
firebase_analytics: 8c78ce6224e0623152379d6cc7ef3d9098477b7e
|
firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835
|
||||||
firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d
|
firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464
|
||||||
firebase_crashlytics: e55dcf895eed0dd87c447dd5aff8db7f1bb8bbdb
|
firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed
|
||||||
firebase_messaging: 38c66c1184695b0c87abe51d40fc590718abed1a
|
firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4
|
||||||
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
|
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||||
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
|
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||||
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
|
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
||||||
FirebaseCoreInternal: 56ea29f3dad2894f81b060f706f9d53509b6ed3b
|
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6
|
||||||
FirebaseCrashlytics: f83cbf176d5c637ade108c0aacf1ccbd5ec499bf
|
FirebaseCrashlytics: a6ece278a837c7e88de2d9b5da0a3542f2342395
|
||||||
FirebaseInstallations: 3e884b01feabdf67582a80f3250425a00979b4ed
|
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2
|
||||||
FirebaseMessaging: 43ec73bbfedd0c385a849bb91593ab4ad4b9e48e
|
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5
|
||||||
FirebaseRemoteConfigInterop: 0896fd52ab72586a355c8f389ff85aaa9e5375e1
|
FirebaseRemoteConfigInterop: 1e31ec72b89c9924367c59bfb5ec9ab60d1d6766
|
||||||
FirebaseSessions: f4692789e770bec66ce17d772c0e9561c4f11737
|
FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
||||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
@@ -501,14 +531,16 @@ SPEC CHECKSUMS:
|
|||||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe
|
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||||
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
|
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
|
Kingfisher: 7ac7a7288653787a54206b11a3c74f49ab650f1f
|
||||||
livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4
|
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
||||||
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
|
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
||||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
@@ -517,27 +549,28 @@ SPEC CHECKSUMS:
|
|||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed
|
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
|
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
|
||||||
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
||||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||||
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||||
|
|
||||||
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f
|
PODFILE CHECKSUM: 9924dcd1590471adb798f3a0876bedd6a65ea145
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
7310A7DF2EB10963002C0FD3 /* WatchRunner Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
|
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
|
||||||
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
A1D34487886D362AC8B99B2E /* Pods_WatchRunner_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 802C1CFCA7F1E069AAEFB454 /* Pods_WatchRunner_Watch_App.framework */; };
|
||||||
B87C0E607033790E71B54D73 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6D834CA86410B09796B312B /* Pods_Runner.framework */; };
|
B87C0E607033790E71B54D73 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6D834CA86410B09796B312B /* Pods_Runner.framework */; };
|
||||||
D174D53FF3E8EA943491A5CC /* Pods_SolianShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */; };
|
D174D53FF3E8EA943491A5CC /* Pods_SolianShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */; };
|
||||||
D1772CE196985AE8E8C9F2E5 /* Pods_SolianNotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */; };
|
D1772CE196985AE8E8C9F2E5 /* Pods_SolianNotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */; };
|
||||||
@@ -58,6 +60,17 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
7310A7DE2EB10963002C0FD3 /* Embed Watch Content */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
|
||||||
|
dstSubfolderSpec = 16;
|
||||||
|
files = (
|
||||||
|
7310A7DF2EB10963002C0FD3 /* WatchRunner Watch App.app in Embed Watch Content */,
|
||||||
|
);
|
||||||
|
name = "Embed Watch Content";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = {
|
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -84,6 +97,7 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.profile.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
@@ -100,6 +114,7 @@
|
|||||||
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WatchRunner Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
|
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
|
||||||
@@ -111,6 +126,8 @@
|
|||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
802C1CFCA7F1E069AAEFB454 /* Pods_WatchRunner_Watch_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WatchRunner_Watch_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.debug.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -120,6 +137,7 @@
|
|||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||||
|
A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.release.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -162,6 +180,13 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
|
path = "WatchRunner Watch App";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
73268D272DEB012A0076E970 /* Services */ = {
|
73268D272DEB012A0076E970 /* Services */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
@@ -205,6 +230,14 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7310A7D12EB10962002C0FD3 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A1D34487886D362AC8B99B2E /* Pods_WatchRunner_Watch_App.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
73ACDFA82E3D0E6100B63535 /* Frameworks */ = {
|
73ACDFA82E3D0E6100B63535 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -258,6 +291,7 @@
|
|||||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
|
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
|
||||||
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
|
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
|
||||||
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
|
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
|
||||||
|
802C1CFCA7F1E069AAEFB454 /* Pods_WatchRunner_Watch_App.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -280,6 +314,9 @@
|
|||||||
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */,
|
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */,
|
||||||
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */,
|
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */,
|
||||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */,
|
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */,
|
||||||
|
86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */,
|
||||||
|
A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */,
|
||||||
|
103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -303,6 +340,7 @@
|
|||||||
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
||||||
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
||||||
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||||
|
7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
91E124CE95BCB4DCD890160D /* Pods */,
|
91E124CE95BCB4DCD890160D /* Pods */,
|
||||||
@@ -319,6 +357,7 @@
|
|||||||
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
|
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
|
||||||
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
||||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
|
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
|
||||||
|
7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -363,6 +402,28 @@
|
|||||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
|
7310A7D32EB10962002C0FD3 /* WatchRunner Watch App */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "WatchRunner Watch App" */;
|
||||||
|
buildPhases = (
|
||||||
|
DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
7310A7D02EB10962002C0FD3 /* Sources */,
|
||||||
|
7310A7D12EB10962002C0FD3 /* Frameworks */,
|
||||||
|
7310A7D22EB10962002C0FD3 /* Resources */,
|
||||||
|
C74B07D6587D29C67A198025 /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */,
|
||||||
|
);
|
||||||
|
name = "WatchRunner Watch App";
|
||||||
|
productName = "WatchRunner Watch App";
|
||||||
|
productReference = 7310A7D42EB10962002C0FD3 /* WatchRunner Watch App.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = {
|
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */;
|
buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */;
|
||||||
@@ -434,6 +495,7 @@
|
|||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
|
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
|
||||||
|
7310A7DE2EB10963002C0FD3 /* Embed Watch Content */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
@@ -463,7 +525,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 1640;
|
LastSwiftUpdateCheck = 2600;
|
||||||
LastUpgradeCheck = 1510;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
@@ -471,6 +533,9 @@
|
|||||||
CreatedOnToolsVersion = 14.0;
|
CreatedOnToolsVersion = 14.0;
|
||||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
};
|
};
|
||||||
|
7310A7D32EB10962002C0FD3 = {
|
||||||
|
CreatedOnToolsVersion = 26.0.1;
|
||||||
|
};
|
||||||
73ACDFAA2E3D0E6100B63535 = {
|
73ACDFAA2E3D0E6100B63535 = {
|
||||||
CreatedOnToolsVersion = 16.4;
|
CreatedOnToolsVersion = 16.4;
|
||||||
};
|
};
|
||||||
@@ -504,6 +569,7 @@
|
|||||||
73CDD6792DEC00480059D95D /* SolianNotificationService */,
|
73CDD6792DEC00480059D95D /* SolianNotificationService */,
|
||||||
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
||||||
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||||
|
7310A7D32EB10962002C0FD3 /* WatchRunner Watch App */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -516,6 +582,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7310A7D22EB10962002C0FD3 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
73ACDFA92E3D0E6100B63535 /* Resources */ = {
|
73ACDFA92E3D0E6100B63535 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -683,6 +756,45 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
};
|
};
|
||||||
|
C74B07D6587D29C67A198025 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-WatchRunner Watch App-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
|
E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -734,6 +846,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7310A7D02EB10962002C0FD3 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
73ACDFA72E3D0E6100B63535 /* Sources */ = {
|
73ACDFA72E3D0E6100B63535 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -943,6 +1062,147 @@
|
|||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
};
|
};
|
||||||
|
7310A7E02EB10963002C0FD3 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = WatchRunner;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
7310A7E12EB10963002C0FD3 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = WatchRunner;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
7310A7E22EB10963002C0FD3 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = WatchRunner;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
73ACDFC42E3D0E6100B63535 /* Debug */ = {
|
73ACDFC42E3D0E6100B63535 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -1487,6 +1747,16 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "WatchRunner Watch App" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
7310A7E02EB10963002C0FD3 /* Debug */,
|
||||||
|
7310A7E12EB10963002C0FD3 /* Release */,
|
||||||
|
7310A7E22EB10963002C0FD3 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = {
|
73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import WatchConnectivity
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
let notifyDelegate = NotifyDelegate()
|
let notifyDelegate = NotifyDelegate()
|
||||||
|
private var watchConnectivityService: WatchConnectivityService?
|
||||||
|
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
@@ -12,7 +14,7 @@ import UIKit
|
|||||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||||
|
|
||||||
let replyableMessageCategory = UNNotificationCategory(
|
let replyableMessageCategory = UNNotificationCategory(
|
||||||
identifier: "REPLYABLE_MESSAGE",
|
identifier: "CHAT_MESSAGE",
|
||||||
actions: [
|
actions: [
|
||||||
UNTextInputNotificationAction(
|
UNTextInputNotificationAction(
|
||||||
identifier: "reply_action",
|
identifier: "reply_action",
|
||||||
@@ -28,6 +30,55 @@ import UIKit
|
|||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
|
||||||
|
if WCSession.isSupported() {
|
||||||
|
watchConnectivityService = WatchConnectivityService()
|
||||||
|
}
|
||||||
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WatchConnectivityService: NSObject, WCSessionDelegate {
|
||||||
|
private let session: WCSession
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.session = .default
|
||||||
|
super.init()
|
||||||
|
print("[iOS] Activating WCSession")
|
||||||
|
self.session.delegate = self
|
||||||
|
self.session.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("[iOS] WCSession activation failed with error: \(error.localizedDescription)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("[iOS] WCSession activated with state: \(activationState.rawValue)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionDidBecomeInactive(_ session: WCSession) {}
|
||||||
|
|
||||||
|
func sessionDidDeactivate(_ session: WCSession) {
|
||||||
|
session.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
|
||||||
|
print("[iOS] Received message: \(message)")
|
||||||
|
if let request = message["request"] as? String, request == "data" {
|
||||||
|
let token = UserDefaults.standard.getFlutterToken()
|
||||||
|
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||||
|
|
||||||
|
print("[iOS] Retrieved token: \(token ?? "nil")")
|
||||||
|
print("[iOS] Retrieved serverUrl: \(serverUrl)")
|
||||||
|
|
||||||
|
var data: [String: Any] = ["serverUrl": serverUrl]
|
||||||
|
if let token = token {
|
||||||
|
data["token"] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[iOS] Replying with data: \(data)")
|
||||||
|
replyHandler(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,334 @@
|
|||||||
{"images":[{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@2x.png","scale":"2x","platform":"ios"},{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@3x.png","scale":"3x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@2x.png","scale":"2x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@3x.png","scale":"3x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@2x.png","scale":"2x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@3x.png","scale":"3x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@2x.png","scale":"2x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@3x.png","scale":"3x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@2x.png","scale":"2x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@3x.png","scale":"3x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@2x.png","scale":"2x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@3x.png","scale":"3x","platform":"ios"},{"size":"68x68","idiom":"universal","filename":"Icon-App-68x68@2x.png","scale":"2x","platform":"ios"},{"size":"76x76","idiom":"universal","filename":"Icon-App-76x76@2x.png","scale":"2x","platform":"ios"},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x","platform":"ios"},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-1024x1024@1x.png","scale":"1x","platform":"ios"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"68x68","idiom":"universal","filename":"Icon-App-Dark-68x68@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"76x76","idiom":"universal","filename":"Icon-App-Dark-76x76@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-Dark-83.5x83.5@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-Dark-1024x1024@1x.png","scale":"1x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]}],"info":{"version":1,"author":"xcode"}}
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-38x38@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-38x38@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-64x64@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-64x64@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-68x68@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "68x68"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-20x20@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-20x20@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-29x29@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-29x29@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-38x38@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-38x38@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-40x40@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-40x40@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-60x60@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-60x60@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-64x64@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-64x64@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-68x68@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "68x68"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-76x76@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-83.5x83.5@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "Icon-App-Dark-1024x1024@1x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 762 B |
@@ -36,6 +36,14 @@
|
|||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>solian</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
func getAttachmentUrl(for identifier: String) -> String {
|
func getAttachmentUrl(for identifier: String) -> String {
|
||||||
let serverBaseUrl = "https://api.solian.app"
|
let serverBaseUrl = UserDefaults.standard.getServerUrl()
|
||||||
|
|
||||||
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
|
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ extension UserDefaults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getServerUrl(forKey key: String = "app_server_url") -> String {
|
func getServerUrl(forKey key: String = "app_server_url") -> String {
|
||||||
return self.getFlutterValue(forKey: key) ?? "https://nt.solian.app"
|
return self.getFlutterValue(forKey: key) ?? "https://api.solian.app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
|
private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
|
||||||
switch content.userInfo["type"] as? String {
|
switch content.userInfo["type"] as? String {
|
||||||
case "messages.new":
|
case "messages.new":
|
||||||
content.categoryIdentifier = "REPLYABLE_MESSAGE"
|
|
||||||
try handleMessagingNotification(request: request, content: content)
|
try handleMessagingNotification(request: request, content: content)
|
||||||
default:
|
default:
|
||||||
try handleDefaultNotification(content: content)
|
try handleDefaultNotification(content: content)
|
||||||
@@ -86,10 +85,8 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
customIdentifier: nil
|
customIdentifier: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
content.categoryIdentifier = "CHAT_MESSAGE"
|
||||||
self.donateInteraction(for: intent)
|
self.contentHandler?(content)
|
||||||
let updatedContent = try? request.content.updating(from: intent)
|
|
||||||
self.contentHandler?(updatedContent ?? content)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"platform" : "universal",
|
||||||
|
"reference" : "systemIndigoColor"
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-20x20@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-20x20@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-29x29@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-29x29@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-38x38@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-38x38@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-40x40@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-40x40@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-60x60@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-60x60@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-64x64@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-64x64@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-68x68@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "68x68"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-76x76@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-83.5x83.5@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-1024x1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-16x16.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-16x16@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-32x32.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-32x32@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-128x128.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-128x128@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-256x256.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-256x256@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-512x512.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-512x512@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-22x22@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "22x22"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-24x24@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "24x24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-27.5x27.5@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "27.5x27.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-29x29@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-30x30@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "30x30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-32x32@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-33x33@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "33x33"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-40x40@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-43.5x43.5@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "43.5x43.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-44x44@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "44x44"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-46x46@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "46x46"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-50x50@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "50x50"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-51x51@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "51x51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-54x54@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "54x54"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-86x86@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "86x86"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-98x98@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "98x98"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-108x108@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "108x108"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-117x117@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "117x117"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-129x129@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "129x129"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-1024x1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 473 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
6
ios/WatchRunner Watch App/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
21
ios/WatchRunner Watch App/Assets.xcassets/Logo.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/WatchRunner Watch App/Assets.xcassets/Logo.imageset/icon.png
vendored
Normal file
|
After Width: | Height: | Size: 70 KiB |
50
ios/WatchRunner Watch App/ContentView.swift
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// ContentView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// The root view of the app.
|
||||||
|
struct ContentView: View {
|
||||||
|
@StateObject private var appState = AppState()
|
||||||
|
@State private var selection: Panel? = .explore
|
||||||
|
|
||||||
|
enum Panel: Hashable {
|
||||||
|
case explore
|
||||||
|
case chat
|
||||||
|
case notifications
|
||||||
|
case account
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationSplitView {
|
||||||
|
List(selection: $selection) {
|
||||||
|
AppInfoHeaderView()
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.environmentObject(appState)
|
||||||
|
|
||||||
|
Label("Explore", systemImage: "globe.fill").tag(Panel.explore)
|
||||||
|
Label("Chat", systemImage: "message.fill").tag(Panel.chat)
|
||||||
|
Label("Notifications", systemImage: "bell.fill").tag(Panel.notifications)
|
||||||
|
Label("Account", systemImage: "person.circle.fill").tag(Panel.account)
|
||||||
|
}
|
||||||
|
.listStyle(.automatic)
|
||||||
|
} detail: {
|
||||||
|
switch selection {
|
||||||
|
case .explore:
|
||||||
|
ExploreView().environmentObject(appState)
|
||||||
|
case .chat:
|
||||||
|
ChatView().environmentObject(appState)
|
||||||
|
case .notifications:
|
||||||
|
NotificationView().environmentObject(appState)
|
||||||
|
case .account:
|
||||||
|
AccountView().environmentObject(appState)
|
||||||
|
case .none:
|
||||||
|
Text("Select a panel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
ios/WatchRunner Watch App/Layouts/FlowLayout.swift
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// FlowLayout.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Custom Layouts
|
||||||
|
|
||||||
|
struct FlowLayout: Layout {
|
||||||
|
var alignment: HorizontalAlignment = .leading
|
||||||
|
var spacing: CGFloat = 10
|
||||||
|
|
||||||
|
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||||
|
let containerWidth = proposal.width ?? 0
|
||||||
|
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
|
||||||
|
|
||||||
|
var currentX: CGFloat = 0
|
||||||
|
var currentY: CGFloat = 0
|
||||||
|
var lineHeight: CGFloat = 0
|
||||||
|
var totalHeight: CGFloat = 0
|
||||||
|
|
||||||
|
for size in sizes {
|
||||||
|
if currentX + size.width > containerWidth {
|
||||||
|
// New line
|
||||||
|
currentX = 0
|
||||||
|
currentY += lineHeight + spacing
|
||||||
|
totalHeight = currentY + size.height
|
||||||
|
lineHeight = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
currentX += size.width + spacing
|
||||||
|
lineHeight = max(lineHeight, size.height)
|
||||||
|
}
|
||||||
|
totalHeight = currentY + lineHeight
|
||||||
|
|
||||||
|
return CGSize(width: containerWidth, height: totalHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
||||||
|
let containerWidth = bounds.width
|
||||||
|
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
|
||||||
|
|
||||||
|
var currentX: CGFloat = 0
|
||||||
|
var currentY: CGFloat = 0
|
||||||
|
var lineHeight: CGFloat = 0
|
||||||
|
var lineElements: [(offset: Int, size: CGSize)] = []
|
||||||
|
|
||||||
|
func placeLine() {
|
||||||
|
let lineWidth = lineElements.map { $0.size.width }.reduce(0, +) + CGFloat(lineElements.count - 1) * spacing
|
||||||
|
var startX: CGFloat = 0
|
||||||
|
switch alignment {
|
||||||
|
case .leading:
|
||||||
|
startX = bounds.minX
|
||||||
|
case .center:
|
||||||
|
startX = bounds.minX + (containerWidth - lineWidth) / 2
|
||||||
|
case .trailing:
|
||||||
|
startX = bounds.maxX - lineWidth
|
||||||
|
default:
|
||||||
|
startX = bounds.minX
|
||||||
|
}
|
||||||
|
|
||||||
|
var xOffset = startX
|
||||||
|
for (offset, size) in lineElements {
|
||||||
|
subviews[offset].place(at: CGPoint(x: xOffset, y: bounds.minY + currentY), proposal: ProposedViewSize(size)) // Use bounds.minY + currentY
|
||||||
|
xOffset += size.width + spacing
|
||||||
|
}
|
||||||
|
lineElements.removeAll() // Clear elements for the next line
|
||||||
|
}
|
||||||
|
|
||||||
|
for (offset, size) in sizes.enumerated() {
|
||||||
|
if currentX + size.width > containerWidth && !lineElements.isEmpty {
|
||||||
|
// New line
|
||||||
|
placeLine()
|
||||||
|
currentX = 0
|
||||||
|
currentY += lineHeight + spacing
|
||||||
|
lineHeight = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
lineElements.append((offset, size))
|
||||||
|
currentX += size.width + spacing
|
||||||
|
lineHeight = max(lineHeight, size.height)
|
||||||
|
}
|
||||||
|
placeLine() // Place the last line
|
||||||
|
}
|
||||||
|
}
|
||||||
365
ios/WatchRunner Watch App/Models/Models.swift
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
// Models.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Models
|
||||||
|
|
||||||
|
struct AppToken: Codable {
|
||||||
|
let token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnActivity: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let type: String
|
||||||
|
let data: ActivityData?
|
||||||
|
let createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityData: Codable {
|
||||||
|
case post(SnPost)
|
||||||
|
case discovery(DiscoveryData)
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
if let post = try? container.decode(SnPost.self) {
|
||||||
|
self = .post(post)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let discoveryData = try? container.decode(DiscoveryData.self) {
|
||||||
|
self = .discovery(discoveryData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self = .unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
// Not needed for decoding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnPost: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let title: String?
|
||||||
|
let content: String?
|
||||||
|
let publisher: SnPublisher
|
||||||
|
let attachments: [SnCloudFile]
|
||||||
|
let tags: [SnPostTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiscoveryData: Codable {
|
||||||
|
let items: [DiscoveryItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiscoveryItem: Codable, Identifiable {
|
||||||
|
var id = UUID()
|
||||||
|
let type: String
|
||||||
|
let data: DiscoveryItemData
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case type, data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DiscoveryItemData: Codable {
|
||||||
|
case realm(SnRealm)
|
||||||
|
case publisher(SnPublisher)
|
||||||
|
case article(SnWebArticle)
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
if let realm = try? container.decode(SnRealm.self) {
|
||||||
|
self = .realm(realm)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let publisher = try? container.decode(SnPublisher.self) {
|
||||||
|
self = .publisher(publisher)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let article = try? container.decode(SnWebArticle.self) {
|
||||||
|
self = .article(article)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self = .unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
// Not needed for decoding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnRealm: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let description: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnPublisher: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let nick: String?
|
||||||
|
let description: String?
|
||||||
|
let picture: SnCloudFile?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnCloudFile: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let mimeType: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnPostTag: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let slug: String
|
||||||
|
let name: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnWebArticle: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let title: String
|
||||||
|
let url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnNotification: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let topic: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
let content: String
|
||||||
|
let meta: [String: AnyCodable]?
|
||||||
|
let priority: Int
|
||||||
|
let viewedAt: Date?
|
||||||
|
let accountId: String
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case topic
|
||||||
|
case title
|
||||||
|
case subtitle
|
||||||
|
case content
|
||||||
|
case meta
|
||||||
|
case priority
|
||||||
|
case viewedAt = "viewedAt"
|
||||||
|
case accountId = "accountId"
|
||||||
|
case createdAt = "createdAt"
|
||||||
|
case updatedAt = "updatedAt"
|
||||||
|
case deletedAt = "deletedAt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AnyCodable: Codable {
|
||||||
|
let value: Any
|
||||||
|
|
||||||
|
init(_ value: Any) {
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
if let intValue = try? container.decode(Int.self) {
|
||||||
|
value = intValue
|
||||||
|
} else if let doubleValue = try? container.decode(Double.self) {
|
||||||
|
value = doubleValue
|
||||||
|
} else if let boolValue = try? container.decode(Bool.self) {
|
||||||
|
value = boolValue
|
||||||
|
} else if let stringValue = try? container.decode(String.self) {
|
||||||
|
value = stringValue
|
||||||
|
} else if let arrayValue = try? container.decode([AnyCodable].self) {
|
||||||
|
value = arrayValue
|
||||||
|
} else if let dictValue = try? container.decode([String: AnyCodable].self) {
|
||||||
|
value = dictValue
|
||||||
|
} else {
|
||||||
|
value = NSNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
switch value {
|
||||||
|
case let intValue as Int:
|
||||||
|
try container.encode(intValue)
|
||||||
|
case let doubleValue as Double:
|
||||||
|
try container.encode(doubleValue)
|
||||||
|
case let boolValue as Bool:
|
||||||
|
try container.encode(boolValue)
|
||||||
|
case let stringValue as String:
|
||||||
|
try container.encode(stringValue)
|
||||||
|
case let arrayValue as [AnyCodable]:
|
||||||
|
try container.encode(arrayValue)
|
||||||
|
case let dictValue as [String: AnyCodable]:
|
||||||
|
try container.encode(dictValue)
|
||||||
|
default:
|
||||||
|
try container.encodeNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationResponse {
|
||||||
|
let notifications: [SnNotification]
|
||||||
|
let total: Int
|
||||||
|
let hasMore: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActivityResponse {
|
||||||
|
let activities: [SnActivity]
|
||||||
|
let hasMore: Bool
|
||||||
|
let nextCursor: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnAccount: Codable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let nick: String
|
||||||
|
let profile: SnUserProfile
|
||||||
|
let createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnUserProfile: Codable {
|
||||||
|
let bio: String?
|
||||||
|
let picture: SnCloudFile?
|
||||||
|
let background: SnCloudFile?
|
||||||
|
let level: Int
|
||||||
|
let experience: Int
|
||||||
|
let levelingProgress: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnAccountStatus: Codable {
|
||||||
|
let id: String
|
||||||
|
let attitude: Int
|
||||||
|
let isOnline: Bool
|
||||||
|
let isInvisible: Bool
|
||||||
|
let isNotDisturb: Bool
|
||||||
|
let isCustomized: Bool
|
||||||
|
let label: String
|
||||||
|
let meta: [String: AnyCodable]?
|
||||||
|
let clearedAt: Date?
|
||||||
|
let accountId: String
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Chat Models
|
||||||
|
|
||||||
|
struct SnChatRoom: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String?
|
||||||
|
let description: String?
|
||||||
|
let type: Int
|
||||||
|
let isPublic: Bool
|
||||||
|
let isCommunity: Bool
|
||||||
|
let picture: SnCloudFile?
|
||||||
|
let background: SnCloudFile?
|
||||||
|
let realmId: String?
|
||||||
|
let realm: SnRealm?
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
let members: [SnChatMember]?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnChatMessage: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let type: String
|
||||||
|
let content: String?
|
||||||
|
let nonce: String?
|
||||||
|
let meta: [String: AnyCodable]
|
||||||
|
let membersMentioned: [String]?
|
||||||
|
let editedAt: Date?
|
||||||
|
let attachments: [SnCloudFile]
|
||||||
|
let reactions: [SnChatReaction]
|
||||||
|
let repliedMessageId: String?
|
||||||
|
let forwardedMessageId: String?
|
||||||
|
let senderId: String
|
||||||
|
let sender: SnChatMember
|
||||||
|
let chatRoomId: String
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id, type, content, nonce, meta, membersMentioned, editedAt, attachments, reactions, repliedMessageId, forwardedMessageId, senderId, sender, chatRoomId, createdAt, updatedAt, deletedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try container.decode(String.self, forKey: .id)
|
||||||
|
type = try container.decode(String.self, forKey: .type)
|
||||||
|
content = try container.decodeIfPresent(String.self, forKey: .content)
|
||||||
|
nonce = try container.decodeIfPresent(String.self, forKey: .nonce)
|
||||||
|
meta = try container.decode([String: AnyCodable].self, forKey: .meta)
|
||||||
|
membersMentioned = try container.decodeIfPresent([String].self, forKey: .membersMentioned) ?? []
|
||||||
|
editedAt = try container.decodeIfPresent(Date.self, forKey: .editedAt)
|
||||||
|
attachments = try container.decode([SnCloudFile].self, forKey: .attachments)
|
||||||
|
reactions = try container.decode([SnChatReaction].self, forKey: .reactions)
|
||||||
|
repliedMessageId = try container.decodeIfPresent(String.self, forKey: .repliedMessageId)
|
||||||
|
forwardedMessageId = try container.decodeIfPresent(String.self, forKey: .forwardedMessageId)
|
||||||
|
senderId = try container.decode(String.self, forKey: .senderId)
|
||||||
|
sender = try container.decode(SnChatMember.self, forKey: .sender)
|
||||||
|
chatRoomId = try container.decode(String.self, forKey: .chatRoomId)
|
||||||
|
createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||||
|
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
|
||||||
|
deletedAt = try container.decodeIfPresent(Date.self, forKey: .deletedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnChatReaction: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let messageId: String
|
||||||
|
let senderId: String
|
||||||
|
let sender: SnChatMember
|
||||||
|
let symbol: String
|
||||||
|
let attitude: Int
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnChatMember: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let chatRoomId: String
|
||||||
|
let chatRoom: SnChatRoom?
|
||||||
|
let accountId: String
|
||||||
|
let account: SnAccount
|
||||||
|
let nick: String?
|
||||||
|
let role: Int
|
||||||
|
let notify: Int
|
||||||
|
let joinedAt: Date?
|
||||||
|
let breakUntil: Date?
|
||||||
|
let timeoutUntil: Date?
|
||||||
|
let isBot: Bool
|
||||||
|
let status: SnAccountStatus?
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnChatSummary: Codable {
|
||||||
|
let unreadCount: Int
|
||||||
|
let lastMessage: SnChatMessage?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatRoomsResponse {
|
||||||
|
let rooms: [SnChatRoom]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatInvitesResponse {
|
||||||
|
let invites: [SnChatMember]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MessageSyncResponse: Codable {
|
||||||
|
let messages: [SnChatMessage]
|
||||||
|
let currentTimestamp: Date
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case messages
|
||||||
|
case currentTimestamp = "current_timestamp"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
ios/WatchRunner Watch App/Previews/CustomPreviews.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// CustomPreviews.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationStack {
|
||||||
|
ActivityListView(filter: "Preview", mockActivities: SnActivity.mock)
|
||||||
|
.environmentObject(AppState())
|
||||||
|
}
|
||||||
|
}
|
||||||
35
ios/WatchRunner Watch App/Previews/MockData.swift
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// MockData.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
extension SnActivity {
|
||||||
|
static var mock: [SnActivity] {
|
||||||
|
let mockPublisher = SnPublisher(id: "pub1", name: "Mock Publisher", nick: "mock_nick", description: "A publisher for testing", picture: SnCloudFile(id: "mock_avatar_id", mimeType: "image/png"))
|
||||||
|
let mockTag1 = SnPostTag(id: "tag1", slug: "swiftui", name: "SwiftUI")
|
||||||
|
let mockTag2 = SnPostTag(id: "tag2", slug: "watchos", name: "watchOS")
|
||||||
|
let mockAttachment1 = SnCloudFile(id: "mock_image_id_1", mimeType: "image/jpeg")
|
||||||
|
let mockAttachment2 = SnCloudFile(id: "mock_image_id_2", mimeType: "image/png")
|
||||||
|
|
||||||
|
let post1 = SnPost(id: "1", title: "Hello from a Mock Post!", content: "This is a mock post content. It can be a bit longer to see how it wraps.", publisher: mockPublisher, attachments: [mockAttachment1, mockAttachment2], tags: [mockTag1, mockTag2])
|
||||||
|
let activity1 = SnActivity(id: "1", type: "posts.new", data: .post(post1), createdAt: Date())
|
||||||
|
|
||||||
|
let realm1 = SnRealm(id: "r1", name: "SwiftUI Previews", description: "A place for designing in previews.")
|
||||||
|
let publisher1 = SnPublisher(id: "p1", name: "The Mock Times", nick: "mock_times", description: "All the news that's fit to mock.", picture: nil)
|
||||||
|
let article1 = SnWebArticle(id: "a1", title: "The Art of Mocking Data", url: "https://example.com")
|
||||||
|
|
||||||
|
let discoveryItem1 = DiscoveryItem(type: "realm", data: .realm(realm1))
|
||||||
|
let discoveryItem2 = DiscoveryItem(type: "publisher", data: .publisher(publisher1))
|
||||||
|
let discoveryItem3 = DiscoveryItem(type: "article", data: .article(article1))
|
||||||
|
let discoveryData = DiscoveryData(items: [discoveryItem1, discoveryItem2, discoveryItem3])
|
||||||
|
let activity2 = SnActivity(id: "2", type: "discovery", data: .discovery(discoveryData), createdAt: Date())
|
||||||
|
|
||||||
|
return [activity1, activity2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
95
ios/WatchRunner Watch App/Services/ImageLoader.swift
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
//
|
||||||
|
// ImageLoader.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
import KingfisherWebP
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// MARK: - Image Loader
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class ImageLoader: ObservableObject {
|
||||||
|
@Published var image: Image?
|
||||||
|
@Published var errorMessage: String?
|
||||||
|
@Published var isLoading = false
|
||||||
|
|
||||||
|
private var currentTask: DownloadTask?
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
currentTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadImage(from initialUrl: URL, token: String) async {
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = nil
|
||||||
|
image = nil
|
||||||
|
|
||||||
|
// Create request modifier for authorization
|
||||||
|
let modifier = AnyModifier { request in
|
||||||
|
var r = request
|
||||||
|
r.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
r.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use WebP processor as default since the app seems to handle WebP images
|
||||||
|
let processor = WebPProcessor.default
|
||||||
|
|
||||||
|
// Use KingfisherManager to retrieve image with caching
|
||||||
|
currentTask = KingfisherManager.shared.retrieveImage(
|
||||||
|
with: initialUrl,
|
||||||
|
options: [
|
||||||
|
.requestModifier(modifier),
|
||||||
|
.processor(processor),
|
||||||
|
.cacheOriginalImage, // Cache the original image data
|
||||||
|
.loadDiskFileSynchronously // Load from disk cache synchronously if available
|
||||||
|
]
|
||||||
|
) { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
switch result {
|
||||||
|
case .success(let value):
|
||||||
|
self.image = Image(uiImage: value.image)
|
||||||
|
self.isLoading = false
|
||||||
|
case .failure(_):
|
||||||
|
// If WebP processor fails (likely due to format), try with default processor
|
||||||
|
let defaultProcessor = DefaultImageProcessor.default
|
||||||
|
self.currentTask = KingfisherManager.shared.retrieveImage(
|
||||||
|
with: initialUrl,
|
||||||
|
options: [
|
||||||
|
.requestModifier(modifier),
|
||||||
|
.processor(defaultProcessor),
|
||||||
|
.cacheOriginalImage,
|
||||||
|
.loadDiskFileSynchronously
|
||||||
|
]
|
||||||
|
) { [weak self] fallbackResult in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
switch fallbackResult {
|
||||||
|
case .success(let value):
|
||||||
|
self.image = Image(uiImage: value.image)
|
||||||
|
case .failure(let fallbackError):
|
||||||
|
self.errorMessage = fallbackError.localizedDescription
|
||||||
|
print("[watchOS] Image loading failed: \(fallbackError.localizedDescription)")
|
||||||
|
}
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
currentTask?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
637
ios/WatchRunner Watch App/Services/NetworkService.swift
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
//
|
||||||
|
// NetworkService.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29. //
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - WebSocket Data Structures
|
||||||
|
|
||||||
|
enum WebSocketState: Equatable {
|
||||||
|
case connected
|
||||||
|
case connecting
|
||||||
|
case disconnected
|
||||||
|
case serverDown
|
||||||
|
case duplicateDevice
|
||||||
|
case error(String)
|
||||||
|
|
||||||
|
// Equatable conformance
|
||||||
|
static func == (lhs: WebSocketState, rhs: WebSocketState) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.connected, .connected),
|
||||||
|
(.connecting, .connecting),
|
||||||
|
(.disconnected, .disconnected),
|
||||||
|
(.serverDown, .serverDown),
|
||||||
|
(.duplicateDevice, .duplicateDevice):
|
||||||
|
return true
|
||||||
|
case let (.error(a), .error(b)):
|
||||||
|
return a == b
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WebSocketPacket {
|
||||||
|
let type: String
|
||||||
|
let data: [String: Any]?
|
||||||
|
let endpoint: String?
|
||||||
|
let errorMessage: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Network Service
|
||||||
|
|
||||||
|
class NetworkService {
|
||||||
|
private let session = URLSession.shared
|
||||||
|
|
||||||
|
// Add a serial queue for WebSocket operations
|
||||||
|
private let webSocketQueue = DispatchQueue(label: "com.solian.websocketQueue")
|
||||||
|
|
||||||
|
func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> ActivityResponse {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
var components = URLComponents(url: baseURL.appendingPathComponent("/sphere/activities"), resolvingAgainstBaseURL: false)!
|
||||||
|
var queryItems = [URLQueryItem(name: "take", value: "20")]
|
||||||
|
if filter.lowercased() != "explore" {
|
||||||
|
queryItems.append(URLQueryItem(name: "filter", value: filter.lowercased()))
|
||||||
|
}
|
||||||
|
if let cursor = cursor {
|
||||||
|
queryItems.append(URLQueryItem(name: "cursor", value: cursor))
|
||||||
|
}
|
||||||
|
components.queryItems = queryItems
|
||||||
|
|
||||||
|
var request = URLRequest(url: components.url!)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
let activities = try decoder.decode([SnActivity].self, from: data)
|
||||||
|
|
||||||
|
let hasMore = (activities.first?.type ?? "empty") != "empty"
|
||||||
|
let nextCursor = activities.isEmpty ? nil : activities.map { $0.createdAt }.min()?.ISO8601Format()
|
||||||
|
|
||||||
|
return ActivityResponse(activities: activities, hasMore: hasMore, nextCursor: nextCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPost(title: String, content: String, token: String, serverUrl: String) async throws {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/sphere/posts")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let body: [String: Any] = ["title": title, "content": content]
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] createPost failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchNotifications(offset: Int = 0, take: Int = 20, token: String, serverUrl: String) async throws -> NotificationResponse {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
var components = URLComponents(url: baseURL.appendingPathComponent("/ring/notifications"), resolvingAgainstBaseURL: false)!
|
||||||
|
let queryItems = [URLQueryItem(name: "offset", value: String(offset)), URLQueryItem(name: "take", value: String(take))]
|
||||||
|
components.queryItems = queryItems
|
||||||
|
|
||||||
|
var request = URLRequest(url: components.url!)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
let notifications = try decoder.decode([SnNotification].self, from: data)
|
||||||
|
|
||||||
|
let httpResponse = response as? HTTPURLResponse
|
||||||
|
let total = Int(httpResponse?.value(forHTTPHeaderField: "X-Total") ?? "0") ?? 0
|
||||||
|
let hasMore = offset + notifications.count < total
|
||||||
|
|
||||||
|
return NotificationResponse(notifications: notifications, total: total, hasMore: hasMore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchUserProfile(token: String, serverUrl: String) async throws -> SnAccount {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/pass/accounts/me")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
return try decoder.decode(SnAccount.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAccountStatus(token: String, serverUrl: String) async throws -> SnAccountStatus? {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
return try decoder.decode(SnAccountStatus.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createOrUpdateStatus(attitude: Int, isInvisible: Bool, isNotDisturb: Bool, label: String?, token: String, serverUrl: String) async throws -> SnAccountStatus {
|
||||||
|
// Check if there\'s already a customized status
|
||||||
|
let existingStatus = try? await fetchAccountStatus(token: token, serverUrl: serverUrl)
|
||||||
|
let method = (existingStatus?.isCustomized == true) ? "PATCH" : "POST"
|
||||||
|
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = method
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
var body: [String: Any] = [
|
||||||
|
"attitude": attitude,
|
||||||
|
"is_invisible": isInvisible,
|
||||||
|
"is_not_disturb": isNotDisturb,
|
||||||
|
]
|
||||||
|
|
||||||
|
if let label = label, !label.isEmpty {
|
||||||
|
body["label"] = label
|
||||||
|
}
|
||||||
|
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 && httpResponse.statusCode != 200 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] createOrUpdateStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
return try decoder.decode(SnAccountStatus.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearStatus(token: String, serverUrl: String) async throws {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "DELETE"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] clearStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Chat API Methods
|
||||||
|
|
||||||
|
func fetchChatRooms(token: String, serverUrl: String) async throws -> ChatRoomsResponse {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/sphere/chat")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
let rooms = try decoder.decode([SnChatRoom].self, from: data)
|
||||||
|
return ChatRoomsResponse(rooms: rooms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchChatRoom(identifier: String, token: String, serverUrl: String) async throws -> SnChatRoom {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/sphere/chat/\(identifier)")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
||||||
|
throw URLError(.resourceUnavailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
return try decoder.decode(SnChatRoom.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchChatInvites(token: String, serverUrl: String) async throws -> ChatInvitesResponse {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/sphere/chat/invites")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
let invites = try decoder.decode([SnChatMember].self, from: data)
|
||||||
|
return ChatInvitesResponse(invites: invites)
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/accept")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] acceptChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func declineChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/decline")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] declineChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Message API Methods
|
||||||
|
|
||||||
|
func fetchChatMessages(chatRoomId: String, token: String, serverUrl: String, before: Date? = nil, take: Int = 50) async throws -> [SnChatMessage] {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try a different pattern: /sphere/chat/messages with roomId as query param
|
||||||
|
var components = URLComponents(
|
||||||
|
url: baseURL.appendingPathComponent("/sphere/chat/\(chatRoomId)/messages"),
|
||||||
|
resolvingAgainstBaseURL: false
|
||||||
|
)!
|
||||||
|
var queryItems = [
|
||||||
|
URLQueryItem(name: "take", value: String(take)),
|
||||||
|
]
|
||||||
|
if let before = before {
|
||||||
|
queryItems.append(URLQueryItem(name: "before", value: ISO8601DateFormatter().string(from: before)))
|
||||||
|
}
|
||||||
|
components.queryItems = queryItems
|
||||||
|
|
||||||
|
var request = URLRequest(url: components.url!)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
|
_ = String(data: data, encoding: .utf8) ?? "Unable to decode response body"
|
||||||
|
|
||||||
|
if httpResponse.statusCode != 200 {
|
||||||
|
print("[watchOS] fetchChatMessages failed with status \(httpResponse.statusCode)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if data is empty
|
||||||
|
if data.isEmpty {
|
||||||
|
print("[watchOS] fetchChatMessages received empty response data")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
do {
|
||||||
|
let messages = try decoder.decode([SnChatMessage].self, from: data)
|
||||||
|
print("[watchOS] fetchChatMessages successfully decoded \(messages.count) messages")
|
||||||
|
return messages
|
||||||
|
} catch {
|
||||||
|
print("error: ", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - WebSocket
|
||||||
|
|
||||||
|
private var webSocketTask: URLSessionWebSocketTask?
|
||||||
|
private var heartbeatTimer: Timer?
|
||||||
|
private var reconnectTimer: Timer?
|
||||||
|
private var isDisconnectingManually = false
|
||||||
|
|
||||||
|
private var lastToken: String?
|
||||||
|
private var lastServerUrl: String?
|
||||||
|
|
||||||
|
private var heartbeatAt: Date?
|
||||||
|
var heartbeatDelay: TimeInterval?
|
||||||
|
|
||||||
|
private let connectLock = NSLock()
|
||||||
|
|
||||||
|
private let packetSubject = PassthroughSubject<WebSocketPacket, Error>()
|
||||||
|
private let stateSubject = CurrentValueSubject<WebSocketState, Never>(.disconnected) // Changed to CurrentValueSubject
|
||||||
|
|
||||||
|
private var currentConnectionState: WebSocketState = .disconnected { // New property
|
||||||
|
didSet {
|
||||||
|
// Only send updates if the state has actually changed
|
||||||
|
if oldValue != currentConnectionState {
|
||||||
|
stateSubject.send(currentConnectionState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var packetStream: AnyPublisher<WebSocketPacket, Error> {
|
||||||
|
packetSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
var stateStream: AnyPublisher<WebSocketState, Never> {
|
||||||
|
stateSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectWebSocket(token: String, serverUrl: String) {
|
||||||
|
webSocketQueue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.connectLock.lock()
|
||||||
|
defer { self.connectLock.unlock() }
|
||||||
|
|
||||||
|
// Prevent redundant connection attempts
|
||||||
|
if self.currentConnectionState == .connecting || self.currentConnectionState == .connected {
|
||||||
|
print("[WebSocket] Already connecting or connected, ignoring new connect request.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentConnectionState = .connecting
|
||||||
|
|
||||||
|
// Ensure any existing task is cancelled before starting a new one
|
||||||
|
self.webSocketTask?.cancel(with: .goingAway, reason: nil)
|
||||||
|
self.webSocketTask = nil
|
||||||
|
|
||||||
|
self.isDisconnectingManually = false // Reset this flag for a new connection attempt
|
||||||
|
|
||||||
|
self.lastToken = token
|
||||||
|
self.lastServerUrl = serverUrl
|
||||||
|
|
||||||
|
guard var urlComponents = URLComponents(string: serverUrl) else {
|
||||||
|
self.currentConnectionState = .error("Invalid server URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
urlComponents.scheme = urlComponents.scheme?.replacingOccurrences(of: "http", with: "ws")
|
||||||
|
urlComponents.path = "/ws"
|
||||||
|
urlComponents.queryItems = [URLQueryItem(name: "deviceAlt", value: "watch")]
|
||||||
|
|
||||||
|
guard let url = urlComponents.url else {
|
||||||
|
self.currentConnectionState = .error("Invalid WebSocket URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
print("[WebSocket] Trying connecting to \(url)")
|
||||||
|
|
||||||
|
self.webSocketTask = self.session.webSocketTask(with: request)
|
||||||
|
self.webSocketTask?.resume()
|
||||||
|
|
||||||
|
self.listenForWebSocketMessages()
|
||||||
|
self.scheduleHeartbeat()
|
||||||
|
self.currentConnectionState = .connected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listenForWebSocketMessages() {
|
||||||
|
// Ensure webSocketTask is still valid before attempting to receive
|
||||||
|
guard let task = webSocketTask else {
|
||||||
|
print("[WebSocket] listenForWebSocketMessages: webSocketTask is nil, stopping listen.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task.receive { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .failure(let error):
|
||||||
|
print("[WebSocket] Error in receiving message: \(error)")
|
||||||
|
// Only attempt to reconnect if not manually disconnecting
|
||||||
|
if !self.isDisconnectingManually {
|
||||||
|
self.currentConnectionState = .error(error.localizedDescription)
|
||||||
|
self.scheduleReconnect()
|
||||||
|
} else {
|
||||||
|
// If manually disconnecting, just ensure state is disconnected
|
||||||
|
self.currentConnectionState = .disconnected
|
||||||
|
}
|
||||||
|
case .success(let message):
|
||||||
|
switch message {
|
||||||
|
case .string(let text):
|
||||||
|
self.handleWebSocketMessage(text: text)
|
||||||
|
case .data(let data):
|
||||||
|
if let text = String(data: data, encoding: .utf8) {
|
||||||
|
self.handleWebSocketMessage(text: text)
|
||||||
|
}
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Continue listening for next message only if task is still valid
|
||||||
|
if self.webSocketTask === task { // Check if it's the same task
|
||||||
|
self.listenForWebSocketMessages()
|
||||||
|
} else {
|
||||||
|
print("[WebSocket] listenForWebSocketMessages: Task changed, stopping listen for old task.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleWebSocketMessage(text: String) {
|
||||||
|
guard let data = text.data(using: .utf8) else {
|
||||||
|
print("[WebSocket] Could not convert message to data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||||
|
let type = json["type"] as? String
|
||||||
|
{
|
||||||
|
let packet = WebSocketPacket(
|
||||||
|
type: type,
|
||||||
|
data: json["data"] as? [String: Any],
|
||||||
|
endpoint: json["endpoint"] as? String,
|
||||||
|
errorMessage: json["errorMessage"] as? String
|
||||||
|
)
|
||||||
|
|
||||||
|
print("[WebSocket] Received packet: \(packet.type) \(packet.errorMessage ?? "")")
|
||||||
|
|
||||||
|
if packet.type == "error.dupe" {
|
||||||
|
self.currentConnectionState = .duplicateDevice
|
||||||
|
self.disconnectWebSocket()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if packet.type == "pong" {
|
||||||
|
if let beatAt = self.heartbeatAt {
|
||||||
|
let now = Date()
|
||||||
|
self.heartbeatDelay = now.timeIntervalSince(beatAt)
|
||||||
|
print("[WebSocket] Server respond last heartbeat for \((self.heartbeatDelay ?? 0) * 1000) ms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.packetSubject.send(packet)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[WebSocket] Could not parse message json: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleReconnect() {
|
||||||
|
reconnectTimer?.invalidate()
|
||||||
|
reconnectTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||||
|
guard let self = self, let token = self.lastToken, let serverUrl = self.lastServerUrl else { return }
|
||||||
|
print("[WebSocket] Attempting to reconnect...")
|
||||||
|
|
||||||
|
// No need to call disconnectWebSocket here, connectWebSocket will handle cancelling old task
|
||||||
|
self.isDisconnectingManually = false // Reset for the new connection attempt
|
||||||
|
|
||||||
|
self.connectWebSocket(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleHeartbeat() {
|
||||||
|
heartbeatTimer?.invalidate()
|
||||||
|
heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in
|
||||||
|
self?.beatTheHeart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beatTheHeart() {
|
||||||
|
heartbeatAt = Date()
|
||||||
|
print("[WebSocket] We\'re beating the heart! \(String(describing: self.heartbeatAt))")
|
||||||
|
sendWebSocketMessage(message: "{\"type\":\"ping\"}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendWebSocketMessage(message: String) {
|
||||||
|
webSocketTask?.send(.string(message)) { error in
|
||||||
|
if let error = error {
|
||||||
|
print("[WebSocket] Error sending message: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnectWebSocket() {
|
||||||
|
isDisconnectingManually = true
|
||||||
|
reconnectTimer?.invalidate()
|
||||||
|
heartbeatTimer?.invalidate()
|
||||||
|
|
||||||
|
// Cancel the task and then nil it out
|
||||||
|
webSocketTask?.cancel(with: .goingAway, reason: nil)
|
||||||
|
webSocketTask = nil // Set to nil immediately after cancelling
|
||||||
|
|
||||||
|
self.currentConnectionState = .disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
56
ios/WatchRunner Watch App/State/AppState.swift
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// AppState.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// MARK: - App State
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class AppState: ObservableObject {
|
||||||
|
@Published var token: String? = nil
|
||||||
|
@Published var serverUrl: String? = nil
|
||||||
|
@Published var isReady = false
|
||||||
|
|
||||||
|
let networkService = NetworkService()
|
||||||
|
private var wcService = WatchConnectivityService()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private var hasAttemptedConnection = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
wcService.$token.combineLatest(wcService.$serverUrl, wcService.$isFetched)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] (token: String?, serverUrl: String?, isFetched: Bool?) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.token = token
|
||||||
|
self.serverUrl = serverUrl
|
||||||
|
|
||||||
|
if let token = token, let serverUrl = serverUrl, !token.isEmpty, !serverUrl.isEmpty {
|
||||||
|
self.isReady = true
|
||||||
|
// Only connect once when we have valid credentials and tried fetch from phone
|
||||||
|
if !self.hasAttemptedConnection && isFetched == true {
|
||||||
|
self.hasAttemptedConnection = true
|
||||||
|
print("[AppState] Connecting WebSocket to server: \(serverUrl)")
|
||||||
|
self.networkService.connectWebSocket(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.isReady = false
|
||||||
|
if self.hasAttemptedConnection {
|
||||||
|
self.hasAttemptedConnection = false
|
||||||
|
// Disconnect WebSocket if token or serverUrl become invalid
|
||||||
|
self.networkService.disconnectWebSocket()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestData() {
|
||||||
|
wcService.requestDataFromPhone()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
//
|
||||||
|
// WatchConnectivityService.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import WatchConnectivity
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// MARK: - Watch Connectivity
|
||||||
|
|
||||||
|
class WatchConnectivityService: NSObject, WCSessionDelegate, ObservableObject {
|
||||||
|
@Published var token: String?
|
||||||
|
@Published var serverUrl: String?
|
||||||
|
@Published var isFetched: Bool?
|
||||||
|
|
||||||
|
private let session: WCSession
|
||||||
|
private let userDefaults = UserDefaults.standard
|
||||||
|
private let tokenKey = "token"
|
||||||
|
private let serverUrlKey = "serverUrl"
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.session = .default
|
||||||
|
super.init()
|
||||||
|
print("[watchOS] Activating WCSession")
|
||||||
|
self.session.delegate = self
|
||||||
|
self.session.activate()
|
||||||
|
|
||||||
|
// Load cached data
|
||||||
|
self.token = userDefaults.string(forKey: tokenKey)
|
||||||
|
self.serverUrl = userDefaults.string(forKey: serverUrlKey)
|
||||||
|
self.isFetched = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("[watchOS] WCSession activation failed with error: \(error.localizedDescription)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("[watchOS] WCSession activated with state: \(activationState.rawValue)")
|
||||||
|
if activationState == .activated {
|
||||||
|
requestDataFromPhone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
|
||||||
|
print("[watchOS] Received message: \(message)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let token = message["token"] as? String {
|
||||||
|
self.token = token
|
||||||
|
self.userDefaults.set(token, forKey: self.tokenKey)
|
||||||
|
}
|
||||||
|
if let serverUrl = message["serverUrl"] as? String {
|
||||||
|
self.serverUrl = serverUrl
|
||||||
|
self.userDefaults.set(serverUrl, forKey: self.serverUrlKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestDataFromPhone() {
|
||||||
|
if self.isFetched == true {
|
||||||
|
print("[watchOS] Skipped fetch from phone due to tried.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard session.isReachable else {
|
||||||
|
self.isFetched = true
|
||||||
|
print("[watchOS] Phone is not reachable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[watchOS] Requesting data from phone")
|
||||||
|
session.sendMessage(["request": "data"]) { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
print("[watchOS] Received reply: \(response)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isFetched = true
|
||||||
|
if let token = response["token"] as? String {
|
||||||
|
self.token = token
|
||||||
|
self.userDefaults.set(token, forKey: self.tokenKey)
|
||||||
|
}
|
||||||
|
if let serverUrl = response["serverUrl"] as? String {
|
||||||
|
self.serverUrl = serverUrl
|
||||||
|
self.userDefaults.set(serverUrl, forKey: self.serverUrlKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} errorHandler: { error in
|
||||||
|
print("[watchOS] sendMessage failed with error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ios/WatchRunner Watch App/Utils/AttachmentUtils.swift
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// AttachmentUtils.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Helper Functions
|
||||||
|
|
||||||
|
func getAttachmentUrl(for fileId: String, serverUrl: String) -> URL? {
|
||||||
|
let urlString: String
|
||||||
|
if fileId.starts(with: "http") {
|
||||||
|
urlString = fileId
|
||||||
|
} else {
|
||||||
|
urlString = "\(serverUrl)/drive/files/\(fileId)"
|
||||||
|
}
|
||||||
|
return URL(string: urlString)
|
||||||
|
}
|
||||||
73
ios/WatchRunner Watch App/ViewModels/ActivityViewModel.swift
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// ActivityViewModel.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// MARK: - View Models
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class ActivityViewModel: ObservableObject {
|
||||||
|
@Published var activities: [SnActivity] = []
|
||||||
|
@Published var isLoading = false
|
||||||
|
@Published var isLoadingMore = false
|
||||||
|
@Published var errorMessage: String?
|
||||||
|
@Published var hasMore = false
|
||||||
|
|
||||||
|
private let networkService = NetworkService()
|
||||||
|
let filter: String
|
||||||
|
private var isMock = false
|
||||||
|
private var hasFetched = false
|
||||||
|
private var nextCursor: String?
|
||||||
|
|
||||||
|
init(filter: String, mockActivities: [SnActivity]? = nil) {
|
||||||
|
self.filter = filter
|
||||||
|
if let mockActivities = mockActivities {
|
||||||
|
self.activities = mockActivities
|
||||||
|
self.isMock = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchActivities(token: String, serverUrl: String) async {
|
||||||
|
if isMock || hasFetched { return }
|
||||||
|
guard !isLoading else { return }
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = nil
|
||||||
|
hasFetched = true
|
||||||
|
nextCursor = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await networkService.fetchActivities(filter: filter, cursor: nil, token: token, serverUrl: serverUrl)
|
||||||
|
self.activities = response.activities
|
||||||
|
self.hasMore = response.hasMore
|
||||||
|
self.nextCursor = response.nextCursor
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = error.localizedDescription
|
||||||
|
print("[watchOS] fetchActivities failed with error: \(error)")
|
||||||
|
hasFetched = false
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMoreActivities(token: String, serverUrl: String) async {
|
||||||
|
guard !isLoadingMore && hasMore && nextCursor != nil else { return }
|
||||||
|
isLoadingMore = true
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await networkService.fetchActivities(filter: filter, cursor: nextCursor, token: token, serverUrl: serverUrl)
|
||||||
|
self.activities.append(contentsOf: response.activities)
|
||||||
|
self.hasMore = response.hasMore
|
||||||
|
self.nextCursor = response.nextCursor
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = error.localizedDescription
|
||||||
|
print("[watchOS] loadMoreActivities failed with error: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingMore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// ComposePostViewModel.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class ComposePostViewModel: ObservableObject {
|
||||||
|
@Published var title = ""
|
||||||
|
@Published var content = ""
|
||||||
|
@Published var isPosting = false
|
||||||
|
@Published var errorMessage: String?
|
||||||
|
@Published var didPost = false
|
||||||
|
|
||||||
|
private let networkService = NetworkService()
|
||||||
|
|
||||||
|
func createPost(token: String, serverUrl: String) async {
|
||||||
|
guard !isPosting else { return }
|
||||||
|
isPosting = true
|
||||||
|
errorMessage = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await networkService.createPost(title: title, content: content, token: token, serverUrl: serverUrl)
|
||||||
|
didPost = true
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
isPosting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
284
ios/WatchRunner Watch App/Views/AccountView.swift
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
//
|
||||||
|
// AccountView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AccountView: View {
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@State private var user: SnAccount?
|
||||||
|
@State private var status: SnAccountStatus?
|
||||||
|
@State private var isLoading = false
|
||||||
|
@State private var error: Error?
|
||||||
|
@State private var showingClearConfirmation = false
|
||||||
|
|
||||||
|
@StateObject private var profileImageLoader = ImageLoader()
|
||||||
|
@StateObject private var bannerImageLoader = ImageLoader()
|
||||||
|
|
||||||
|
private let networkService = NetworkService()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
if isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.padding()
|
||||||
|
} else if let error = error {
|
||||||
|
VStack {
|
||||||
|
Text("Failed to load account")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
} else if let user = user {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
// Banner
|
||||||
|
if user.profile.background != nil {
|
||||||
|
if bannerImageLoader.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.frame(height: 80)
|
||||||
|
} else if let bannerImage = bannerImageLoader.image {
|
||||||
|
bannerImage
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(height: 80)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(8)
|
||||||
|
} else if bannerImageLoader.errorMessage != nil {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray.opacity(0.3))
|
||||||
|
.frame(height: 80)
|
||||||
|
.cornerRadius(8)
|
||||||
|
} else {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray.opacity(0.3))
|
||||||
|
.frame(height: 80)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile Picture
|
||||||
|
HStack(spacing: 16)
|
||||||
|
{
|
||||||
|
if profileImageLoader.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
} else if let profileImage = profileImageLoader.image {
|
||||||
|
profileImage
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.clipShape(Circle())
|
||||||
|
} else if profileImageLoader.errorMessage != nil {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.red.opacity(0.3))
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.overlay(
|
||||||
|
Image(systemName: "exclamationmark.triangle")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.foregroundColor(.red)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.gray.opacity(0.3))
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.overlay(
|
||||||
|
Image(systemName: "person.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Username and Handle
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(user.nick)
|
||||||
|
.font(.headline)
|
||||||
|
Text("@\(user.name)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("Status")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
if status?.isCustomized == true {
|
||||||
|
Button(action: {
|
||||||
|
showingClearConfirmation = true
|
||||||
|
}) {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.red.opacity(0.1))
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
Image(systemName: "trash")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
}
|
||||||
|
NavigationLink(
|
||||||
|
destination: StatusCreationView(initialStatus: status?.isCustomized == true ? status : nil)
|
||||||
|
.environmentObject(appState)
|
||||||
|
) {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.blue.opacity(0.1))
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
Image(systemName: "pencil")
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let status = status {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Circle()
|
||||||
|
.fill(status.isOnline ? Color.green : Color.gray)
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
Text(status.label.isEmpty ? "No status" : status.label)
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.isInvisible {
|
||||||
|
Text("Invisible")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
if status.isNotDisturb {
|
||||||
|
Text("Do Not Disturb")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
if let clearedAt = status.clearedAt {
|
||||||
|
Text("Clears: \(clearedAt.formatted(date: .abbreviated, time: .shortened))")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("No status set")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level and Progress
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Level \(user.profile.level)")
|
||||||
|
.font(.title3)
|
||||||
|
.bold()
|
||||||
|
ProgressView(value: user.profile.levelingProgress)
|
||||||
|
.progressViewStyle(LinearProgressViewStyle())
|
||||||
|
.frame(height: 8)
|
||||||
|
Text("Experience: \(user.profile.experience)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bio
|
||||||
|
if let bio = user.profile.bio, !bio.isEmpty {
|
||||||
|
Text(bio)
|
||||||
|
.font(.body)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(alignment: .leading)
|
||||||
|
} else {
|
||||||
|
Text("No bio available")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member since
|
||||||
|
Text("Joined at \(user.createdAt.formatted(.dateTime.month(.abbreviated).year()))")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(alignment: .leading)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
// Load images when user data is available
|
||||||
|
.task(id: user.profile.picture?.id) {
|
||||||
|
if let serverUrl = appState.serverUrl, let pictureId = user.profile.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token {
|
||||||
|
await profileImageLoader.loadImage(from: imageUrl, token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task(id: user.profile.background?.id) {
|
||||||
|
if let serverUrl = appState.serverUrl, let backgroundId = user.profile.background?.id, let imageUrl = getAttachmentUrl(for: backgroundId, serverUrl: serverUrl), let token = appState.token {
|
||||||
|
await bannerImageLoader.loadImage(from: imageUrl, token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("No account data")
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Account")
|
||||||
|
.confirmationDialog("Clear Status", isPresented: $showingClearConfirmation) {
|
||||||
|
Button("Clear Status", role: .destructive) {
|
||||||
|
Task {
|
||||||
|
await clearStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
} message: {
|
||||||
|
Text("Are you sure you want to clear your status? This action cannot be undone.")
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
Task.detached {
|
||||||
|
await loadUserProfile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadUserProfile() async {
|
||||||
|
guard let token = appState.token, let serverUrl = appState.serverUrl else {
|
||||||
|
error = NSError(domain: "AccountView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
error = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
user = try await networkService.fetchUserProfile(token: token, serverUrl: serverUrl)
|
||||||
|
status = try await networkService.fetchAccountStatus(token: token, serverUrl: serverUrl)
|
||||||
|
} catch {
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearStatus() async {
|
||||||
|
guard let token = appState.token, let serverUrl = appState.serverUrl else {
|
||||||
|
error = NSError(domain: "AccountView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await networkService.clearStatus(token: token, serverUrl: serverUrl)
|
||||||
|
// Refresh status after clearing
|
||||||
|
status = try await networkService.fetchAccountStatus(token: token, serverUrl: serverUrl)
|
||||||
|
} catch {
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
AccountView()
|
||||||
|
.environmentObject(AppState())
|
||||||
|
}
|
||||||
86
ios/WatchRunner Watch App/Views/ActivityListView.swift
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// ActivityListView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Views
|
||||||
|
|
||||||
|
struct ActivityListView: View {
|
||||||
|
@StateObject private var viewModel: ActivityViewModel
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
|
||||||
|
init(filter: String, mockActivities: [SnActivity]? = nil) {
|
||||||
|
_viewModel = StateObject(wrappedValue: ActivityViewModel(filter: filter, mockActivities: mockActivities))
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if viewModel.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else if let errorMessage = viewModel.errorMessage {
|
||||||
|
VStack {
|
||||||
|
Text("Error fetching data")
|
||||||
|
.font(.headline)
|
||||||
|
Text(errorMessage)
|
||||||
|
.font(.caption)
|
||||||
|
.lineLimit(nil)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
} else if viewModel.activities.isEmpty {
|
||||||
|
Text("No activities found.")
|
||||||
|
} else {
|
||||||
|
List {
|
||||||
|
ForEach(viewModel.activities) { activity in
|
||||||
|
switch activity.type {
|
||||||
|
case "posts.new", "posts.new.replies":
|
||||||
|
if case .post(let post) = activity.data {
|
||||||
|
NavigationLink(
|
||||||
|
destination: PostDetailView(post: post).environmentObject(appState)
|
||||||
|
) {
|
||||||
|
PostRowView(post: post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "discovery":
|
||||||
|
if case .discovery(let discoveryData) = activity.data {
|
||||||
|
DiscoveryView(discoveryData: discoveryData)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
Text("Unknown activity type: \(activity.type)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if viewModel.hasMore {
|
||||||
|
if viewModel.isLoadingMore {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
ProgressView()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button("Load More") {
|
||||||
|
Task {
|
||||||
|
if let token = appState.token, let serverUrl = appState.serverUrl {
|
||||||
|
await viewModel.loadMoreActivities(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if appState.isReady, let token = appState.token, let serverUrl = appState.serverUrl {
|
||||||
|
Task.detached {
|
||||||
|
await viewModel.fetchActivities(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(viewModel.filter)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
62
ios/WatchRunner Watch App/Views/AppInfoHeaderView.swift
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// AppInfoHeader.swift
|
||||||
|
// Runner
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AppInfoHeaderView : View {
|
||||||
|
@EnvironmentObject var appState: AppState // Access AppState
|
||||||
|
@State private var webSocketConnectionState: WebSocketState = .disconnected // New state for WebSocket status
|
||||||
|
@State private var cancellables = Set<AnyCancellable>() // For managing subscriptions
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image("Logo")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Solian").font(.headline)
|
||||||
|
Text("for Apple Watch").font(.system(size: 11))
|
||||||
|
|
||||||
|
// Display WebSocket connection status
|
||||||
|
Text(webSocketStatusMessage)
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
setupWebSocketListeners()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
cancellables.forEach { $0.cancel() }
|
||||||
|
cancellables.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var webSocketStatusMessage: String {
|
||||||
|
switch webSocketConnectionState {
|
||||||
|
case .connected: return "Connected"
|
||||||
|
case .connecting: return "Connecting..."
|
||||||
|
case .disconnected: return "Disconnected"
|
||||||
|
case .serverDown: return "Server Down"
|
||||||
|
case .duplicateDevice: return "Duplicate Device"
|
||||||
|
case .error(let msg): return "Error: \(msg)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupWebSocketListeners() {
|
||||||
|
appState.networkService.stateStream
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { state in
|
||||||
|
webSocketConnectionState = state
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
109
ios/WatchRunner Watch App/Views/AttachmentView.swift
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
//
|
||||||
|
// AttachmentImageView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AVKit
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
struct AttachmentView: View {
|
||||||
|
let attachment: SnCloudFile
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@StateObject private var imageLoader = ImageLoader()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if let mimeType = attachment.mimeType {
|
||||||
|
if mimeType.starts(with: "image") {
|
||||||
|
if let serverUrl = appState.serverUrl, let imageUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) {
|
||||||
|
NavigationLink(
|
||||||
|
destination: ImageViewer(imageUrl: imageUrl).environmentObject(appState)
|
||||||
|
) {
|
||||||
|
if imageLoader.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else if let image = imageLoader.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.cornerRadius(8)
|
||||||
|
} else if let errorMessage = imageLoader.errorMessage {
|
||||||
|
Text("Failed to load attachment: \(errorMessage)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.cornerRadius(8)
|
||||||
|
} else {
|
||||||
|
Text("File: \(attachment.id)")
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
} else {
|
||||||
|
Text("Image URL not available.")
|
||||||
|
}
|
||||||
|
} else if mimeType.starts(with: "video") {
|
||||||
|
if let serverUrl = appState.serverUrl, let videoUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) {
|
||||||
|
NavigationLink(destination: VideoPlayerView(videoUrl: videoUrl)) {
|
||||||
|
if imageLoader.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else if let image = imageLoader.image {
|
||||||
|
ZStack {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.cornerRadius(8)
|
||||||
|
|
||||||
|
Image(systemName: "play.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 36, height: 36)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.shadow(color: .black.opacity(0.6), radius: 4, x: 0, y: 2)
|
||||||
|
}
|
||||||
|
} else if imageLoader.errorMessage != nil {
|
||||||
|
Image(systemName: "play.rectangle.fill")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.cornerRadius(8)
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
} else {
|
||||||
|
Text("Video URL not available.")
|
||||||
|
}
|
||||||
|
} else if mimeType.starts(with: "audio") {
|
||||||
|
if let serverUrl = appState.serverUrl, let audioUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) {
|
||||||
|
AudioPlayerView(audioUrl: audioUrl)
|
||||||
|
} else {
|
||||||
|
Text("Cannot play audio: URL not available.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Unsupported media type: \(mimeType)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("File: \(attachment.id) (No MIME type)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task(id: attachment.id) {
|
||||||
|
if let serverUrl = appState.serverUrl, let attachmentUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl), let token = appState.token {
|
||||||
|
if attachment.mimeType?.starts(with: "image") == true {
|
||||||
|
await imageLoader.loadImage(from: attachmentUrl, token: token)
|
||||||
|
}
|
||||||
|
if attachment.mimeType?.starts(with: "video") == true {
|
||||||
|
let thumbnailUrl = attachmentUrl
|
||||||
|
.appending(queryItems: [URLQueryItem(name: "thumbnail", value: "true")]) // Construct thumbnail URL
|
||||||
|
await imageLoader.loadImage(from: thumbnailUrl, token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
ios/WatchRunner Watch App/Views/AudioPlayerView.swift
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
//
|
||||||
|
// AudioPlayerView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
struct AudioPlayerView: View {
|
||||||
|
let audioUrl: URL
|
||||||
|
@State private var player: AVPlayer?
|
||||||
|
@State private var isPlaying: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
if player != nil {
|
||||||
|
Button(action: togglePlayPause) {
|
||||||
|
Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
||||||
|
.font(.largeTitle)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
} else {
|
||||||
|
Text("Loading audio...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
player = AVPlayer(url: audioUrl)
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
player?.pause()
|
||||||
|
player = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func togglePlayPause() {
|
||||||
|
guard let player = player else { return }
|
||||||
|
if isPlaying {
|
||||||
|
player.pause()
|
||||||
|
} else {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
isPlaying.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
785
ios/WatchRunner Watch App/Views/ChatViews.swift
Normal file
@@ -0,0 +1,785 @@
|
|||||||
|
//
|
||||||
|
// ChatView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ChatView: View {
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@State private var selectedTab = 0
|
||||||
|
@State private var chatRooms: [SnChatRoom] = []
|
||||||
|
@State private var chatInvites: [SnChatMember] = []
|
||||||
|
@State private var isLoading = false
|
||||||
|
@State private var error: Error?
|
||||||
|
@State private var showingInvites = false
|
||||||
|
|
||||||
|
private let tabs = ["All", "Direct", "Group"]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TabView(selection: $selectedTab) {
|
||||||
|
ForEach(0..<tabs.count, id: \.self) { index in
|
||||||
|
VStack {
|
||||||
|
if isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else if error != nil {
|
||||||
|
VStack {
|
||||||
|
Text("Error loading chats")
|
||||||
|
.font(.caption)
|
||||||
|
Button("Retry") {
|
||||||
|
Task {
|
||||||
|
await loadChatRooms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.caption2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ChatRoomListView(
|
||||||
|
chatRooms: filteredChatRooms(for: index),
|
||||||
|
selectedTab: index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Text(tabs[index])
|
||||||
|
}
|
||||||
|
.tag(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabViewStyle(.page)
|
||||||
|
.navigationTitle("Chat")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Button {
|
||||||
|
showingInvites = true
|
||||||
|
} label: {
|
||||||
|
ZStack {
|
||||||
|
Image(systemName: "envelope")
|
||||||
|
if !chatInvites.isEmpty {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.red)
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
.offset(x: 8, y: -8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingInvites) {
|
||||||
|
ChatInvitesView(invites: $chatInvites, appState: appState)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
Task.detached {
|
||||||
|
await loadChatRooms()
|
||||||
|
await loadChatInvites()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func filteredChatRooms(for tabIndex: Int) -> [SnChatRoom] {
|
||||||
|
switch tabIndex {
|
||||||
|
case 0: // All
|
||||||
|
return chatRooms
|
||||||
|
case 1: // Direct
|
||||||
|
return chatRooms.filter { $0.type == 1 }
|
||||||
|
case 2: // Group
|
||||||
|
return chatRooms.filter { $0.type != 1 }
|
||||||
|
default:
|
||||||
|
return chatRooms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadChatRooms() async {
|
||||||
|
guard let token = appState.token, let serverUrl = appState.serverUrl else { return }
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
error = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await appState.networkService.fetchChatRooms(token: token, serverUrl: serverUrl)
|
||||||
|
chatRooms = response.rooms
|
||||||
|
} catch {
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadChatInvites() async {
|
||||||
|
guard let token = appState.token, let serverUrl = appState.serverUrl else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await appState.networkService.fetchChatInvites(token: token, serverUrl: serverUrl)
|
||||||
|
chatInvites = response.invites
|
||||||
|
} catch {
|
||||||
|
// Handle error silently for invites
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatRoomListView: View {
|
||||||
|
let chatRooms: [SnChatRoom]
|
||||||
|
let selectedTab: Int
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if chatRooms.isEmpty {
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "message")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("No chats yet")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List(chatRooms) { room in
|
||||||
|
ChatRoomListItem(room: room)
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatRoomListItem: View {
|
||||||
|
let room: SnChatRoom
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@StateObject private var avatarLoader = ImageLoader()
|
||||||
|
|
||||||
|
private var displayName: String {
|
||||||
|
if room.type == 1, let members = room.members, !members.isEmpty {
|
||||||
|
// For direct messages, show the other member's name
|
||||||
|
return members[0].account.nick
|
||||||
|
} else {
|
||||||
|
// For group chats, show room name or fallback
|
||||||
|
return room.name ?? "Group Chat"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var subtitle: String {
|
||||||
|
if room.type == 1, let members = room.members, members.count > 1 {
|
||||||
|
// For direct messages, show member usernames
|
||||||
|
return members.map { "@\($0.account.name)" }.joined(separator: ", ")
|
||||||
|
} else if let description = room.description {
|
||||||
|
// For group chats with description
|
||||||
|
return description
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var avatarPictureId: String? {
|
||||||
|
if room.type == 1, let members = room.members, !members.isEmpty {
|
||||||
|
// For direct messages, use the other member's avatar
|
||||||
|
return members[0].account.profile.picture?.id
|
||||||
|
} else {
|
||||||
|
// For group chats, use room picture
|
||||||
|
return room.picture?.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink(
|
||||||
|
destination: ChatRoomView(room: room)
|
||||||
|
.environmentObject(appState)
|
||||||
|
) {
|
||||||
|
HStack {
|
||||||
|
// Avatar using ImageLoader pattern
|
||||||
|
Group {
|
||||||
|
if avatarLoader.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
} else if let image = avatarLoader.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.clipShape(Circle())
|
||||||
|
} else if avatarLoader.errorMessage != nil {
|
||||||
|
// Error state - show fallback
|
||||||
|
Circle()
|
||||||
|
.fill(Color.gray.opacity(0.3))
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.overlay(
|
||||||
|
Text(displayName.prefix(1).uppercased())
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// No image available - show initial
|
||||||
|
Circle()
|
||||||
|
.fill(Color.gray.opacity(0.3))
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.overlay(
|
||||||
|
Text(displayName.prefix(1).uppercased())
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task(id: avatarPictureId) {
|
||||||
|
if let serverUrl = appState.serverUrl,
|
||||||
|
let pictureId = avatarPictureId,
|
||||||
|
let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl),
|
||||||
|
let token = appState.token {
|
||||||
|
await avatarLoader.loadImage(from: imageUrl, token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(displayName)
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
if !subtitle.isEmpty {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Unread count badge placeholder
|
||||||
|
// In a full implementation, this would show unread count
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ChatRoomView: View {
|
||||||
|
let room: SnChatRoom
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@State private var messages: [SnChatMessage] = []
|
||||||
|
@State private var isLoading = false
|
||||||
|
@State private var error: Error?
|
||||||
|
@State private var wsState: WebSocketState = .disconnected // New state for WebSocket status
|
||||||
|
@State private var hasLoadedMessages = false // Track if messages have been loaded
|
||||||
|
@State private var messageText = "" // Text input for sending messages
|
||||||
|
@State private var isSending = false // Track sending state
|
||||||
|
@State private var isInputHidden = false // Track if input should be hidden during scrolling
|
||||||
|
@State private var scrollTimer: Timer? // Timer to show input after scrolling stops
|
||||||
|
|
||||||
|
@State private var cancellables = Set<AnyCancellable>() // For managing subscriptions
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
// Display WebSocket connection status
|
||||||
|
if (wsState != .connected)
|
||||||
|
{
|
||||||
|
Text(webSocketStatusMessage)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.animation(.easeInOut, value: wsState) // Animate status changes
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else if error != nil {
|
||||||
|
VStack {
|
||||||
|
Text("Error loading messages")
|
||||||
|
.font(.caption)
|
||||||
|
Button("Retry") {
|
||||||
|
Task {
|
||||||
|
await loadMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.caption2)
|
||||||
|
}
|
||||||
|
} else if messages.isEmpty {
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "bubble.left")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("No messages yet")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ScrollViewReader { scrollView in
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 8) {
|
||||||
|
ForEach(messages) { message in
|
||||||
|
ChatMessageItem(message: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
// Scroll to bottom when messages load
|
||||||
|
if let lastMessage = messages.last {
|
||||||
|
scrollView.scrollTo(lastMessage.id, anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: messages.count) { _, _ in
|
||||||
|
// Scroll to bottom when new messages arrive
|
||||||
|
if let lastMessage = messages.last {
|
||||||
|
withAnimation {
|
||||||
|
scrollView.scrollTo(lastMessage.id, anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onScrollPhaseChange { _, phase in
|
||||||
|
switch phase {
|
||||||
|
case .interacting:
|
||||||
|
if !isInputHidden {
|
||||||
|
withAnimation(.easeOut(duration: 0.2)) {
|
||||||
|
isInputHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .idle:
|
||||||
|
withAnimation(.easeIn(duration: 0.3)) {
|
||||||
|
isInputHidden = false
|
||||||
|
}
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message input area
|
||||||
|
if !isInputHidden {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
TextField("Send message...", text: $messageText)
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.disabled(isSending)
|
||||||
|
.frame(height: 40)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await sendMessage()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if isSending {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "arrow.up.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.buttonStyle(.glass)
|
||||||
|
.disabled(messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSending)
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.top, 8)
|
||||||
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(room.name ?? "Chat")
|
||||||
|
.task {
|
||||||
|
await loadMessages()
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
setupWebSocketListeners()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
cancellables.forEach { $0.cancel() }
|
||||||
|
cancellables.removeAll()
|
||||||
|
scrollTimer?.invalidate()
|
||||||
|
scrollTimer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var webSocketStatusMessage: String {
|
||||||
|
switch wsState {
|
||||||
|
case .connected: return "Connected"
|
||||||
|
case .connecting: return "Connecting..."
|
||||||
|
case .disconnected: return "Disconnected"
|
||||||
|
case .serverDown: return "Server Down"
|
||||||
|
case .duplicateDevice: return "Duplicate Device"
|
||||||
|
case .error(let msg): return "Error: \(msg)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadMessages() async {
|
||||||
|
// Prevent reloading if already loaded
|
||||||
|
guard !hasLoadedMessages else { return }
|
||||||
|
|
||||||
|
guard let token = appState.token, let serverUrl = appState.serverUrl else {
|
||||||
|
isLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
error = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
let messages = try await appState.networkService.fetchChatMessages(
|
||||||
|
chatRoomId: room.id,
|
||||||
|
token: token,
|
||||||
|
serverUrl: serverUrl
|
||||||
|
)
|
||||||
|
// Sort with newest messages first (for flipped list, newest will appear at bottom)
|
||||||
|
self.messages = messages.sorted { $0.createdAt < $1.createdAt }
|
||||||
|
hasLoadedMessages = true
|
||||||
|
} catch {
|
||||||
|
print("[watchOS] Error loading messages: \(error.localizedDescription)")
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendMessage() async {
|
||||||
|
let content = messageText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !content.isEmpty,
|
||||||
|
let token = appState.token,
|
||||||
|
let serverUrl = appState.serverUrl else { return }
|
||||||
|
|
||||||
|
isSending = true
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Generate a nonce for the message
|
||||||
|
let nonce = UUID().uuidString
|
||||||
|
|
||||||
|
// Prepare the request data
|
||||||
|
let messageData: [String: Any] = [
|
||||||
|
"content": content,
|
||||||
|
"attachments_id": [], // Empty for now, can be extended for attachments
|
||||||
|
"meta": [:],
|
||||||
|
"nonce": nonce
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create the URL
|
||||||
|
guard let url = URL(string: "\(serverUrl)/sphere/chat/\(room.id)/messages") else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the request
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: messageData, options: [])
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse,
|
||||||
|
(200...299).contains(httpResponse.statusCode) else {
|
||||||
|
throw URLError(.badServerResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response to get the sent message
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
let sentMessage = try decoder.decode(SnChatMessage.self, from: data)
|
||||||
|
|
||||||
|
// Add the message to the local list
|
||||||
|
messages.append(sentMessage)
|
||||||
|
|
||||||
|
// Clear the input
|
||||||
|
messageText = ""
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print("[watchOS] Error sending message: \(error.localizedDescription)")
|
||||||
|
// Could show an error alert here
|
||||||
|
}
|
||||||
|
|
||||||
|
isSending = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendReadReceipt() {
|
||||||
|
let data: [String: Any] = ["chat_room_id": room.id]
|
||||||
|
let packet: [String: Any] = ["type": "messages.read", "data": data, "endpoint": "sphere"]
|
||||||
|
if let jsonData = try? JSONSerialization.data(withJSONObject: packet, options: []),
|
||||||
|
let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||||
|
appState.networkService.sendWebSocketMessage(message: jsonString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupWebSocketListeners() {
|
||||||
|
// Listen for WebSocket packets (new messages)
|
||||||
|
appState.networkService.packetStream
|
||||||
|
.receive(on: DispatchQueue.main) // Ensure UI updates on main thread
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
|
if case .failure(let err) = completion {
|
||||||
|
print("[ChatRoomView] WebSocket packet stream error: \(err.localizedDescription)")
|
||||||
|
}
|
||||||
|
}, receiveValue: { packet in
|
||||||
|
if ["messages.new", "messages.update", "messages.delete"].contains(packet.type),
|
||||||
|
let messageData = packet.data {
|
||||||
|
do {
|
||||||
|
let jsonData = try JSONSerialization.data(withJSONObject: messageData, options: [])
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
let message = try decoder.decode(SnChatMessage.self, from: jsonData)
|
||||||
|
|
||||||
|
if message.chatRoomId == room.id {
|
||||||
|
switch packet.type {
|
||||||
|
case "messages.new":
|
||||||
|
if message.type.hasPrefix("call") {
|
||||||
|
// TODO: Handle ongoing call
|
||||||
|
}
|
||||||
|
if !messages.contains(where: { $0.id == message.id }) {
|
||||||
|
messages.append(message)
|
||||||
|
}
|
||||||
|
sendReadReceipt()
|
||||||
|
case "messages.update":
|
||||||
|
if let index = messages.firstIndex(where: { $0.id == message.id }) {
|
||||||
|
messages[index] = message
|
||||||
|
}
|
||||||
|
case "messages.delete":
|
||||||
|
messages.removeAll(where: { $0.id == message.id })
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[ChatRoomView] Error decoding message from websocket: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// Listen for WebSocket connection state changes
|
||||||
|
appState.networkService.stateStream
|
||||||
|
.receive(on: DispatchQueue.main) // Ensure UI updates on main thread
|
||||||
|
.sink { state in
|
||||||
|
wsState = state
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatMessageItem: View {
|
||||||
|
let message: SnChatMessage
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@StateObject private var avatarLoader = ImageLoader()
|
||||||
|
|
||||||
|
private var avatarPictureId: String? {
|
||||||
|
message.sender.account.profile.picture?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 8) {
|
||||||
|
// Avatar
|
||||||
|
Group {
|
||||||
|
if avatarLoader.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
} else if let image = avatarLoader.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
.clipShape(Circle())
|
||||||
|
} else {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.gray.opacity(0.3))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
.overlay(
|
||||||
|
Text(message.sender.account.nick.prefix(1).uppercased())
|
||||||
|
.font(.system(size: 10, weight: .medium))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task(id: avatarPictureId) {
|
||||||
|
if let serverUrl = appState.serverUrl,
|
||||||
|
let pictureId = avatarPictureId,
|
||||||
|
let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl),
|
||||||
|
let token = appState.token {
|
||||||
|
await avatarLoader.loadImage(from: imageUrl, token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Text(message.sender.account.nick)
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
Spacer()
|
||||||
|
Text(message.createdAt, style: .time)
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let content = message.content, !content.isEmpty {
|
||||||
|
Text(content)
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.lineLimit(nil)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !message.attachments.isEmpty {
|
||||||
|
AttachmentView(attachment: message.attachments[0])
|
||||||
|
if message.attachments.count > 1 {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "paperclip.circle.fill")
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
.foregroundStyle(.gray)
|
||||||
|
Text("\(message.attachments.count - 1)+ attachments")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatInvitesView: View {
|
||||||
|
@Binding var invites: [SnChatMember]
|
||||||
|
let appState: AppState
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State private var isLoading = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
VStack {
|
||||||
|
if invites.isEmpty {
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "envelope.open")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("No invites")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List(invites) { invite in
|
||||||
|
ChatInviteItem(invite: invite, appState: appState, invites: $invites)
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Invites")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatInviteItem: View {
|
||||||
|
let invite: SnChatMember
|
||||||
|
let appState: AppState
|
||||||
|
@Binding var invites: [SnChatMember]
|
||||||
|
@State private var isAccepting = false
|
||||||
|
@State private var isDeclining = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.gray.opacity(0.3))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
.overlay(
|
||||||
|
Text((invite.chatRoom?.name ?? "C").prefix(1).uppercased())
|
||||||
|
.font(.system(size: 10, weight: .medium))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(invite.chatRoom?.name ?? "Unknown Chat")
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text(invite.role == 100 ? "Owner" : invite.role >= 50 ? "Moderator" : "Member")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
if invite.chatRoom?.type == 1 {
|
||||||
|
Text("Direct")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(Color.blue.opacity(0.1))
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await acceptInvite()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if isAccepting {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(isAccepting || isDeclining)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await declineInvite()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if isDeclining {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(isAccepting || isDeclining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func acceptInvite() async {
|
||||||
|
guard let token = appState.token,
|
||||||
|
let serverUrl = appState.serverUrl,
|
||||||
|
let chatRoomId = invite.chatRoom?.id else { return }
|
||||||
|
|
||||||
|
isAccepting = true
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await appState.networkService.acceptChatInvite(chatRoomId: chatRoomId, token: token, serverUrl: serverUrl)
|
||||||
|
// Remove from invites list
|
||||||
|
invites.removeAll { $0.id == invite.id }
|
||||||
|
} catch {
|
||||||
|
// Handle error - could show alert
|
||||||
|
print("Failed to accept invite: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
isAccepting = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func declineInvite() async {
|
||||||
|
guard let token = appState.token,
|
||||||
|
let serverUrl = appState.serverUrl,
|
||||||
|
let chatRoomId = invite.chatRoom?.id else { return }
|
||||||
|
|
||||||
|
isDeclining = true
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await appState.networkService.declineChatInvite(chatRoomId: chatRoomId, token: token, serverUrl: serverUrl)
|
||||||
|
// Remove from invites list
|
||||||
|
invites.removeAll { $0.id == invite.id }
|
||||||
|
} catch {
|
||||||
|
// Handle error - could show alert
|
||||||
|
print("Failed to decline invite: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
isDeclining = false
|
||||||
|
}
|
||||||
|
}
|
||||||
53
ios/WatchRunner Watch App/Views/ComposePostView.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// ComposePostView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ComposePostView: View {
|
||||||
|
@StateObject private var viewModel = ComposePostViewModel()
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
TextField("Title", text: $viewModel.title)
|
||||||
|
TextField("Content", text: $viewModel.content)
|
||||||
|
}
|
||||||
|
.navigationTitle("New Post")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Cancel", systemImage: "xmark") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Post", systemImage: "square.and.arrow.up") {
|
||||||
|
Task {
|
||||||
|
if let token = appState.token, let serverUrl = appState.serverUrl {
|
||||||
|
await viewModel.createPost(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.disabled(viewModel.isPosting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.didPost) {
|
||||||
|
if viewModel.didPost {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil), actions: {
|
||||||
|
Button("OK") { viewModel.errorMessage = nil }
|
||||||
|
}, message: {
|
||||||
|
Text(viewModel.errorMessage ?? "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
ios/WatchRunner Watch App/Views/DiscoveryViews.swift
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
//
|
||||||
|
// DiscoveryViews.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DiscoveryView: View {
|
||||||
|
let discoveryData: DiscoveryData
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink(destination: DiscoveryDetailView(discoveryData: discoveryData)) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Discovery")
|
||||||
|
.font(.headline)
|
||||||
|
Text("\(discoveryData.items.count) new items to discover")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiscoveryDetailView: View {
|
||||||
|
let discoveryData: DiscoveryData
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List(discoveryData.items) { item in
|
||||||
|
NavigationLink(destination: destinationView(for: item)) {
|
||||||
|
itemView(for: item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Discovery")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func itemView(for item: DiscoveryItem) -> some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
switch item.data {
|
||||||
|
case .realm(let realm):
|
||||||
|
Text("Realm").font(.headline)
|
||||||
|
Text(realm.name).foregroundColor(.secondary)
|
||||||
|
case .publisher(let publisher):
|
||||||
|
Text("Publisher").font(.headline)
|
||||||
|
Text(publisher.name).foregroundColor(.secondary)
|
||||||
|
case .article(let article):
|
||||||
|
Text("Article").font(.headline)
|
||||||
|
Text(article.title).foregroundColor(.secondary)
|
||||||
|
case .unknown:
|
||||||
|
Text("Unknown item")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func destinationView(for item: DiscoveryItem) -> some View {
|
||||||
|
switch item.data {
|
||||||
|
case .realm(let realm):
|
||||||
|
RealmDetailView(realm: realm)
|
||||||
|
case .publisher(let publisher):
|
||||||
|
PublisherDetailView(publisher: publisher)
|
||||||
|
case .article(let article):
|
||||||
|
ArticleDetailView(article: article)
|
||||||
|
case .unknown:
|
||||||
|
Text("Detail view not available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RealmDetailView: View {
|
||||||
|
let realm: SnRealm
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(realm.name).font(.headline)
|
||||||
|
if let description = realm.description {
|
||||||
|
Text(description).font(.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Realm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PublisherDetailView: View {
|
||||||
|
let publisher: SnPublisher
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(publisher.name).font(.headline)
|
||||||
|
if let description = publisher.description {
|
||||||
|
Text(description).font(.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Publisher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ArticleDetailView: View {
|
||||||
|
let article: SnWebArticle
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(article.title).font(.headline)
|
||||||
|
Text(article.url).font(.caption).foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.navigationTitle("Article")
|
||||||
|
}
|
||||||
|
}
|
||||||
59
ios/WatchRunner Watch App/Views/ExploreView.swift
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// ExploreView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// The main view with the TabView for filtering.
|
||||||
|
struct ExploreView: View {
|
||||||
|
@StateObject private var appState = AppState()
|
||||||
|
@State private var isComposing = false
|
||||||
|
@State private var selectedTab: String = "Explore"
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
if appState.isReady {
|
||||||
|
TabView(selection: $selectedTab) {
|
||||||
|
ActivityListView(filter: "Explore")
|
||||||
|
.tag("Explore")
|
||||||
|
.tabItem {
|
||||||
|
Label("Explore", systemImage: "safari")
|
||||||
|
}
|
||||||
|
.labelStyle(.titleOnly)
|
||||||
|
|
||||||
|
ActivityListView(filter: "Subscriptions")
|
||||||
|
.tag("Subscriptions")
|
||||||
|
.tabItem {
|
||||||
|
Label("Subscriptions", systemImage: "star")
|
||||||
|
}
|
||||||
|
.labelStyle(.titleOnly)
|
||||||
|
|
||||||
|
ActivityListView(filter: "Friends")
|
||||||
|
.tag("Friends")
|
||||||
|
.tabItem {
|
||||||
|
Label("Friends", systemImage: "person.2")
|
||||||
|
}
|
||||||
|
.labelStyle(.titleOnly)
|
||||||
|
}
|
||||||
|
.navigationTitle(selectedTab)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(action: { isComposing = true }) {
|
||||||
|
Label("Compose", systemImage: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environmentObject(appState)
|
||||||
|
} else {
|
||||||
|
ProgressView { Text("Connecting to phone...") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $isComposing) {
|
||||||
|
ComposePostView()
|
||||||
|
.environmentObject(appState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
ios/WatchRunner Watch App/Views/ImageViewer.swift
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ImageViewer: View {
|
||||||
|
let imageUrl: URL
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@StateObject private var imageLoader = ImageLoader()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if imageLoader.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else if let image = imageLoader.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.scaledToFit()
|
||||||
|
} else if let errorMessage = imageLoader.errorMessage {
|
||||||
|
Text("Failed to load image: \(errorMessage)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
} else {
|
||||||
|
Text("Failed to load image.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task(id: imageUrl) {
|
||||||
|
if let token = appState.token {
|
||||||
|
await imageLoader.loadImage(from: imageUrl, token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Image")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
198
ios/WatchRunner Watch App/Views/NotificationView.swift
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
|
||||||
|
//
|
||||||
|
// NotificationView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class NotificationViewModel: ObservableObject {
|
||||||
|
@Published var notifications = [SnNotification]()
|
||||||
|
@Published var isLoading = false
|
||||||
|
@Published var isLoadingMore = false
|
||||||
|
@Published var errorMessage: String?
|
||||||
|
@Published var hasMore = false
|
||||||
|
|
||||||
|
private let networkService = NetworkService()
|
||||||
|
private var hasFetched = false
|
||||||
|
private var offset = 0
|
||||||
|
private let pageSize = 20
|
||||||
|
|
||||||
|
func fetchNotifications(token: String, serverUrl: String) async {
|
||||||
|
if hasFetched { return }
|
||||||
|
guard !isLoading else { return }
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = nil
|
||||||
|
hasFetched = true
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await networkService.fetchNotifications(offset: offset, take: pageSize, token: token, serverUrl: serverUrl)
|
||||||
|
self.notifications = response.notifications
|
||||||
|
self.hasMore = response.hasMore
|
||||||
|
offset += response.notifications.count
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = error.localizedDescription
|
||||||
|
print("[watchOS] fetchNotifications failed with error: \(error)")
|
||||||
|
hasFetched = false
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMoreNotifications(token: String, serverUrl: String) async {
|
||||||
|
guard !isLoadingMore && hasMore else { return }
|
||||||
|
isLoadingMore = true
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await networkService.fetchNotifications(offset: offset, take: pageSize, token: token, serverUrl: serverUrl)
|
||||||
|
self.notifications.append(contentsOf: response.notifications)
|
||||||
|
self.hasMore = response.hasMore
|
||||||
|
offset += response.notifications.count
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = error.localizedDescription
|
||||||
|
print("[watchOS] loadMoreNotifications failed with error: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingMore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationView: View {
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@StateObject private var viewModel = NotificationViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if viewModel.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else if let errorMessage = viewModel.errorMessage {
|
||||||
|
VStack {
|
||||||
|
Text("Error")
|
||||||
|
.font(.headline)
|
||||||
|
Text(errorMessage)
|
||||||
|
.font(.caption)
|
||||||
|
Button("Retry") {
|
||||||
|
Task {
|
||||||
|
if let token = appState.token, let serverUrl = appState.serverUrl {
|
||||||
|
await viewModel.fetchNotifications(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
} else if viewModel.notifications.isEmpty {
|
||||||
|
Text("No notifications")
|
||||||
|
} else {
|
||||||
|
List {
|
||||||
|
ForEach(viewModel.notifications) { notification in
|
||||||
|
NavigationLink(destination: NotificationDetailView(notification: notification)) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Text(notification.title)
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
if notification.viewedAt == nil {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.blue)
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !notification.subtitle.isEmpty {
|
||||||
|
Text(notification.subtitle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
if notification.content.count > 100 {
|
||||||
|
Text(notification.content.prefix(100) + "...")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.lineLimit(2)
|
||||||
|
} else {
|
||||||
|
Text(notification.content)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
Text(notification.createdAt, style: .relative)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if viewModel.hasMore {
|
||||||
|
if viewModel.isLoadingMore {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
ProgressView()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button("Load More") {
|
||||||
|
Task {
|
||||||
|
if let token = appState.token, let serverUrl = appState.serverUrl {
|
||||||
|
await viewModel.loadMoreNotifications(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if appState.isReady, let token = appState.token, let serverUrl = appState.serverUrl {
|
||||||
|
Task.detached {
|
||||||
|
await viewModel.fetchNotifications(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Notifications")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationDetailView: View {
|
||||||
|
let notification: SnNotification
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text(notification.title)
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
if !notification.subtitle.isEmpty {
|
||||||
|
Text(notification.subtitle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(notification.content)
|
||||||
|
.font(.body)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text(notification.createdAt, style: .date)
|
||||||
|
Text("·")
|
||||||
|
Text(notification.createdAt, style: .time)
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
|
if notification.viewedAt == nil {
|
||||||
|
Text("Unread")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.navigationTitle("Notification")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
151
ios/WatchRunner Watch App/Views/PostViews.swift
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
//
|
||||||
|
// PostViews.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PostRowView: View {
|
||||||
|
let post: SnPost
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@StateObject private var imageLoader = ImageLoader() // Instantiate ImageLoader
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
if imageLoader.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
} else if let image = imageLoader.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
.clipShape(Circle())
|
||||||
|
} else if let errorMessage = imageLoader.errorMessage {
|
||||||
|
Text("Failed: \(errorMessage)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
} else {
|
||||||
|
// Placeholder if no image and not loading
|
||||||
|
Image(systemName: "person.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
Text(post.publisher.nick ?? post.publisher.name)
|
||||||
|
.font(.subheadline)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
.task(id: post.publisher.picture?.id) { // Use task(id:) to reload image when pictureId changes
|
||||||
|
if let serverUrl = appState.serverUrl, let pictureId = post.publisher.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token {
|
||||||
|
await imageLoader.loadImage(from: imageUrl, token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let title = post.title, !title.isEmpty {
|
||||||
|
Text(title)
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let content = post.content, !content.isEmpty {
|
||||||
|
Text(content)
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !post.attachments.isEmpty {
|
||||||
|
AttachmentView(attachment: post.attachments[0])
|
||||||
|
if post.attachments.count > 1 {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "paperclip.circle.fill")
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
.foregroundStyle(.gray)
|
||||||
|
Text("\(post.attachments.count - 1)+ attachments")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.padding(.vertical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PostDetailView: View {
|
||||||
|
let post: SnPost
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@StateObject private var publisherImageLoader = ImageLoader() // Instantiate ImageLoader for publisher avatar
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
if publisherImageLoader.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
} else if let image = publisherImageLoader.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.clipShape(Circle())
|
||||||
|
} else if let errorMessage = publisherImageLoader.errorMessage {
|
||||||
|
Text("Failed: \(errorMessage)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "person.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
Text("@\(post.publisher.name)")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
// Use task(id:) to reload image when pictureId changes
|
||||||
|
.task(id: post.publisher.picture?.id) {
|
||||||
|
if let serverUrl = appState.serverUrl, let pictureId = post.publisher.picture?.id, let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl), let token = appState.token {
|
||||||
|
await publisherImageLoader.loadImage(from: imageUrl, token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let title = post.title, !title.isEmpty {
|
||||||
|
Text(title)
|
||||||
|
.font(.title2)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let content = post.content, !content.isEmpty {
|
||||||
|
Text(content)
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !post.attachments.isEmpty {
|
||||||
|
Text("Attachments").font(.headline)
|
||||||
|
ForEach(post.attachments) { attachment in
|
||||||
|
AttachmentView(attachment: attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !post.tags.isEmpty {
|
||||||
|
Text("Tags").font(.headline)
|
||||||
|
FlowLayout(alignment: .leading, spacing: 4) {
|
||||||
|
ForEach(post.tags) { tag in
|
||||||
|
Text("#\(tag.name ?? tag.slug)")
|
||||||
|
.font(.caption)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(Capsule().fill(Color.accentColor.opacity(0.2)))
|
||||||
|
.cornerRadius(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.navigationTitle("Post")
|
||||||
|
}
|
||||||
|
}
|
||||||
132
ios/WatchRunner Watch App/Views/StatusCreationView.swift
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
//
|
||||||
|
// StatusCreationView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct StatusCreationView: View {
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
let initialStatus: SnAccountStatus?
|
||||||
|
|
||||||
|
@State private var attitude: Int
|
||||||
|
@State private var isInvisible: Bool
|
||||||
|
@State private var isNotDisturb: Bool
|
||||||
|
@State private var label: String
|
||||||
|
@State private var isSubmitting: Bool = false
|
||||||
|
@State private var error: Error? = nil
|
||||||
|
|
||||||
|
private let networkService = NetworkService()
|
||||||
|
|
||||||
|
init(initialStatus: SnAccountStatus? = nil) {
|
||||||
|
self.initialStatus = initialStatus
|
||||||
|
_attitude = State(initialValue: initialStatus?.attitude ?? 1)
|
||||||
|
_isInvisible = State(initialValue: initialStatus?.isInvisible ?? false)
|
||||||
|
_isNotDisturb = State(initialValue: initialStatus?.isNotDisturb ?? false)
|
||||||
|
_label = State(initialValue: initialStatus?.label ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
// Title
|
||||||
|
Text("Set Status")
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.top)
|
||||||
|
|
||||||
|
// Label TextField
|
||||||
|
TextField("Status label", text: $label)
|
||||||
|
.textFieldStyle(.automatic)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
// Attitude Picker
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Mood")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Picker("Attitude", selection: $attitude) {
|
||||||
|
Text("😊 Positive").tag(0)
|
||||||
|
Text("😐 Neutral").tag(1)
|
||||||
|
Text("😢 Negative").tag(2)
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(height: 80)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
// Toggles
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Toggle("Invisible", isOn: $isInvisible)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Toggle("Do Not Disturb", isOn: $isNotDisturb)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error message
|
||||||
|
if let error = error {
|
||||||
|
Text("Error: \(error.localizedDescription)")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.font(.caption)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.buttonStyle(.glass)
|
||||||
|
|
||||||
|
Button(isSubmitting ? "Saving..." : "Save") {
|
||||||
|
Task {
|
||||||
|
await submitStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.glassProminent)
|
||||||
|
.disabled(isSubmitting)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Status")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func submitStatus() async {
|
||||||
|
guard let token = appState.token, let serverUrl = appState.serverUrl else {
|
||||||
|
error = NSError(domain: "StatusCreationView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Authentication not available"])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting = true
|
||||||
|
error = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
_ = try await networkService.createOrUpdateStatus(
|
||||||
|
attitude: attitude,
|
||||||
|
isInvisible: isInvisible,
|
||||||
|
isNotDisturb: isNotDisturb,
|
||||||
|
label: label.isEmpty ? nil : label,
|
||||||
|
token: token,
|
||||||
|
serverUrl: serverUrl
|
||||||
|
)
|
||||||
|
dismiss()
|
||||||
|
} catch {
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
StatusCreationView()
|
||||||
|
.environmentObject(AppState())
|
||||||
|
}
|
||||||
12
ios/WatchRunner Watch App/Views/VideoPlayerView.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import AVKit
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
struct VideoPlayerView: View {
|
||||||
|
let videoUrl: URL
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VideoPlayer(player: AVPlayer(url: videoUrl))
|
||||||
|
.edgesIgnoringSafeArea(.all) // Make it full screen
|
||||||
|
}
|
||||||
|
}
|
||||||
17
ios/WatchRunner Watch App/WatchRunnerApp.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// WatchRunnerApp.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct WatchRunner_Watch_AppApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
ios/WatchRunner-Watch-App-Info.plist
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/wasm.dart';
|
import 'package:drift/wasm.dart';
|
||||||
import 'package:island/database/drift_db.dart';
|
import 'package:island/database/drift_db.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
AppDatabase constructDb() {
|
AppDatabase constructDb() {
|
||||||
return AppDatabase(connectOnWeb());
|
return AppDatabase(connectOnWeb());
|
||||||
@@ -9,12 +10,17 @@ AppDatabase constructDb() {
|
|||||||
DatabaseConnection connectOnWeb() {
|
DatabaseConnection connectOnWeb() {
|
||||||
return DatabaseConnection.delayed(
|
return DatabaseConnection.delayed(
|
||||||
Future(() async {
|
Future(() async {
|
||||||
|
try {
|
||||||
final result = await WasmDatabase.open(
|
final result = await WasmDatabase.open(
|
||||||
databaseName: 'solar_network_data',
|
databaseName: 'solar_network_data',
|
||||||
sqlite3Uri: Uri.parse('sqlite3.wasm'),
|
sqlite3Uri: Uri.parse('sqlite3.wasm'),
|
||||||
driftWorkerUri: Uri.parse('drift_worker.dart.js'),
|
driftWorkerUri: Uri.parse('drift_worker.dart.js'),
|
||||||
);
|
);
|
||||||
return result.resolvedExecutor;
|
return result.resolvedExecutor;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
talker.error('Failed to open WASM database...', e, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||