Compare commits
490 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
33ec0b1d9a
|
|||
|
f698385494
|
|||
|
6ecdf89d20
|
|||
|
683f686540
|
|||
|
6a115ab1cc
|
|||
|
d05283d3b1
|
|||
|
b9653e7264
|
|||
|
87d1c8b320
|
|||
|
22eb54b61f
|
|||
|
a90ad2debc
|
|||
|
2f00bf660d
|
|||
|
322a93324c
|
|||
|
c3a3be0807
|
|||
|
27c7c8f039
|
|||
|
a7960da362
|
|||
|
64ab30b0a9
|
|||
|
935e6d5833
|
|||
|
938b128b1e
|
|||
|
c9764daa20
|
|||
|
7bc44e8f06
|
|||
|
4a7ff96a8b
|
|||
|
e759d5f46c
|
|||
|
f5ca6a37bf
|
|||
|
5fc8859f3b
|
|||
|
e30e7adbe2
|
|||
|
68be4db160
|
|||
|
aa91e376ca
|
|||
|
caffb85588
|
|||
|
521b192205
|
|||
|
77ac0428ea
|
|||
|
88c8227c66
|
|||
|
b20d8350a8
|
|||
|
98b27bed0e
|
|||
|
3a7d8b1a0d
|
|||
|
b4801d6af6
|
|||
|
aab5b957af
|
|||
|
43d706a184
|
|||
|
98df275f88
|
|||
|
5663df6ef1
|
|||
|
e996a0c95f
|
|||
|
a090e93f57
|
|||
|
c69034c071
|
|||
|
369ea6cf5b
|
|||
|
2e371b5296
|
|||
|
2e9d61bcfa
|
|||
|
9c2b5b0dfa
|
|||
|
3b40f515b3
|
|||
|
5ee61dbef2
|
|||
|
b151ef6686
|
|||
|
ff934d0f08
|
|||
|
abe5ded896
|
|||
|
f1d72a5215
|
|||
|
864cbe73b7
|
|||
|
108a6da074
|
|||
|
f9a09599c9
|
|||
|
9067dadd3e
|
|||
|
09f8df1e78
|
|||
|
2c5f246c55
|
|||
|
a66c6ea654
|
|||
|
3ad4bb4518
|
|||
|
53f0dcb825
|
|||
|
557f5a2389
|
|||
|
78f14f890f
|
|||
|
77b2effb34
|
|||
|
f02b4abf65
|
|||
|
3f37c4f761
|
|||
|
5deb910fa4
|
|||
|
f50a19f573
|
|||
|
98c8a356e8
|
|||
|
d0c16ea08f
|
|||
|
f2c1b2a531
|
|||
|
3061f0c5a9
|
|||
|
98f7f33c65
|
|||
|
d9af5d32fd
|
|||
|
f2031697ec
|
|||
|
9b85b7573c
|
|||
|
4fb739b33b
|
|||
|
c03ba3bc3a
|
|||
|
fc65440420
|
|||
|
7b85533184
|
|||
|
77d9eb60c6
|
|||
|
4d8953cd22
|
|||
|
fafa460fe8
|
|||
|
faf3a677d4
|
|||
|
0f644a0234
|
|||
|
18d16fdd57
|
|||
|
18e890d63c
|
|||
|
9c5e50c16a
|
|||
|
96a2c8182e
|
|||
|
56b27c3e82
|
|||
|
ad4bf94195
|
|||
|
b77a832d8a
|
|||
|
5e61805db7
|
|||
|
35b96b0bd2
|
|||
|
c8ad791ff3
|
|||
|
1e908502dc
|
|||
|
715ce1a368
|
|||
|
548c9963ee
|
|||
|
db5199438a
|
|||
|
4409a6fb1e
|
|||
|
26a24b0e41
|
|||
|
9b948d259b
|
|||
|
1f713b5b2b
|
|||
|
f92cfafda4
|
|||
|
fa208b44d7
|
|||
|
94adecafbb
|
|||
|
0303ef4a93
|
|||
|
c2b18ce10b
|
|||
|
0767bb53ce
|
|||
|
b233f9a410
|
|||
|
256024fb46
|
|||
|
4a80aaf24d
|
|||
|
aafd160c44
|
|||
|
4a800725e3
|
|||
|
24791b3293
|
|||
|
3ac263d483
|
|||
|
2445d8adf8
|
|||
|
d4f95bbbf4
|
|||
|
943e4b7b5c
|
|||
|
7edc02a1d3
|
|||
|
3f9881e943
|
|||
|
50c25e919c
|
|||
|
99fb08dd55
|
|||
|
e43bc6b8a8
|
|||
|
c247cdf81c
|
|||
|
3ffa730505
|
|||
|
1cc34d3073
|
|||
|
96a919cc4e
|
|||
|
e7e3bfcadf
|
|||
|
a8617a5040
|
|||
|
d94f8d004f
|
|||
|
d93b066979
|
|||
|
320664a547
|
|||
|
98f4698d5b
|
|||
|
82397dd087
|
|||
|
4ec10ceb47
|
|||
|
4b03b45a0d
|
|||
|
7a72d32649
|
|||
|
5152dd13ea
|
|||
|
fd377aa7af
|
|||
|
67044148f1
|
|||
|
92bc43e4df
|
|||
|
a1a7b34c86
|
|||
|
40c0e052cf
|
|||
|
9a75228e38
|
|||
|
a9fd75cc45
|
|||
|
a713b30d93
|
|||
|
e516f0a862
|
|||
|
429b966c4b
|
|||
|
f14da0d3a2
|
|||
|
d201182bd2
|
|||
|
6f6422c15e
|
|||
|
9f6ae639ee
|
|||
|
35f4d7d885
|
|||
|
a9c8f49797
|
|||
|
5e9341a19c
|
|||
|
645a6dca93
|
|||
|
ea8e7ead2d
|
|||
|
5f2f083d72
|
|||
|
5cf40e27de
|
|||
|
1ab7295918
|
|||
|
07f191171c
|
|||
|
4a5dac248e
|
|||
|
3b983a6444
|
|||
|
4607b77355
|
|||
|
7957e4894a
|
|||
|
f94f80c375
|
|||
|
74fa2215a6
|
|||
|
0d11435feb
|
|||
|
e22598b0a6
|
|||
|
84cfe643f5
|
|||
|
05ac04e9a2
|
|||
|
66f283d6e8
|
|||
|
c779c7523c
|
|||
|
ac7cb29afe
|
|||
|
935aa77223
|
|||
|
24e5b3b824
|
|||
|
0391893b32
|
|||
|
b8d24876c8
|
|||
|
0493661f9a
|
|||
|
b40afde00f
|
|||
|
78a4022531
|
|||
|
8a291c80b7
|
|||
|
1395d65b76
|
|||
|
eb4942e0ed
|
|||
|
f254cfa81e
|
|||
|
4927795260
|
|||
|
e4019dadc8
|
|||
|
5e7d77e1a1
|
|||
|
bfcbed035c
|
|||
|
5ebefae961
|
|||
|
d4758674bb
|
|||
|
f5f1ddc0ea
|
|||
|
2720b59485
|
|||
|
29b1ac7fce
|
|||
|
83ca5551ad
|
|||
| 611cb024a9 | |||
|
74fb56891d
|
|||
|
ac4fa5eb85
|
|||
|
8857718709
|
|||
|
dd17b2b9c1
|
|||
|
848439f664
|
|||
|
f83117424d
|
|||
|
8c19c32c76
|
|||
|
d62b2bed80
|
|||
|
5a23eb1768
|
|||
|
5f6e4763d3
|
|||
|
580c36fb89
|
|||
|
6c25af3b30
|
|||
|
a1da72d447
|
|||
|
ab4120cc22
|
|||
|
52eff0fa25
|
|||
|
beeb28abf2
|
|||
|
c0ab3837ac
|
|||
|
59d38c0d8d
|
|||
|
bd2247ce86
|
|||
|
da2d3f7f17
|
|||
|
7497b77384
|
|||
|
f542d9fa97
|
|||
|
e70439870e
|
|||
|
d764b042fe
|
|||
|
a76b97d1d2
|
|||
|
cfbe6e580b
|
|||
|
f08b9e057f
|
|||
|
0509f37c96
|
|||
|
a7dc9ac6fa
|
|||
|
caf2f5f1f6
|
|||
|
12b79af3a2
|
|||
|
88f149584e
|
|||
|
877001b802
|
|||
| fec28f6223 | |||
| 85005ff9c3 | |||
| e3c92a3c55 | |||
| 9e9fbc5d6a | |||
| 8d1d836b52 | |||
| bc60ce5d42 | |||
| c093123e3a | |||
| 3de73538c7 | |||
| ba8d5cee09 | |||
|
5ee2e70442
|
|||
|
53a3a32907
|
|||
|
9a628779d9
|
|||
|
b60bd63d0c
|
|||
|
01cc71fd47
|
|||
|
a2b0cd0b6a
|
|||
|
7f971bcee3
|
|||
|
7de98a1731
|
|||
|
b52eb95b14
|
|||
|
b3ef7d6ad0
|
|||
|
d28c11940d
|
|||
|
504322c2dd
|
|||
|
a07ec3ca36
|
|||
| d96691e920 | |||
|
6273b2d917
|
|||
|
ab90d244b5
|
|||
|
dc6af6d9e5
|
|||
|
0ca801d963
|
|||
|
3edcdd72af
|
|||
|
402bb3fe04
|
|||
|
8ba55eb1be
|
|||
|
983ae2a1fc
|
|||
|
6fc94001b3
|
|||
|
44dbcfdc94
|
|||
|
b57caf56db
|
|||
|
dbcd1b6d36
|
|||
|
a8055de910
|
|||
|
49b15e7674
|
|||
|
e2369c40db
|
|||
|
44c5d91620
|
|||
|
7a5a2407b7
|
|||
|
234434f102
|
|||
|
9c3b228d02
|
|||
|
82682cae9a
|
|||
|
fcbd5fe680
|
|||
|
ad91b17af7
|
|||
|
24fa637329
|
|||
|
926ae5402f
|
|||
|
1a37d384e6
|
|||
|
d4cf598f69
|
|||
|
0106c08891
|
|||
|
9697def808
|
|||
|
6572875229
|
|||
|
66590b9079
|
|||
|
08b9604b55
|
|||
|
0602bbd277
|
|||
|
76e7ba7898
|
|||
|
6e6616b236
|
|||
|
071d51b25e
|
|||
|
a958362461
|
|||
|
6749bb00fe
|
|||
|
11fb20c673
|
|||
|
a7990f83db
|
|||
|
5f4cdf7937
|
|||
|
3330ca14dd
|
|||
|
1719b1c8fe
|
|||
|
3c2c51bfaf
|
|||
|
239d6750ff
|
|||
|
8b0c91977a
|
|||
|
f74cca8464
|
|||
|
08091d51bf
|
|||
|
481190811b
|
|||
|
4b32b65d1c
|
|||
|
50ac7109bb
|
|||
|
62da279c71
|
|||
|
fde6dbf891
|
|||
|
613bf4fb42
|
|||
|
00ae586016
|
|||
|
ea0d132dce
|
|||
|
aa2df1e847
|
|||
|
50672795f3
|
|||
|
383de9568d
|
|||
|
01fa228e45
|
|||
|
1e71ad33a6
|
|||
|
92c0260ecd
|
|||
|
0a161ad255
|
|||
|
c003f27b9a
|
|||
|
19db8309c4
|
|||
|
aa72ce08e8
|
|||
|
4639b00b86
|
|||
|
cc5460ea55
|
|||
|
eafac811e6
|
|||
|
e3be691596
|
|||
|
aa180a1358
|
|||
|
c2707b8af1
|
|||
|
62fd0500f3
|
|||
|
eeae865cc8
|
|||
|
cdf1413fe0
|
|||
|
327b4c04f1
|
|||
|
bd903ce29c
|
|||
|
1b8ecb15ce
|
|||
|
d4e380a97a
|
|||
|
126048b4fa
|
|||
|
8bec18813d
|
|||
|
1ae81794b1
|
|||
|
2a7d12de48
|
|||
|
64c60ead48
|
|||
| 001549b190 | |||
| 4595865ad3 | |||
|
|
1834643167 | ||
|
|
0e816eaa3e | ||
|
|
7c1f24b824 | ||
|
c6594ea2ce
|
|||
|
3bec6e683e
|
|||
|
83e92e2eed
|
|||
|
|
b7d44d96ba | ||
|
a83b929d42
|
|||
|
9423affa75
|
|||
|
cda23db609
|
|||
|
61074bc5a3
|
|||
|
5feafa9255
|
|||
|
e604577c1f
|
|||
|
af0ddd1273
|
|||
|
8a6bb34808
|
|||
|
4ef8445c77
|
|||
|
ec39ad6ca3
|
|||
|
eabb3154f1
|
|||
|
910bf20eef
|
|||
|
5efa9b2ae8
|
|||
|
dd3e39e891
|
|||
|
b6896ded23
|
|||
|
f28a73ff9c
|
|||
|
a014b64235
|
|||
|
7e0e7c20d7
|
|||
|
389fa515ba
|
|||
|
681ead02eb
|
|||
|
8d1c145b0b
|
|||
|
51b4754182
|
|||
|
8a2b321701
|
|||
|
f685a7a249
|
|||
|
76009147e9
|
|||
|
ce12f28e56
|
|||
|
3604373a1e
|
|||
|
9704a4c2c7
|
|||
|
67def56ad1
|
|||
|
1be33916af
|
|||
|
e8ff1bfd22
|
|||
|
3ae56f3d89
|
|||
|
707143e998
|
|||
|
1fd34eb2a3
|
|||
|
d7ca41e946
|
|||
|
ad9fb0719a
|
|||
|
e2d315afd4
|
|||
|
6124dbfd79
|
|||
|
5327f04ec0
|
|||
|
41c56a2319
|
|||
|
f9d033542e
|
|||
|
91784e65e6
|
|||
|
9d39c6a825
|
|||
|
537e49f1a4
|
|||
|
75bbd4df71
|
|||
|
6ef4580d93
|
|||
|
6ffd498761
|
|||
|
27157e7cc1
|
|||
|
bbb07d574a
|
|||
|
c660a419e2
|
|||
|
c3f61467c8
|
|||
|
9bc47df452
|
|||
|
9ef8ca4d45
|
|||
|
b55cbd08d1
|
|||
|
8c6bd0feaa
|
|||
|
7dd4b20628
|
|||
|
fec0cb7640
|
|||
|
75deb04a2b
|
|||
|
7c7ed21a96
|
|||
|
a201f20793
|
|||
|
598c51bc1a
|
|||
|
e1ea61c5f1
|
|||
|
ac424bde36
|
|||
|
b43b70df3f
|
|||
|
4321aa621a
|
|||
|
d5d275fb43
|
|||
|
6bb3307144
|
|||
|
391604d4a2
|
|||
|
1d9361c12f
|
|||
|
a129b9cdd0
|
|||
|
3bf815ac61
|
|||
|
77bae4d6fd
|
|||
|
0a301c4c9b
|
|||
|
27b390a51c
|
|||
|
018386d14e
|
|||
|
3825d7c6c7
|
|||
|
bf930291e4
|
|||
|
a8c4988790
|
|||
|
28dd204b1a
|
|||
|
3cbc1a59a7
|
|||
|
277e9ae3d1
|
|||
|
27b3ca25b7
|
|||
|
f871cd3b62
|
|||
|
a8a59ee30c
|
|||
|
2cd1416a13
|
|||
|
6be7dfbc61
|
|||
|
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
|
|||
|
a2cc55696f
|
|||
|
e79f857feb
|
|||
|
affba29c04
|
|||
|
756746b144
|
@@ -14,13 +14,13 @@ The backend of the Solar Network is written in Go and is a microservices app. Th
|
||||
|
||||
## 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
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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!
|
||||
|
||||
|
||||
@@ -62,3 +62,9 @@ If you want to build the release version, use the flutter build command. Learn m
|
||||
```bash
|
||||
flutter build <platform>
|
||||
```
|
||||
|
||||
### Known Issues
|
||||
|
||||
Due to the issues with the flutter build tools, [see](https://github.com/flutter/flutter/issues/160622).
|
||||
|
||||
Since there is a watchOS app for iOS, you're unable to use the flutter cli to run iOS app. Use xcode instead.
|
||||
@@ -75,3 +75,4 @@ dependencies {
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,16 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- App protocol -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
||||
<data android:scheme="solian" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deeplinking -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -51,6 +61,12 @@
|
||||
<data android:scheme="http" android:host="solian.app" />
|
||||
<data android:scheme="https" />
|
||||
</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 -->
|
||||
<intent-filter>
|
||||
|
||||
1478
assets/i18n/es-ES.json
Normal file
1478
assets/i18n/ja-JP.json
Normal file
1478
assets/i18n/ko-KR.json
Normal file
@@ -122,9 +122,6 @@
|
||||
"addVideo": "添加视频",
|
||||
"addPhoto": "添加照片",
|
||||
"addFile": "添加文件",
|
||||
"uploadFile": "上传文件",
|
||||
"settingsDefaultPool": "选择文件池",
|
||||
"settingsDefaultPoolHelper": "为文件上传选择一个默认池",
|
||||
"createDirectMessage": "创建新私人消息",
|
||||
"gotoDirectMessage": "前往私信",
|
||||
"react": "反应",
|
||||
@@ -166,10 +163,7 @@
|
||||
"checkInResultLevel2": "中平",
|
||||
"checkInResultLevel3": "吉",
|
||||
"checkInResultLevel4": "大吉",
|
||||
"checkInResultLevel5": "生日快乐 🥳",
|
||||
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
||||
"eventCalander": "活动日历",
|
||||
"eventCalanderEmpty": "该日无活动。",
|
||||
"fortuneGraph": "时运趋势",
|
||||
"noFortuneData": "本月沒有时运數據。",
|
||||
"creatorHub": "创作者中心",
|
||||
@@ -283,18 +277,6 @@
|
||||
"levelingProgress": "等级进度",
|
||||
"levelingProgressExperience": "{} 经验值",
|
||||
"levelingProgressLevel": "等级 {}",
|
||||
"levelingStage1": "新手",
|
||||
"levelingStage2": "学徒",
|
||||
"levelingStage3": "熟练工",
|
||||
"levelingStage4": "行家",
|
||||
"levelingStage5": "专家",
|
||||
"levelingStage6": "大师",
|
||||
"levelingStage7": "宗师",
|
||||
"levelingStage8": "传奇",
|
||||
"levelingStage9": "神话",
|
||||
"levelingStage10": "不朽",
|
||||
"levelingStage11": "神圣",
|
||||
"levelingStage12": "超凡",
|
||||
"fileUploadingProgress": "正在上传文件 #{}: {}%",
|
||||
"removeChatMember": "移除聊天室成员",
|
||||
"removeChatMemberHint": "确定要将此成员从聊天室中移除吗?",
|
||||
@@ -316,11 +298,9 @@
|
||||
"walletCreate": "创建钱包",
|
||||
"settingsServerUrl": "服务器 URL",
|
||||
"settingsApplied": "设置已应用。",
|
||||
"settingsCustomFontsHelper": "用逗号分隔。",
|
||||
"notifications": "通知",
|
||||
"posts": "帖子",
|
||||
"settingsBackgroundImage": "背景图片",
|
||||
"settingsBackgroundImageEnable": "显示背景图片",
|
||||
"settingsBackgroundImageClear": "清除背景图片",
|
||||
"settingsBackgroundGenerateColor": "从背景图像生成主题色",
|
||||
"messageNone": "没有内容可显示",
|
||||
@@ -331,8 +311,6 @@
|
||||
"chatBreakNone": "无",
|
||||
"settingsRealmCompactView": "紧凑领域视图",
|
||||
"settingsMixedFeed": "混合动态",
|
||||
"settingsDataSavingMode": "流量节省模式",
|
||||
"dataSavingHint": "流量节省模式",
|
||||
"settingsAutoTranslate": "自动翻译",
|
||||
"settingsHideBottomNav": "隐藏底部导航",
|
||||
"settingsSoundEffects": "音效",
|
||||
@@ -688,7 +666,6 @@
|
||||
"publisherFeatureDevelopDescription": "为你的开发者解锁包括应用套件,API 及更多开发功能。",
|
||||
"publisherFeatureDevelopHint": "目前该功能还在开发中,你需要邀请才可解锁。",
|
||||
"learnMore": "了解更多",
|
||||
"discoverWebArticles": "来自站外的文章",
|
||||
"webArticlesStand": "文章亭",
|
||||
"about": "关于",
|
||||
"somethingWentWrong": "发生了一些错误",
|
||||
@@ -711,8 +688,6 @@
|
||||
"sharePostPhoto": "通过图片分享帖子",
|
||||
"wouldYouLikeToNavigateToChat": "你想要前往聊天页面吗?",
|
||||
"abuseReports": "举报",
|
||||
"discoverRealms": "发现领域",
|
||||
"discoverPublishers": "发现发布者",
|
||||
"membershipCancel": "取消会员订阅",
|
||||
"membershipCancelConfirm": "你确定要取消会员订阅吗?",
|
||||
"membershipCancelHint": "你确定要取消会员订阅吗?你将不会再次被扣费。你的会员资格将在当前计费周期结束前保持有效。并且你将无法重新订阅,直到当前订阅结束。",
|
||||
@@ -775,21 +750,6 @@
|
||||
"rename": "重命名",
|
||||
"markAsSensitive": "标记为敏感",
|
||||
"fileName": "文件名",
|
||||
"sensitiveCategories": {
|
||||
"language": "语言",
|
||||
"sexualContent": "色情内容",
|
||||
"violence": "暴力",
|
||||
"profanity": "亵渎",
|
||||
"hateSpeech": "仇恨言论",
|
||||
"racism": "种族主义",
|
||||
"adultContent": "成人内容",
|
||||
"drugAbuse": "药物滥用",
|
||||
"alcoholAbuse": "酗酒",
|
||||
"gambling": "赌博",
|
||||
"selfHarm": "自残",
|
||||
"childAbuse": "虐待儿童",
|
||||
"other": "其他"
|
||||
},
|
||||
"poll": "投票",
|
||||
"pollsRecent": "最近投票",
|
||||
"pollCreateNew": "创建新投票",
|
||||
@@ -832,12 +792,100 @@
|
||||
"one": "+{} 个文件被折叠",
|
||||
"other": "+{} 个文件被折叠"
|
||||
},
|
||||
"pollQuestions": "问题",
|
||||
"pollAnswerSubmitted": "调查问卷已提交。",
|
||||
"modifyAnswers": "修改问卷",
|
||||
"back": "返回",
|
||||
"submit": "提交",
|
||||
"pollOptionDefaultLabel": "选项 1",
|
||||
"pollUpdated": "投票已更新。",
|
||||
"pollCreated": "投票已创建。",
|
||||
"pollCreate": "创建投票",
|
||||
"pollEdit": "编辑投票",
|
||||
"pollPreviewJsonDebug": "调试预览",
|
||||
"pollTitleRequired": "标题不可为空",
|
||||
"pollEndDateOptional": "结束日期和时间 (可选)",
|
||||
"notSet": "未设置",
|
||||
"pick": "选择",
|
||||
"clear": "清除",
|
||||
"questions": "问题",
|
||||
"pollAddQuestion": "新增问题",
|
||||
"pollQuestionTypeSingleChoice": "单选",
|
||||
"pollQuestionTypeMultipleChoice": "多选",
|
||||
"pollQuestionTypeFreeText": "自由文本",
|
||||
"pollQuestionTypeYesNo": "是/否",
|
||||
"pollQuestionTypeRating": "评价",
|
||||
"pollNoQuestionsYet": "没有问题",
|
||||
"pollNoQuestionsHint": "点击「添加问题」开始建立您的问卷调查。",
|
||||
"pollDebugPreview": "调试预览",
|
||||
"pollUntitledQuestion": "无标题的问题",
|
||||
"moveUp": "上移",
|
||||
"moveDown": "下移",
|
||||
"required": "必填项",
|
||||
"pollQuestionTitle": "题目",
|
||||
"pollQuestionTitleRequired": "题目是必填项",
|
||||
"pollQuestionDescriptionOptional": "题目描述(可选)",
|
||||
"options": "选项",
|
||||
"pollAddOption": "添加选项",
|
||||
"pollOptionLabel": "选项标签",
|
||||
"pollLongTextAnswerPreview": "长文本答案 (预览)",
|
||||
"pollShortTextAnswerPreview": "短文本答案 (预览)",
|
||||
"award": "赞赏",
|
||||
"awardPost": "赞赏帖子",
|
||||
"awardMessage": "消息",
|
||||
"awardMessageHint": "输入您的赞赏信息……",
|
||||
"awardAttitude": "态度",
|
||||
"awardAttitudePositive": "积极",
|
||||
"awardAttitudeNegative": "消极的",
|
||||
"awardAmount": "金额",
|
||||
"awardAmountHint": "输入金额……",
|
||||
"awardAmountRequired": "「金额」为必填字段",
|
||||
"awardAmountInvalid": "请输入有效金额",
|
||||
"awardMessageTooLong": "消息太长(最多4096个字符)",
|
||||
"awardSuccess": "奖励已成功发送!",
|
||||
"awardSubmit": "赞赏",
|
||||
"awardPostPreview": "帖子预览",
|
||||
"awardNoContent": "暂无内容",
|
||||
"awardByPublisher": "由 {} 发表",
|
||||
"awardBenefits": "赞赏福利",
|
||||
"awardBenefitsDescription": "为该帖子授予奖励可以提升其价值和曝光度。价值更高的帖子更有可能在社区中被推荐和突出显示。",
|
||||
"checkInResultLevel5": "生日快乐 🥳",
|
||||
"region": "区域",
|
||||
"accountRegionHint": "该地区将用于内容交付和本地化。",
|
||||
"settingsCustomFontsHelper": "用逗号分隔。",
|
||||
"settingsBackgroundImageEnable": "显示背景图片",
|
||||
"settingsDataSavingMode": "流量节省模式",
|
||||
"dataSavingHint": "流量节省模式",
|
||||
"postTypePost": "帖子",
|
||||
"searchDrafts": "搜索草稿……",
|
||||
"noSearchResults": "无搜索结果",
|
||||
"contactMethodMakePublic": "设为公开",
|
||||
"contactMethodMakePrivate": "设为仅自己可见",
|
||||
"contactMethodPublic": "公开",
|
||||
"contactMethodPrivate": "私密",
|
||||
"discoverRealms": "发现领域",
|
||||
"discoverPublishers": "发现发布者",
|
||||
"discoverShuffledPost": "随机的帖子",
|
||||
"projects": "项目",
|
||||
"noProjects": "未找到项目。",
|
||||
"deleteProject": "删除项目",
|
||||
"deleteProjectHint": "确定要删除此项目吗?此操作无法撤销。",
|
||||
"createProject": "新建项目",
|
||||
"editProject": "编辑项目",
|
||||
"projectDetails": "项目详情",
|
||||
"createBot": "创建机器人",
|
||||
"bots": "机器人",
|
||||
"noBots": "暂无机器人。",
|
||||
"deleteBotHint": "您确定要删除此机器人吗?此操作无法撤消。",
|
||||
"deleteBot": "删除机器人",
|
||||
"discoverWebArticles": "来自站外的文章",
|
||||
"messageJumpNotLoaded": "引用的消息没有被加载,无法跳转。",
|
||||
"postUnlinkRealm": "不关联领域",
|
||||
"postSlug": "别名",
|
||||
"postSlugHint": "这个别名可以用于在网页通过 URL 浏览到你的帖子,它应该在同一发布者中是唯一。",
|
||||
"attachmentOnDevice": "离线",
|
||||
"attachmentOnCloud": "在线",
|
||||
"attachments": "附件",
|
||||
"publisherCollabInvitation": "协作邀请",
|
||||
"publisherCollabInvitationCount": {
|
||||
"zero": "无邀请",
|
||||
@@ -845,24 +893,58 @@
|
||||
"other": "{} 个可用邀请"
|
||||
},
|
||||
"failedToLoadUserInfo": "加载用户信息失败",
|
||||
"failedToLoadUserInfoNetwork": "这看起来是个网络问题,你可以按下面的按钮来重试",
|
||||
"failedToLoadUserInfoNetwork": "这看起来是个网络问题,你可以按下面的按钮来重试。",
|
||||
"failedToLoadUserInfoUnauthorized": "看来您的会话已被注销或不再可用,如果您愿意,您仍然可以再次尝试获取用户信息。",
|
||||
"okay": "了解",
|
||||
"postDetail": "帖子详情",
|
||||
"postCount": {
|
||||
"zero": "没有帖子",
|
||||
"one": "{} 帖子",
|
||||
"other": "{} 帖子"
|
||||
},
|
||||
"mimeType": "类型",
|
||||
"fileSize": "大小",
|
||||
"fileHash": "哈希",
|
||||
"exifData": "EXIF 数据",
|
||||
"postShuffle": "随机帖子",
|
||||
"leveling": "等级",
|
||||
"levelingHistory": "经验记录",
|
||||
"stellarProgram": "恒星计划",
|
||||
"socialCredits": "社会信用点",
|
||||
"credits": "信用",
|
||||
"creditsStatus": "积分状态",
|
||||
"socialCreditsDescription": "社会信用是 Solar Network 评价用户的一种方式。它基于用户的行为和互动来计算。以 100 分为基准,分数越高表示用户在社区中的信誉越好。分数会随着时间的推移而变化,反映用户的最新行为。信用等级高的用户可以享受到更多的福利,反之的用户部份功能可能受到限制。",
|
||||
"socialCreditsLevelPoor": "糟糕",
|
||||
"socialCreditsLevelNormal": "正常",
|
||||
"socialCreditsLevelGood": "良好",
|
||||
"socialCreditsLevelExcellent": "优秀",
|
||||
"orderByPopularity": "按热度排序",
|
||||
"orderByReleaseDate": "按发布日期排序",
|
||||
"editBot": "编辑机器人",
|
||||
"botAutomatedBy": "由 {} 自动化",
|
||||
"botDetails": "机器人详情",
|
||||
"overview": "总览",
|
||||
"keys": "密钥",
|
||||
"botNotFound": "机器人未找到。",
|
||||
"newBotKey": "新建密钥",
|
||||
"newBotKeyHint": "输入新密钥的名称,密钥只会显示一次。",
|
||||
"revokeBotKey": "撤销密钥",
|
||||
"revokeBotKeyHint": "你确定要撤销这个密钥?这个操作无法撤回,所有使用该密钥的应用程式会停止工作。",
|
||||
"noBotKeys": "还没有密钥。",
|
||||
"revoke": "撤销",
|
||||
"keyName": "密钥名称",
|
||||
"newKeyGenerated": "新密钥已生成",
|
||||
"copyKeyHint": "请安全的保存该密钥,你不会再次看到它。",
|
||||
"rotateKey": "旋转密钥",
|
||||
"rotateBotKey": "旋转密钥",
|
||||
"rotateBotKeyHint": "你确认要旋转这个密钥?久的密钥会立即失效,该操作无法撤销。",
|
||||
"webFeedArticleCount": {
|
||||
"zero": "没有文章",
|
||||
"one": "{} 篇文章",
|
||||
"other": "{} 篇文章"
|
||||
},
|
||||
"webFeedSubscribed": "你已经订阅了这个源",
|
||||
"webFeedUnsubscribed": "你已经取消订阅这个源",
|
||||
"appDetails": "应用详情",
|
||||
"secrets": "密钥",
|
||||
"appNotFound": "应用未找到。",
|
||||
@@ -875,9 +957,537 @@
|
||||
"copySecretHint": "请复制此密钥并将其存放在安全的地方。您将无法再次看到它。",
|
||||
"expiresIn": "过期时间(秒)",
|
||||
"isOidc": "OIDC 兼容",
|
||||
"pinPost": "置顶帖子",
|
||||
"unpinPost": "取消置顶",
|
||||
"pinnedPost": "已置顶",
|
||||
"publisherPage": "发布者页面",
|
||||
"realmPage": "领域页面",
|
||||
"replyPage": "回复页面",
|
||||
"pinPostPublisherHint": "将此帖子置顶于发布者页面",
|
||||
"pinPostRealmHint": "将此帖子置顶于领域页面",
|
||||
"pinPostRealmDisabledHint": "这个帖子不属于任何领域",
|
||||
"pinPostReplyHint": "将此帖子置顶于回复页面",
|
||||
"pinPostReplyDisabledHint": "这个帖子不是回复",
|
||||
"pin": "置顶",
|
||||
"unpinPostHint": "你确定要取消置顶该帖子吗?",
|
||||
"all": "全部",
|
||||
"statusPresent": "至今",
|
||||
"accountAutomated": "机器人",
|
||||
"chatBreakClearButton": "清除",
|
||||
"chatBreak5m": "5 分钟",
|
||||
"chatBreak10m": "10 分钟",
|
||||
"chatBreak15m": "15 分钟",
|
||||
"chatBreak30m": "30 分钟",
|
||||
"chatBreakCustomMinutes": "自定义(分钟)",
|
||||
"errorGeneric": "错误: {}",
|
||||
"searchMessages": "搜索消息",
|
||||
"messagesCount": "{} 消息",
|
||||
"dotSeparator": "·",
|
||||
"roleValidationHint": "成员角色必须设置在0到100之间",
|
||||
"searchMessagesHint": "搜索消息……",
|
||||
"searchLinks": "链接",
|
||||
"searchAttachments": "附件",
|
||||
"noMessagesFound": "未找到消息",
|
||||
"openInBrowser": "在浏览器中打开",
|
||||
"highlightPost": "精选帖子",
|
||||
"notableDayNext": "距离 {} 还有"
|
||||
"filters": "过滤器",
|
||||
"apply": "申请",
|
||||
"pubName": "题目名称",
|
||||
"realm": "领域",
|
||||
"shuffle": "随机",
|
||||
"pinned": "已置顶",
|
||||
"noResultsFound": "未找到结果",
|
||||
"toggleFilters": "切换过滤器",
|
||||
"notableDayNext": "距离 {} 还有",
|
||||
"expandPoll": "展开投票",
|
||||
"collapsePoll": "折叠投票",
|
||||
"embedView": "嵌入视图",
|
||||
"embedUri": "嵌入的 URI",
|
||||
"aspectRatio": "长宽比",
|
||||
"renderer": "渲染器",
|
||||
"addEmbed": "添加嵌入",
|
||||
"editEmbed": "编辑嵌入",
|
||||
"deleteEmbed": "删除嵌入",
|
||||
"deleteEmbedConfirm": "您确定要删除此嵌入吗?",
|
||||
"currentEmbed": "当前嵌入",
|
||||
"noEmbed": "尚未嵌入",
|
||||
"save": "保存",
|
||||
"webView": "Web 视图",
|
||||
"settingsDefaultPool": "选择文件池",
|
||||
"settingsDefaultPoolHelper": "为文件上传选择一个默认池",
|
||||
"uploadFile": "上传文件",
|
||||
"authDeviceChallenges": "设备活动",
|
||||
"authDeviceHint": "向左轻扫以编辑标签,向右轻扫以注销登录设备。",
|
||||
"settingsMessageDisplayStyle": "消息样式",
|
||||
"auto": "自动",
|
||||
"manual": "手动",
|
||||
"iframeCode": "Iframe代码",
|
||||
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||
"parseIframe": "解析Iframe",
|
||||
"messageActions": "消息选项",
|
||||
"viewEmbedLoadHint": "点击以加载",
|
||||
"levelingStage1": "新手",
|
||||
"levelingStage2": "学徒",
|
||||
"levelingStage3": "熟练工",
|
||||
"levelingStage4": "行家",
|
||||
"levelingStage5": "专家",
|
||||
"levelingStage6": "大师",
|
||||
"levelingStage7": "宗师",
|
||||
"levelingStage8": "传奇",
|
||||
"levelingStage9": "神话",
|
||||
"levelingStage10": "不朽",
|
||||
"levelingStage11": "神圣",
|
||||
"levelingStage12": "超凡",
|
||||
"uploadAttachment": "上传附件",
|
||||
"attachmentPreview": "附件预览",
|
||||
"selectPool": "选择储存池",
|
||||
"choosePool": "选择一个储存池",
|
||||
"errorLoadingPools": "加载池时出错",
|
||||
"quotaCostInfo": "此上传将消耗 {} 配额点",
|
||||
"uploadConstraints": "上传限制",
|
||||
"fileSizeExceeded": "文件大小超过了 {} 的最大限制",
|
||||
"fileTypeNotAccepted": "此储存池不接受该文件类型",
|
||||
"files": "附件",
|
||||
"confirmDeleteFile": "你确定要删除此文件吗?",
|
||||
"deleteFile": "删除文件",
|
||||
"failedToDeleteFile": "删除文件失败",
|
||||
"drive": "云盘",
|
||||
"allPools": "所有池",
|
||||
"includeRecycled": "包括已回收的文件",
|
||||
"confirmDeleteRecycledFiles": "你确定要删除所有被回收的文件吗?",
|
||||
"deleteRecycledFiles": "删除被回收的文件",
|
||||
"recycledFilesDeleted": "被回收文件成功删除",
|
||||
"failedToDeleteRecycledFiles": "删除被回收文件失败",
|
||||
"upload": "上传",
|
||||
"deleteMessage": "删除消息",
|
||||
"deleteMessageConfirmation": "您确定要删除这条消息吗?",
|
||||
"customReaction": "自定义反应",
|
||||
"customReactions": "自定义反应",
|
||||
"stickerPlaceholder": "贴图占位符",
|
||||
"reactionAttitude": "反应属性",
|
||||
"addReaction": "添加反应",
|
||||
"eventCalendar": "活动日历",
|
||||
"eventCalendarEmpty": "该日无活动。",
|
||||
"walletStats": "钱包统计",
|
||||
"totalTransactions": "交易总额",
|
||||
"totalOrders": "总订单",
|
||||
"totalIncome": "总收入",
|
||||
"totalOutgoing": "总支出",
|
||||
"netBalance": "净余额",
|
||||
"messageUpdateLinks": "服务器生成的链接预览",
|
||||
"messageUpdateEdited": "编辑一条消息",
|
||||
"settingsCardBackgroundOpacity": "卡片背景不透明度",
|
||||
"settingsThemeMode": "主题模式",
|
||||
"settingsThemeModeSystem": "跟随系统",
|
||||
"settingsThemeModeLight": "亮色",
|
||||
"settingsThemeModeDark": "暗色",
|
||||
"enterPin": "请输入您的PIN码",
|
||||
"chatReplyingTo": "回复给 {}",
|
||||
"chatForwarding": "正在转发消息",
|
||||
"chatEditing": "编辑消息",
|
||||
"chatNoContent": "没有内容",
|
||||
"sensitiveCategories": {
|
||||
"language": "语言",
|
||||
"sexualContent": "色情内容",
|
||||
"violence": "暴力",
|
||||
"profanity": "亵渎",
|
||||
"hateSpeech": "仇恨言论",
|
||||
"racism": "种族主义",
|
||||
"adultContent": "成人内容",
|
||||
"drugAbuse": "药物滥用",
|
||||
"alcoholAbuse": "酗酒",
|
||||
"gambling": "赌博",
|
||||
"selfHarm": "自残",
|
||||
"childAbuse": "虐待儿童",
|
||||
"other": "其他"
|
||||
},
|
||||
"Searching...": "搜索中……",
|
||||
"searchError": "搜索失败,请重试。",
|
||||
"tryDifferentKeywords": "尝试不同的关键字或删除搜索过滤器",
|
||||
"settingsWindowOpacity": "窗口不透明度",
|
||||
"messageContent": "消息内容",
|
||||
"updateAvailable": "更新可用",
|
||||
"noChangelogProvided": "没有提供更新日志。",
|
||||
"useSecondarySourceForDownload": "使用次要源下载",
|
||||
"installUpdate": "安装更新",
|
||||
"openReleasePage": "打开发行页面",
|
||||
"postCompose": "撰写帖子",
|
||||
"postPublish": "发布帖子",
|
||||
"restoreDraftTitle": "恢复草稿",
|
||||
"restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?",
|
||||
"draft": "草稿",
|
||||
"purchaseGift": "购买礼物",
|
||||
"selectRecipient": "选择款件人",
|
||||
"changeRecipient": "修改款件人",
|
||||
"addMessage": "添加信息",
|
||||
"skipRecipient": "跳过款件人",
|
||||
"giftSubscriptions": "礼物订阅",
|
||||
"purchaseAGift": "购买礼物",
|
||||
"redeemAGift": "兑换礼物",
|
||||
"giftHistory": "礼物历史",
|
||||
"sentGifts": "发送礼物",
|
||||
"receivedGifts": "接收礼物",
|
||||
"noSentGifts": "没有发送过礼物",
|
||||
"noReceivedGifts": "没有收到过礼物",
|
||||
"stellarGift": "恒星订阅",
|
||||
"novaGift": "新星订阅",
|
||||
"supernovaGift": "超新星订阅",
|
||||
"sameAsMembership": "与成员相同",
|
||||
"enterGiftCodeToRedeem": "输入礼品代码以兑换",
|
||||
"enterGiftCode": "输入礼物代码",
|
||||
"giftPurchased": "已购买礼物!",
|
||||
"shareCodeWithRecipient": "与收件人分享此代码来兑换礼物。",
|
||||
"openGiftAnyoneCanRedeem": "这是一份任何人都可以兑换的公开礼物。",
|
||||
"ok": "确定",
|
||||
"selectedRecipient": "选定收件人",
|
||||
"noRecipientSelected": "未选择收件人",
|
||||
"thisWillBeAnOpenGift": "这将是一份公开的礼物",
|
||||
"personalMessage": "个人消息",
|
||||
"addPersonalMessageForRecipient": "为收件人添加个人消息",
|
||||
"giftStatusCreated": "已创建",
|
||||
"giftStatusSent": "发送",
|
||||
"giftStatusRedeemed": "已兑换",
|
||||
"giftStatusCancelled": "已取消",
|
||||
"giftStatusExpired": "已过期",
|
||||
"giftStatusUnknown": "未知",
|
||||
"giftCodeCopiedToClipboard": "礼物代码已复制到剪贴板",
|
||||
"codeLabel": "代码: ",
|
||||
"subscriptionLabel": "订阅: ",
|
||||
"toLabel": "发送至: ",
|
||||
"fromLabel": "来自: ",
|
||||
"messageLabel": "消息: ",
|
||||
"giftRedeemed": "礼物兑换成功!",
|
||||
"giftRedeemedSuccessfully": "您已成功兑换了礼物。您的新订阅现在已经生效。",
|
||||
"cancelGift": "取消礼物",
|
||||
"cancelGiftConfirm": "您确定要取消此礼物?此操作不能撤消。",
|
||||
"giftCancelledSuccessfully": "已成功取消礼物",
|
||||
"createFund": "创建红包",
|
||||
"fundAmount": "红包金额",
|
||||
"enterAmount": "输入金额",
|
||||
"selectCurrency": "选择币种",
|
||||
"splitType": "拆分类型",
|
||||
"evenSplit": "平均分配",
|
||||
"equalAmountEach": "每个收款人的金额相同",
|
||||
"randomSplit": "随机分配",
|
||||
"randomAmountEach": "每个收款人的金额随机",
|
||||
"recipientCount": "收款人总计",
|
||||
"numberOfRecipients": "收款人数量",
|
||||
"addPersonalMessageForRecipients": "为收款人添加个人信息",
|
||||
"invalidAmount": "无效的金额",
|
||||
"invalidRecipientCount": "收款人数量无效",
|
||||
"fundOverview": "红包概述",
|
||||
"totalFundsSent": "已发送的红包总额",
|
||||
"totalFundsReceived": "收到的红包总额",
|
||||
"transactions": "交易",
|
||||
"myFunds": "我的支票",
|
||||
"availableFunds": "可用支票",
|
||||
"fundStatusCreated": "已创建",
|
||||
"fundStatusPartial": "部分领取",
|
||||
"fundStatusCompleted": "已领完",
|
||||
"fundStatusExpired": "已过期",
|
||||
"fundStatusUnknown": "未知",
|
||||
"recipients": "收款人",
|
||||
"fundClaimedSuccessfully": "支票领取成功!",
|
||||
"claim": "申请",
|
||||
"noFundsCreated": "尚未创建任何支票",
|
||||
"createYourFirstFund": "创建您的第一个支票来开始",
|
||||
"noAvailableFunds": "暂无可用支票",
|
||||
"fundsWillAppearHere": "您可以领取的支票将出现在这里",
|
||||
"fundCreatedSuccessfully": "支票创建成功!",
|
||||
"selectRecipients": "选择收款人",
|
||||
"noRecipientsSelected": "尚未选择收款人",
|
||||
"selectRecipientsToSendFund": "选择收款人将支票发送到",
|
||||
"addRecipient": "添加收款人",
|
||||
"addMoreRecipients": "添加更多收款人",
|
||||
"transactionDetails": "交易详情",
|
||||
"remarks": "备注",
|
||||
"payer": "付款方",
|
||||
"payee": "交易方",
|
||||
"transactionType": "交易类型",
|
||||
"transfer": "转账",
|
||||
"payment": "支付",
|
||||
"systemWallet": "中央统筹",
|
||||
"date": "日期",
|
||||
"createTransfer": "创建转账",
|
||||
"transferAmount": "转账金额",
|
||||
"selectPayee": "请选择收款人",
|
||||
"selectedPayee": "选定的收款人",
|
||||
"noPayeeSelected": "没有选择收款人",
|
||||
"selectPayeeToTransfer": "选择要转账的收款人",
|
||||
"addRemark": "添加备注",
|
||||
"transferRemark": "转账备注",
|
||||
"addRemarkForTransfer": "为转账添加备注",
|
||||
"enterPinToConfirmTransfer": "输入您的 6 位PIN码以确认转账",
|
||||
"transferCreatedSuccessfully": "转账成功创建!",
|
||||
"postUpdate": "更新",
|
||||
"fileMetadata": "文件元数据",
|
||||
"resend": "重新发送",
|
||||
"fileInfoTitle": "文件信息",
|
||||
"download": "下载",
|
||||
"info": "信息",
|
||||
"noStickers": "无贴图",
|
||||
"noStickersInPack": "这个包没有贴纸",
|
||||
"noStickerPacks": "无贴图包",
|
||||
"refresh": "刷新",
|
||||
"spoiler": "已隐藏",
|
||||
"activityHeatmap": "活动热力图",
|
||||
"custom": "自定义",
|
||||
"usernameColor": "用户名颜色",
|
||||
"colorType": "颜色类型",
|
||||
"plain": "纯色",
|
||||
"gradient": "渐变",
|
||||
"colorValue": "色值",
|
||||
"gradientDirection": "渐变方向",
|
||||
"gradientDirectionToRight": "向右",
|
||||
"gradientDirectionToLeft": "向左",
|
||||
"gradientDirectionToBottom": "向底部",
|
||||
"gradientDirectionToTop": "向上",
|
||||
"gradientDirectionToBottomRight": "向右下角",
|
||||
"gradientDirectionToBottomLeft": "向左下角",
|
||||
"gradientDirectionToTopRight": "向右上角",
|
||||
"gradientDirectionToTopLeft": "向左下角",
|
||||
"gradientColors": "渐变颜色",
|
||||
"color": "颜色",
|
||||
"addColor": "添加颜色",
|
||||
"availableWithYourPlan": "适用于您的计划",
|
||||
"upgradeRequired": "需要升级恒星计划等级",
|
||||
"settingsDisableAnimation": "禁用动画",
|
||||
"addTag": "添加标签",
|
||||
"accountConnectionProviderSpotify": "Spotify",
|
||||
"accountConnectionProviderSteam": "Steam",
|
||||
"timezoneNotFound": "未找到时区",
|
||||
"awardPoints": "收到 {} 点奖励",
|
||||
"postFeaturedOn": "帖子在 {} 被精选",
|
||||
"messageSentAt": "在 {} 发送",
|
||||
"myTickets": "我的彩票",
|
||||
"drawHistory": "抽奖历史",
|
||||
"lottery": "彩票",
|
||||
"noLotteryTickets": "暂无彩票",
|
||||
"buyYourFirstTicket": "购买您的第一张彩票开始!",
|
||||
"buyTicket": "购买彩票",
|
||||
"ticketNumbers": "数字: {}, 特殊数字: {}",
|
||||
"cost": "花费",
|
||||
"multiplier": "倍率",
|
||||
"prizeWon": "获奖者",
|
||||
"pending": "待开奖",
|
||||
"drawn": "已开奖",
|
||||
"won": "赢",
|
||||
"lost": "输",
|
||||
"noDrawHistory": "暂无开奖历史",
|
||||
"buyLotteryTicket": "购买彩票",
|
||||
"selectNumbers": "选择数字",
|
||||
"select5UniqueNumbers": "选择 5 个唯一数字",
|
||||
"selectSpecialNumber": "选择特殊数字",
|
||||
"selectMultiplier": "选择倍数",
|
||||
"baseCost": "基础花费",
|
||||
"totalCost": "总费用",
|
||||
"prizeStructure": "奖金分级",
|
||||
"enterPinToConfirmPurchase": "输入您的 PIN 码以确认购买",
|
||||
"ticketPurchasedSuccessfully": "彩票购买成功!",
|
||||
"winningNumbers": "获胜数字",
|
||||
"specialNumber": "特殊数字",
|
||||
"totalTickets": "总售出票数",
|
||||
"totalWinners": "中奖者总人数",
|
||||
"prizePool": "奖金池",
|
||||
"enterPinToConfirmPayment": "输入您的 PIN 码以确认付款",
|
||||
"purchase": "购买",
|
||||
"multiplierLabel": "倍率",
|
||||
"specialOnly": "仅特殊的",
|
||||
"matches": "场次",
|
||||
"thoughtDefaultTopic": "寻思",
|
||||
"thoughtAiName": "SN 酱",
|
||||
"thoughtUserName": "您",
|
||||
"thoughtStreamingHint": "SN 酱正在思考...",
|
||||
"thoughtInputHint": "问 SN 酱任何问题...",
|
||||
"thoughtNewConversation": "开始新对话",
|
||||
"thoughtParseError": "解析 AI 响应失败",
|
||||
"thoughtFunctionCall": "调用 {} 函数",
|
||||
"aiThought": "寻思",
|
||||
"aiThoughtTitle": "让 SN 酱寻思寻思",
|
||||
"postReferenceUnavailable": "引用的帖子不可用",
|
||||
"fabLocation": "底部导航按钮位置",
|
||||
"activities": "活动",
|
||||
"presenceTypeGaming": "正在玩",
|
||||
"presenceTypeMusic": "正在听音乐",
|
||||
"presenceTypeWorkout": "锻炼中",
|
||||
"articleCompose": "撰写文章",
|
||||
"backToHub": "返回至主页",
|
||||
"advancedFilters": "高级筛选",
|
||||
"searchPosts": "搜索帖子",
|
||||
"sortBy": "排序方式",
|
||||
"fromDate": "起始日期",
|
||||
"toDate": "截止日期",
|
||||
"popularity": "按热度",
|
||||
"descendingOrder": "降序排序",
|
||||
"selectDate": "选择日期",
|
||||
"pinnedPosts": "已置顶的帖子",
|
||||
"customReactionHint": "自定义反应允许你使用用户上传贴纸作为帖子反应的符号,需要恒星计划订阅。",
|
||||
"publicationSites": "发布者站点",
|
||||
"uploadTasks": "上传任务",
|
||||
"thoughtFunctionCallBegin": "调用工具 {}",
|
||||
"thoughtFunctionCallFinish": "工具 {} 响应",
|
||||
"thoughtUnpaidHint": "寻思因为有未支付的订单而被禁用",
|
||||
"more": "更多",
|
||||
"collapse": "折叠",
|
||||
"pollConfirmDiscard": "您确定要离开吗?您编辑的所有数据都不会被保存。",
|
||||
"discard": "放弃",
|
||||
"fund": "支票",
|
||||
"fundsRecent": "最近支票",
|
||||
"fundCreateNew": "创建新支票",
|
||||
"fundCreateNewHint": "为您的消息创建一个新的红包。选择接收者和金额。",
|
||||
"amountOfSplits": "份数",
|
||||
"enterNumberOfSplits": "单份金额",
|
||||
"orCreateWith": "或\n使用第三方帐户注册",
|
||||
"unindexedFiles": "未索引的文件",
|
||||
"folder": "文件夹",
|
||||
"clearCompleted": "清除已完成的",
|
||||
"uploadSuccess": "上传成功!",
|
||||
"wouldYouLikeToViewFile": "预览此文件?",
|
||||
"contentCantEmpty": "内容不能为空",
|
||||
"features": "特征",
|
||||
"unnamed": "未命名",
|
||||
"fundEnvelopeLoadFailed": "加载支票信封失败",
|
||||
"fundEnvelope": "支票信封",
|
||||
"fundEnvelopeRemaining": "剩余:{} {}",
|
||||
"fundEnvelopeSplit": "拆分:{}",
|
||||
"fundEnvelopeSplitEvenly": "均分",
|
||||
"fundEnvelopeSplitRandomly": "随机",
|
||||
"fundEnvelopeClaimSuccess": "支票领取成功!",
|
||||
"fundEnvelopeStatusCreated": "已创建",
|
||||
"fundEnvelopeStatusPartial": "已领取部分",
|
||||
"fundEnvelopeStatusCompleted": "已全部领取",
|
||||
"fundEnvelopeStatusExpired": "已过期",
|
||||
"fundEnvelopeStatusUnknown": "未知",
|
||||
"fundEnvelopeRecipients": "收款人 ({}/{} 已领取)",
|
||||
"fundEnvelopeExpiredDaysAgo": {
|
||||
"one": "{} 天前过期",
|
||||
"other": "{} 天前过期"
|
||||
},
|
||||
"fundEnvelopeExpiresSoon": "即将到期",
|
||||
"fundEnvelopeExpiresInHours": {
|
||||
"one": "{} 小时后到期",
|
||||
"other": "{}小时后到期"
|
||||
},
|
||||
"fundEnvelopeExpiresInDays": {
|
||||
"one": "{} 天后到期",
|
||||
"other": "{} 天后到期"
|
||||
},
|
||||
"fundEnvelopeRemainingWithSplits": "{} {} / {} 份",
|
||||
"fundEnvelopeUnknownUser": "未知用户",
|
||||
"deleteSite": "删除网站",
|
||||
"deleteSiteConfirm": "您确定要删除此网站?",
|
||||
"siteDeletedSuccess": "网站成功删除",
|
||||
"siteSlug": "标识符",
|
||||
"siteSlugHint": "我的网站",
|
||||
"siteSlugRequired": "请输入一个标识符",
|
||||
"siteSlugInvalid": "标识符只能包含小写字母、数字和短横线",
|
||||
"siteName": "网站名称",
|
||||
"siteNameHint": "我的发布者网站",
|
||||
"siteNameRequired": "请输入网站名称",
|
||||
"siteMode": "模式",
|
||||
"siteModeFullyManaged": "全托管",
|
||||
"siteModeSelfManaged": "自托管",
|
||||
"editPublicationSite": "编辑发布者网站",
|
||||
"deletePublicationSite": "删除发布者网站",
|
||||
"publicationSiteSavedSuccess": "发布者网站成功删除",
|
||||
"publicationSiteDeleteConfirm": "您确定要删除该发布者网站吗?此操作不能撤销。",
|
||||
"publicationSiteDeletedSuccess": "发布者网站成功删除",
|
||||
"newPublicationSite": "新建发布者网站",
|
||||
"siteDetails": "网站描述",
|
||||
"siteInformation": "网站信息",
|
||||
"siteDomain": "域名",
|
||||
"siteCreated": "创建于",
|
||||
"siteUpdated": "更新于",
|
||||
"failedToLoadSite": "加载网站失败",
|
||||
"sitePages": "页面",
|
||||
"noPagesYet": "还没有页面",
|
||||
"createFirstPage": "创建您的第一个页面以开始",
|
||||
"failedToLoadPages": "加载页面失败",
|
||||
"fileManagement": "文件管理",
|
||||
"siteFiles": "文件",
|
||||
"siteFolder": "文件夹",
|
||||
"siteRoot": "根",
|
||||
"noFilesUploadedYet": "还没有文件被删除",
|
||||
"uploadFirstFile": "上传您的第一个文件以开始",
|
||||
"failedToLoadFiles": "加载文件失败",
|
||||
"noFilesFoundInFolder": "选择的文件夹里没有文件",
|
||||
"fileActions": "文件选项",
|
||||
"purgeFiles": "清除文件",
|
||||
"purgeFilesDescription": "从这个网站删除全部文件",
|
||||
"deploySite": "部署网站",
|
||||
"deploySiteDescription": "从ZIP存档上传和部署新版本",
|
||||
"confirmPurge": "确认清空",
|
||||
"purgeFilesConfirm": "这将永久删除上传到本网站的所有文件。此操作无法撤销。您确定要继续吗?",
|
||||
"purgeAllFiles": "清除所有文件",
|
||||
"allFilesPurgedSuccess": "所有文件都清除成功",
|
||||
"failedToPurgeFiles": "清除文件失败:{}",
|
||||
"siteDeployedSuccess": "网站部署成功",
|
||||
"failedToDeploySite": "部署网站失败:{}",
|
||||
"createPage": "创建页面",
|
||||
"editPage": "编辑页面",
|
||||
"pageType": "页面类型",
|
||||
"htmlPage": "HTML 页面",
|
||||
"redirectPage": "重定向页面",
|
||||
"pageTypeRequired": "请选择一个页面类型",
|
||||
"pagePath": "页面路径",
|
||||
"pagePathHint": "例如 /about, /contact 等。",
|
||||
"pagePathRequired": "请输入一个页面路径",
|
||||
"pagePathInvalid": "页面路径只能包含字母、数字、连字符、下划线和斜杠",
|
||||
"pagePathMustStartWithSlash": "页面路径必须以 / 开头",
|
||||
"pagePathNoConsecutiveSlashes": "页面路径不能有连续的斜杠",
|
||||
"pageTitle": "页面标题",
|
||||
"pageTitleHint": "例如关于我们,联系方式等。",
|
||||
"pageTitleRequired": "请输入一个页面标题",
|
||||
"pageContentHtml": "页面内容 (HTML)",
|
||||
"pageContentHint": "<h1>Hello World</h1><p>这是我的页面内容…</p>",
|
||||
"pageContentRequired": "请为这个页面输入HTML内容",
|
||||
"redirectTarget": "重定向目标",
|
||||
"redirectTargetHint": "例如 /new-page, https://example.com 等。",
|
||||
"redirectTargetRequired": "请输入重定向目标",
|
||||
"redirectTargetInvalid": "目标必须是相对路径 (/) 或绝对URL (http/https)",
|
||||
"deletePage": "删除页面",
|
||||
"deletePageConfirm": "您确定要删除此页面?",
|
||||
"savePage": "保存页面",
|
||||
"pageCreatedSuccess": "页面成功创建",
|
||||
"pageUpdatedSuccess": "页面上传成功",
|
||||
"pageDeletedSuccess": "页面已成功删除",
|
||||
"uploadFiles": "上传文件",
|
||||
"uploadPath": "上传路径",
|
||||
"uploadPathHint": "/ (根) 或 /assets/images/",
|
||||
"uploadPathRequired": "请输入一个上传路径",
|
||||
"uploadPathMustStartWithSlash": "路径必须以/开头",
|
||||
"uploadPathNoSpaces": "路径不能包含空格",
|
||||
"uploadPathNoConsecutiveSlashes": "路径不能有连续的斜杠",
|
||||
"percentCompleted": "{}% 已完成",
|
||||
"filesToUpload": "{} 个文件已上传",
|
||||
"fileSizeKb": "大小:{} KB",
|
||||
"uploadingEllipsis": "上传中……",
|
||||
"uploadFilesCount": {
|
||||
"one": "上传 {} 个文件",
|
||||
"other": "上传 {} 个文件"
|
||||
},
|
||||
"allUploadsCompleted": "所有文件已上传",
|
||||
"someUploadsFailed": "一些上传失败",
|
||||
"uploadingInProgress": "上传正在进行中",
|
||||
"readyToUpload": "准备好上传",
|
||||
"allFilesUploadedSuccess": "所有文件已成功上传",
|
||||
"lotteryLastNumberSpecial": "最后选择的数字将是您的特殊数字。",
|
||||
"lotteryMultiplierRequired": "请输入倍率",
|
||||
"lotteryMultiplierRange": "倍率必须在 1 到 10 之间",
|
||||
"dropToShare": "拖到此处以分享",
|
||||
"affiliationSpell": "邀请码",
|
||||
"affiliationSpellHint": "如果您有邀请码,请在此处输入。",
|
||||
"friendsOnline": "在线好友",
|
||||
"createAccountAlmostThere": "就差一步",
|
||||
"createAccountAlmostThereHint": "您距离加入 Solar Network 仅一步之遥!请解决接下来显示的验证码谜题。",
|
||||
"createAccountNotice": "创建账户前您需要了解的事项:",
|
||||
"createAccountConfirmEmail": "账户创建后,您需要前往您的邮箱收件箱激活账户,以获得使用所有功能的权限。",
|
||||
"createAccountNoAltAccounts": "Solar Network 禁止使用多个或替代账户,这会违反我们的服务条款。",
|
||||
"createAccountAgreeTerms": "我已阅读并同意服务条款。",
|
||||
"createAccountProfile": "创建您的个人资料",
|
||||
"createAccountToS": "查看条款与条件",
|
||||
"accountActivationAlert": "请记住激活您的账户",
|
||||
"accountActivationAlertHint": "未激活的账户可能会导致各种权限问题,请点击我们发送到您邮箱收件箱的链接来激活您的账户。",
|
||||
"accountActivationResendHint": "没收到?请尝试点击下方按钮重新发送。如果您在账户未激活期间需要更新邮箱,请随时联系我们的客服。",
|
||||
"accountActivationResend": "重新发送"
|
||||
}
|
||||
|
||||
1478
assets/i18n/zh-OG.json
Normal file
@@ -122,10 +122,6 @@
|
||||
"addVideo": "添加視頻",
|
||||
"addPhoto": "添加照片",
|
||||
"addFile": "添加文件",
|
||||
"uploadFile": "上傳文件",
|
||||
"settingsDefaultPool": "選擇文件池",
|
||||
"settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池",
|
||||
|
||||
"createDirectMessage": "創建新私人消息",
|
||||
"gotoDirectMessage": "前往私信",
|
||||
"react": "反應",
|
||||
@@ -168,8 +164,6 @@
|
||||
"checkInResultLevel3": "好運",
|
||||
"checkInResultLevel4": "最佳運氣",
|
||||
"checkInActivityTitle": "{} 在 {} 簽到並獲得了 {}",
|
||||
"eventCalander": "活動日曆",
|
||||
"eventCalanderEmpty": "該日無活動。",
|
||||
"fortuneGraph": "時運趨勢",
|
||||
"noFortuneData": "本月沒有時運數據。",
|
||||
"creatorHub": "創作者中心",
|
||||
@@ -307,7 +301,6 @@
|
||||
"notifications": "通知",
|
||||
"posts": "帖子",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageEnable": "顯示背景圖片",
|
||||
"settingsBackgroundImageClear": "清除背景圖片",
|
||||
"settingsBackgroundGenerateColor": "從背景圖像生成主題色",
|
||||
"messageNone": "沒有內容可顯示",
|
||||
@@ -319,8 +312,6 @@
|
||||
"settingsRealmCompactView": "緊湊領域視圖",
|
||||
"settingsMixedFeed": "混合動態",
|
||||
"settingsAutoTranslate": "自動翻譯",
|
||||
"settingsDataSavingMode": "低數據模式",
|
||||
"dataSavingHint": "低數據模式",
|
||||
"settingsHideBottomNav": "隱藏底部導航",
|
||||
"settingsSoundEffects": "音效",
|
||||
"settingsAprilFoolFeatures": "愚人節功能",
|
||||
@@ -675,7 +666,6 @@
|
||||
"publisherFeatureDevelopDescription": "為你的開發者解鎖包括應用套件,API 及更多開發功能。",
|
||||
"publisherFeatureDevelopHint": "目前該功能還在開發中,你需要邀請才可解鎖。",
|
||||
"learnMore": "瞭解更多",
|
||||
"discoverWebArticles": "來自站外的文章",
|
||||
"webArticlesStand": "文章亭",
|
||||
"about": "關於",
|
||||
"somethingWentWrong": "發生了一些錯誤",
|
||||
@@ -698,8 +688,6 @@
|
||||
"sharePostPhoto": "通過圖片分享帖子",
|
||||
"wouldYouLikeToNavigateToChat": "你想要前往聊天頁面嗎?",
|
||||
"abuseReports": "舉報",
|
||||
"discoverRealms": "發現領域",
|
||||
"discoverPublishers": "發現發佈者",
|
||||
"membershipCancel": "取消會員訂閱",
|
||||
"membershipCancelConfirm": "你確定要取消會員訂閱嗎?",
|
||||
"membershipCancelHint": "你確定要取消會員訂閱嗎?你將不會再次被扣費。你的會員資格將在當前計費週期結束前保持有效。並且你將無法重新訂閱,直到當前訂閱結束。",
|
||||
@@ -762,21 +750,6 @@
|
||||
"rename": "重命名",
|
||||
"markAsSensitive": "標記為敏感",
|
||||
"fileName": "文件名",
|
||||
"sensitiveCategories": {
|
||||
"language": "語言",
|
||||
"sexualContent": "色情內容",
|
||||
"violence": "暴力",
|
||||
"profanity": "褻瀆",
|
||||
"hateSpeech": "仇恨言論",
|
||||
"racism": "種族主義",
|
||||
"adultContent": "成人內容",
|
||||
"drugAbuse": "藥物濫用",
|
||||
"alcoholAbuse": "酗酒",
|
||||
"gambling": "賭博",
|
||||
"selfHarm": "自殘",
|
||||
"childAbuse": "虐待兒童",
|
||||
"other": "其他"
|
||||
},
|
||||
"poll": "投票",
|
||||
"pollsRecent": "最近投票",
|
||||
"pollCreateNew": "創建新投票",
|
||||
@@ -819,6 +792,159 @@
|
||||
"one": "+{} 個文件被摺疊",
|
||||
"other": "+{} 個文件被摺疊"
|
||||
},
|
||||
"pollQuestions": "問題",
|
||||
"pollAnswerSubmitted": "投票答案已提交。",
|
||||
"modifyAnswers": "修改答案",
|
||||
"back": "返回",
|
||||
"submit": "提交",
|
||||
"pollOptionDefaultLabel": "選項1",
|
||||
"pollUpdated": "投票已更新。",
|
||||
"pollCreated": "投票已創建。",
|
||||
"pollCreate": "創建投票",
|
||||
"pollEdit": "編輯投票",
|
||||
"pollPreviewJsonDebug": "調試預覽",
|
||||
"pollTitleRequired": "標題不可為空",
|
||||
"pollEndDateOptional": "結束日期和時間 (可選)",
|
||||
"notSet": "未設定",
|
||||
"pick": "選擇",
|
||||
"clear": "清除",
|
||||
"questions": "問題",
|
||||
"pollAddQuestion": "添加問題",
|
||||
"pollQuestionTypeSingleChoice": "單選框",
|
||||
"pollQuestionTypeMultipleChoice": "多選框",
|
||||
"pollQuestionTypeFreeText": "自由文本",
|
||||
"pollQuestionTypeYesNo": "是 / 不是",
|
||||
"pollQuestionTypeRating": "評分",
|
||||
"pollNoQuestionsYet": "尚未有問題",
|
||||
"pollNoQuestionsHint": "使用「添加問題」開始建立您的投票。",
|
||||
"pollDebugPreview": "調試預覽",
|
||||
"pollUntitledQuestion": "無標題問題",
|
||||
"moveUp": "往上移動",
|
||||
"moveDown": "往下移動",
|
||||
"required": "必需的",
|
||||
"pollQuestionTitle": "問題標題",
|
||||
"pollQuestionTitleRequired": "問題標題是必需的",
|
||||
"pollQuestionDescriptionOptional": "問題描述(選填)",
|
||||
"options": "選項",
|
||||
"pollAddOption": "添加選項",
|
||||
"pollOptionLabel": "選項標籤",
|
||||
"pollLongTextAnswerPreview": "長文本答案 (預覽)",
|
||||
"pollShortTextAnswerPreview": "短文本答案 (預覽)",
|
||||
"award": "讚賞",
|
||||
"awardPost": "讚賞帖子",
|
||||
"awardMessage": "消息",
|
||||
"awardMessageHint": "輸入您的讚賞消息...",
|
||||
"awardAttitude": "態度",
|
||||
"awardAttitudePositive": "積極",
|
||||
"awardAttitudeNegative": "消极",
|
||||
"awardAmount": "金額",
|
||||
"awardAmountHint": "輸入金額……",
|
||||
"awardAmountRequired": "「金額」為必填字段",
|
||||
"awardAmountInvalid": "請輸入有效金額",
|
||||
"awardMessageTooLong": "消息太長(最多4096個字符)",
|
||||
"awardSuccess": "獎勵已成功發送!",
|
||||
"awardSubmit": "讚賞",
|
||||
"awardPostPreview": "帖子預覽",
|
||||
"awardNoContent": "暫無內容",
|
||||
"awardByPublisher": "由 {} 發表",
|
||||
"awardBenefits": "讚賞福利",
|
||||
"awardBenefitsDescription": "為該帖子授予獎勵可以提升其價值和曝光度。價值更高的帖子更有可能在社區中被推薦和突出顯示。",
|
||||
"checkInResultLevel5": "生日快樂 🥳",
|
||||
"region": "區域",
|
||||
"accountRegionHint": "這個區域將用於內容傳遞和本地化。",
|
||||
"settingsCustomFontsHelper": "使用逗號分隔。",
|
||||
"settingsBackgroundImageEnable": "顯示背景圖片",
|
||||
"settingsDataSavingMode": "低數據模式",
|
||||
"dataSavingHint": "低數據模式",
|
||||
"postTypePost": "帖子",
|
||||
"searchDrafts": "搜尋草稿……",
|
||||
"noSearchResults": "無搜尋結果",
|
||||
"contactMethodMakePublic": "設為公開",
|
||||
"contactMethodMakePrivate": "設定為僅自己可見",
|
||||
"contactMethodPublic": "公開",
|
||||
"contactMethodPrivate": "私密",
|
||||
"discoverRealms": "發現領域",
|
||||
"discoverPublishers": "發現發佈者",
|
||||
"discoverShuffledPost": "隨機帖子",
|
||||
"projects": "項目",
|
||||
"noProjects": "未找到項目。",
|
||||
"deleteProject": "刪除項目",
|
||||
"deleteProjectHint": "確定要刪除此項目嗎?此操作無法撤銷。",
|
||||
"createProject": "新建專案",
|
||||
"editProject": "編輯項目",
|
||||
"projectDetails": "專案描述",
|
||||
"createBot": "創建機器人",
|
||||
"bots": "機器人",
|
||||
"noBots": "還沒有機器人。",
|
||||
"deleteBotHint": "您確定要刪除這個機器人嗎?此操作無法撤銷。",
|
||||
"deleteBot": "刪除機器人",
|
||||
"discoverWebArticles": "來自站外的文章",
|
||||
"messageJumpNotLoaded": "引用的訊息未加載,無法跳轉到該訊息。",
|
||||
"postUnlinkRealm": "未連結到領域",
|
||||
"postSlug": "別名",
|
||||
"postSlugHint": "這個別名可以用於在網頁通過 URL 瀏覽到你的帖子,它應該在同一發布者中是唯一。",
|
||||
"attachmentOnDevice": "離線",
|
||||
"attachmentOnCloud": "在線",
|
||||
"attachments": "附件",
|
||||
"publisherCollabInvitation": "協作邀請",
|
||||
"publisherCollabInvitationCount": {
|
||||
"zero": "無邀請",
|
||||
"one": "{} 個可用邀請",
|
||||
"other": "{} 個可用邀請"
|
||||
},
|
||||
"failedToLoadUserInfo": "無法加載用戶資訊",
|
||||
"failedToLoadUserInfoNetwork": "看起來是網絡問題,您可以點擊下面的按鈕再試一次。",
|
||||
"failedToLoadUserInfoUnauthorized": "看起來您的會話已經登出或不再可用,如果您想的話,您仍然可以嘗試再次獲取用戶資訊。",
|
||||
"okay": "好的",
|
||||
"postDetail": "帖子詳情",
|
||||
"postCount": {
|
||||
"zero": "沒有帖子",
|
||||
"one": "{} 帖子",
|
||||
"other": "{} 帖子"
|
||||
},
|
||||
"mimeType": "類型",
|
||||
"fileSize": "文件大小",
|
||||
"fileHash": "文件哈希",
|
||||
"exifData": "EXIF 數據",
|
||||
"postShuffle": "隨機帖子",
|
||||
"leveling": "等級",
|
||||
"levelingHistory": "經驗記錄",
|
||||
"stellarProgram": "恆星計畫",
|
||||
"socialCredits": "社會信用點",
|
||||
"credits": "信用",
|
||||
"creditsStatus": "積分狀態",
|
||||
"socialCreditsDescription": "社會信用是 Solar Network 評價用戶的一種方式。它基於用戶的行為和互動來計算。以 100 分為基準,分數越高表示用戶在社區中的信譽越好。分數會隨著時間的推移而變化,反映用戶的最新行為。信用等級高的用戶可以享受到更多的福利,反之的用戶部分功能可能受到限制。",
|
||||
"socialCreditsLevelPoor": "糟糕",
|
||||
"socialCreditsLevelNormal": "正常",
|
||||
"socialCreditsLevelGood": "良好",
|
||||
"socialCreditsLevelExcellent": "優秀",
|
||||
"orderByPopularity": "按熱度排序",
|
||||
"orderByReleaseDate": "按發佈日期排序",
|
||||
"editBot": "編輯機器人",
|
||||
"botAutomatedBy": "由 {} 自動化",
|
||||
"botDetails": "機器人描述",
|
||||
"overview": "概述",
|
||||
"keys": "密鑰",
|
||||
"botNotFound": "機器人未找到。",
|
||||
"newBotKey": "新建密鑰",
|
||||
"newBotKeyHint": "輸入新密鑰的名稱,密鑰只會顯示一次。",
|
||||
"revokeBotKey": "撤銷密鑰",
|
||||
"revokeBotKeyHint": "你確定要撤銷這個密鑰?這個操作無法撤回,所有使用該密鑰的應用程式會停止工作。",
|
||||
"noBotKeys": "機器人未找到。",
|
||||
"revoke": "撤銷",
|
||||
"keyName": "密鑰名稱",
|
||||
"newKeyGenerated": "新密鑰已生成",
|
||||
"copyKeyHint": "請安全地保存該密鑰,你不會再次看到它。",
|
||||
"rotateKey": "旋轉密鑰",
|
||||
"rotateBotKey": "旋轉密鑰",
|
||||
"rotateBotKeyHint": "你確認要旋轉這個密鑰?久的密鑰會立即失效,該操作無法撤銷。",
|
||||
"webFeedArticleCount": {
|
||||
"zero": "無文章",
|
||||
"one": "{} 文章",
|
||||
"other": "{} 文章"
|
||||
},
|
||||
"webFeedSubscribed": "你已經訂閱了這個來源",
|
||||
"webFeedUnsubscribed": "你已經取消訂閱這個來源",
|
||||
"appDetails": "應用程式詳情",
|
||||
"secrets": "密鑰",
|
||||
"appNotFound": "找不到應用程式。",
|
||||
@@ -830,5 +956,523 @@
|
||||
"newSecretGenerated": "已產生新密鑰",
|
||||
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
|
||||
"expiresIn": "過期時間(秒)",
|
||||
"isOidc": "OIDC 相容"
|
||||
"isOidc": "OIDC 相容",
|
||||
"pinPost": "置頂帖子",
|
||||
"unpinPost": "取消置頂",
|
||||
"pinnedPost": "已置顶",
|
||||
"publisherPage": "發布者頁面",
|
||||
"realmPage": "領域頁面",
|
||||
"replyPage": "回覆頁面",
|
||||
"pinPostPublisherHint": "將這篇文章置顶到您的發佈者頁面",
|
||||
"pinPostRealmHint": "將這篇文章置顶到領域頁面",
|
||||
"pinPostRealmDisabledHint": "這個帖子不屬於任何領域",
|
||||
"pinPostReplyHint": "將這篇文章置顶到回覆頁面",
|
||||
"pinPostReplyDisabledHint": "這篇帖子不是回覆",
|
||||
"pin": "置顶",
|
||||
"unpinPostHint": "你確定要取消置顶這篇帖子嗎?",
|
||||
"all": "所有",
|
||||
"statusPresent": "至今",
|
||||
"accountAutomated": "機器人",
|
||||
"chatBreakClearButton": "清除",
|
||||
"chatBreak5m": "5 分鐘",
|
||||
"chatBreak10m": "10 分鐘",
|
||||
"chatBreak15m": "15 分鐘",
|
||||
"chatBreak30m": "30 分鐘",
|
||||
"chatBreakCustomMinutes": "自訂(分鐘)",
|
||||
"errorGeneric": "錯誤:{}",
|
||||
"searchMessages": "搜尋消息",
|
||||
"messagesCount": "{} 消息",
|
||||
"dotSeparator": ".",
|
||||
"roleValidationHint": "成員角色必須設置在0到100之間",
|
||||
"searchMessagesHint": "搜尋消息…",
|
||||
"searchLinks": "連結",
|
||||
"searchAttachments": "附件",
|
||||
"noMessagesFound": "未找到消息",
|
||||
"openInBrowser": "在瀏覽器打開",
|
||||
"highlightPost": "精選帖子",
|
||||
"filters": "過濾器",
|
||||
"apply": "應用",
|
||||
"pubName": "題目名稱",
|
||||
"realm": "領域",
|
||||
"shuffle": "隨機",
|
||||
"pinned": "已置顶",
|
||||
"noResultsFound": "未找到結果",
|
||||
"toggleFilters": "切換篩檢器",
|
||||
"notableDayNext": "距離 {} 還有",
|
||||
"expandPoll": "展開投票",
|
||||
"collapsePoll": "摺叠投票",
|
||||
"embedView": "嵌入視圖",
|
||||
"embedUri": "嵌入URL",
|
||||
"aspectRatio": "縱橫比",
|
||||
"renderer": "渲染器",
|
||||
"addEmbed": "添加嵌入",
|
||||
"editEmbed": "編輯嵌入",
|
||||
"deleteEmbed": "刪除嵌入",
|
||||
"deleteEmbedConfirm": "您確定要刪除這個嵌入嗎?",
|
||||
"currentEmbed": "當前嵌入",
|
||||
"noEmbed": "尚未嵌入",
|
||||
"save": "保存",
|
||||
"webView": "網頁視圖",
|
||||
"settingsDefaultPool": "預設檔案池",
|
||||
"settingsDefaultPoolHelper": "選擇文件上傳的默認儲存池",
|
||||
"uploadFile": "上傳檔案",
|
||||
"authDeviceChallenges": "設備活動",
|
||||
"authDeviceHint": "向左滑動以編輯標籤,向右滑動以登出設備。",
|
||||
"settingsMessageDisplayStyle": "訊息顯示樣式",
|
||||
"auto": "自動",
|
||||
"manual": "手動",
|
||||
"iframeCode": "Iframe 代碼",
|
||||
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||
"parseIframe": "解析 Iframe",
|
||||
"messageActions": "消息選項",
|
||||
"viewEmbedLoadHint": "點擊以載入",
|
||||
"levelingStage1": "新手",
|
||||
"levelingStage2": "學徒",
|
||||
"levelingStage3": "學徒工",
|
||||
"levelingStage4": "熟練",
|
||||
"levelingStage5": "專家",
|
||||
"levelingStage6": "大師",
|
||||
"levelingStage7": "宗師",
|
||||
"levelingStage8": "傳說",
|
||||
"levelingStage9": "神話",
|
||||
"levelingStage10": "不朽",
|
||||
"levelingStage11": "神聖",
|
||||
"levelingStage12": "超凡",
|
||||
"uploadAttachment": "上傳附件",
|
||||
"attachmentPreview": "附件預覽",
|
||||
"selectPool": "選擇檔案池",
|
||||
"choosePool": "選擇一個檔案池",
|
||||
"errorLoadingPools": "加載池時出錯",
|
||||
"quotaCostInfo": "這次上傳將消耗 {} 配額點",
|
||||
"uploadConstraints": "上傳限制",
|
||||
"fileSizeExceeded": "檔案大小超過了 {} 的最大限制",
|
||||
"fileTypeNotAccepted": "該文件類型不被此池接受",
|
||||
"files": "附件",
|
||||
"confirmDeleteFile": "你確定要刪除這個文件嗎?",
|
||||
"deleteFile": "刪除文件",
|
||||
"failedToDeleteFile": "刪除文件失敗",
|
||||
"drive": "雲盤",
|
||||
"allPools": "全部的池",
|
||||
"includeRecycled": "包含已回收文件",
|
||||
"confirmDeleteRecycledFiles": "您確定要刪除所有回收的檔案嗎?",
|
||||
"deleteRecycledFiles": "刪除已回收檔案",
|
||||
"recycledFilesDeleted": "已回收檔案刪除成功",
|
||||
"failedToDeleteRecycledFiles": "已回收檔案刪除失敗",
|
||||
"upload": "上傳",
|
||||
"deleteMessage": "刪除訊息",
|
||||
"deleteMessageConfirmation": "確定要刪除此郵件嗎?",
|
||||
"customReaction": "自訂反應",
|
||||
"customReactions": "自訂反應",
|
||||
"stickerPlaceholder": "貼紙佔位符",
|
||||
"reactionAttitude": "反應態度",
|
||||
"addReaction": "添加反應",
|
||||
"eventCalendar": "事件日曆",
|
||||
"eventCalendarEmpty": "該日無活動。",
|
||||
"walletStats": "錢包統計",
|
||||
"totalTransactions": "交易總數",
|
||||
"totalOrders": "訂單總數",
|
||||
"totalIncome": "總收入",
|
||||
"totalOutgoing": "總支出",
|
||||
"netBalance": "淨餘額",
|
||||
"messageUpdateLinks": "伺服器產生的連結預覽",
|
||||
"messageUpdateEdited": "編輯一則訊息",
|
||||
"settingsCardBackgroundOpacity": "卡片背景不透明度",
|
||||
"settingsThemeMode": "主題模式",
|
||||
"settingsThemeModeSystem": "跟隨系統",
|
||||
"settingsThemeModeLight": "淺色",
|
||||
"settingsThemeModeDark": "暗色",
|
||||
"enterPin": "請輸入您的PIN碼",
|
||||
"chatReplyingTo": "回復給 {}",
|
||||
"chatForwarding": "正在轉傳訊息",
|
||||
"chatEditing": "訊息編輯中",
|
||||
"chatNoContent": "內容為空",
|
||||
"sensitiveCategories": {
|
||||
"language": "語言",
|
||||
"sexualContent": "色情內容",
|
||||
"violence": "暴力",
|
||||
"profanity": "褻瀆",
|
||||
"hateSpeech": "仇恨言論",
|
||||
"racism": "種族主義",
|
||||
"adultContent": "成人內容",
|
||||
"drugAbuse": "藥物濫用",
|
||||
"alcoholAbuse": "酗酒",
|
||||
"gambling": "賭博",
|
||||
"selfHarm": "自殘",
|
||||
"childAbuse": "虐待兒童",
|
||||
"other": "其他"
|
||||
},
|
||||
"Searching...": "檢索中……",
|
||||
"searchError": "付款失敗,請重試。",
|
||||
"tryDifferentKeywords": "嘗試不同的關鍵字或刪除搜尋過濾器",
|
||||
"settingsWindowOpacity": "視窗不透明度",
|
||||
"messageContent": "訊息內容",
|
||||
"updateAvailable": "更新可用",
|
||||
"noChangelogProvided": "無更新紀錄。",
|
||||
"useSecondarySourceForDownload": "使用次要來源下載",
|
||||
"installUpdate": "安装更新",
|
||||
"openReleasePage": "開啟發行頁面",
|
||||
"postCompose": "撰寫帖子",
|
||||
"postPublish": "發佈帖子",
|
||||
"restoreDraftTitle": "還原草稿",
|
||||
"restoreDraftMessage": "發現了一個草稿。你想要恢復它嗎?",
|
||||
"draft": "草稿",
|
||||
"purchaseGift": "充值有禮",
|
||||
"selectRecipient": "選擇收件者",
|
||||
"changeRecipient": "修改款件人",
|
||||
"addMessage": "添加消息",
|
||||
"skipRecipient": "跳過款件人",
|
||||
"giftSubscriptions": "贈送訂閱",
|
||||
"purchaseAGift": "充值有禮",
|
||||
"redeemAGift": "兌換禮物",
|
||||
"giftHistory": "禮物記錄",
|
||||
"sentGifts": "發送禮物",
|
||||
"receivedGifts": "接收禮物",
|
||||
"noSentGifts": "沒有送過禮物",
|
||||
"noReceivedGifts": "没有收到过礼物",
|
||||
"stellarGift": "恆星禮物",
|
||||
"novaGift": "新星禮物",
|
||||
"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",
|
||||
"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",
|
||||
"availableWithYourPlan": "Available with your plan",
|
||||
"upgradeRequired": "Upgrade required",
|
||||
"settingsDisableAnimation": "Disable Animation",
|
||||
"addTag": "Add Tag",
|
||||
"accountConnectionProviderSpotify": "Spotify",
|
||||
"accountConnectionProviderSteam": "Steam",
|
||||
"timezoneNotFound": "Time zone not found",
|
||||
"awardPoints": "Awarded {} points",
|
||||
"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": "Use {}",
|
||||
"aiThought": "AI Thought",
|
||||
"aiThoughtTitle": "Let sn-chan think",
|
||||
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||
"fabLocation": "FAB Location",
|
||||
"activities": "Activities",
|
||||
"presenceTypeGaming": "Playing",
|
||||
"presenceTypeMusic": "Listening to Music",
|
||||
"presenceTypeWorkout": "Working out",
|
||||
"articleCompose": "Compose Article",
|
||||
"backToHub": "Back to Hub",
|
||||
"advancedFilters": "Advanced Filters",
|
||||
"searchPosts": "Search Posts",
|
||||
"sortBy": "Sort by",
|
||||
"fromDate": "From Date",
|
||||
"toDate": "To Date",
|
||||
"popularity": "Popularity",
|
||||
"descendingOrder": "Descending Order",
|
||||
"selectDate": "Select Date",
|
||||
"pinnedPosts": "Pinned Posts",
|
||||
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||
"publicationSites": "Publication Sites",
|
||||
"uploadTasks": "Upload Tasks",
|
||||
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||
"thoughtFunctionCallFinish": "{} responded",
|
||||
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||
"more": "More",
|
||||
"collapse": "Collapse",
|
||||
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||
"discard": "Discard",
|
||||
"fund": "Fund",
|
||||
"fundsRecent": "Recent Funds",
|
||||
"fundCreateNew": "Create New",
|
||||
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||
"amountOfSplits": "Amount of Splits",
|
||||
"enterNumberOfSplits": "Enter Splits Amount",
|
||||
"orCreateWith": "Or\ncreate with",
|
||||
"unindexedFiles": "Unindexed files",
|
||||
"folder": "Folder",
|
||||
"clearCompleted": "Clear Completed",
|
||||
"uploadSuccess": "Upload successful!",
|
||||
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||
"contentCantEmpty": "Content cannot be empty",
|
||||
"features": "Features",
|
||||
"unnamed": "Unnamed",
|
||||
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||
"fundEnvelope": "Fund Envelope",
|
||||
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||
"fundEnvelopeSplit": "Split: {}",
|
||||
"fundEnvelopeSplitEvenly": "Evenly",
|
||||
"fundEnvelopeSplitRandomly": "Randomly",
|
||||
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||
"fundEnvelopeStatusCreated": "Created",
|
||||
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||
"fundEnvelopeStatusExpired": "Expired",
|
||||
"fundEnvelopeStatusUnknown": "Unknown",
|
||||
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||
"fundEnvelopeExpiredDaysAgo": {
|
||||
"one": "Expired {} day ago",
|
||||
"other": "Expired {} days ago"
|
||||
},
|
||||
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||
"fundEnvelopeExpiresInHours": {
|
||||
"one": "Expires in {} hour",
|
||||
"other": "Expires in {} hours"
|
||||
},
|
||||
"fundEnvelopeExpiresInDays": {
|
||||
"one": "Expires in {} day",
|
||||
"other": "Expires in {} days"
|
||||
},
|
||||
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||
"fundEnvelopeUnknownUser": "Unknown User",
|
||||
"deleteSite": "Delete Site",
|
||||
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||
"siteDeletedSuccess": "Site deleted successfully",
|
||||
"siteSlug": "Slug",
|
||||
"siteSlugHint": "my-site",
|
||||
"siteSlugRequired": "Please enter a slug",
|
||||
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||
"siteName": "Site Name",
|
||||
"siteNameHint": "My Publication Site",
|
||||
"siteNameRequired": "Please enter a site name",
|
||||
"siteMode": "Mode",
|
||||
"siteModeFullyManaged": "Fully Managed",
|
||||
"siteModeSelfManaged": "Self-Managed",
|
||||
"editPublicationSite": "Edit Publication Site",
|
||||
"deletePublicationSite": "Delete Publication Site",
|
||||
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||
"newPublicationSite": "New Publication Site",
|
||||
"siteDetails": "Site Details",
|
||||
"siteInformation": "Site Information",
|
||||
"siteDomain": "Domain",
|
||||
"siteCreated": "Created",
|
||||
"siteUpdated": "Updated",
|
||||
"failedToLoadSite": "Failed to load site",
|
||||
"sitePages": "Pages",
|
||||
"noPagesYet": "No pages yet",
|
||||
"createFirstPage": "Create your first page to get started",
|
||||
"failedToLoadPages": "Failed to load pages",
|
||||
"fileManagement": "File Management",
|
||||
"siteFiles": "Files",
|
||||
"siteFolder": "Folder",
|
||||
"siteRoot": "Root",
|
||||
"noFilesUploadedYet": "No files uploaded yet",
|
||||
"uploadFirstFile": "Upload your first file to get started",
|
||||
"failedToLoadFiles": "Failed to load files",
|
||||
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||
"fileActions": "File Actions",
|
||||
"purgeFiles": "Purge Files",
|
||||
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||
"deploySite": "Deploy Site",
|
||||
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||
"confirmPurge": "Confirm Purge",
|
||||
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||
"purgeAllFiles": "Purge All Files",
|
||||
"allFilesPurgedSuccess": "All files purged successfully",
|
||||
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||
"siteDeployedSuccess": "Site deployed successfully",
|
||||
"failedToDeploySite": "Failed to deploy site: {}",
|
||||
"createPage": "Create Page",
|
||||
"editPage": "Edit Page",
|
||||
"pageType": "Page Type",
|
||||
"htmlPage": "HTML Page",
|
||||
"redirectPage": "Redirect Page",
|
||||
"pageTypeRequired": "Please select a page type",
|
||||
"pagePath": "Page Path",
|
||||
"pagePathHint": "/about, /contact, etc.",
|
||||
"pagePathRequired": "Please enter a page path",
|
||||
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||
"pageTitle": "Page Title",
|
||||
"pageTitleHint": "About Us, Contact, etc.",
|
||||
"pageTitleRequired": "Please enter a page title",
|
||||
"pageContentHtml": "Page Content (HTML)",
|
||||
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||
"pageContentRequired": "Please enter HTML content for the page",
|
||||
"redirectTarget": "Redirect Target",
|
||||
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||
"redirectTargetRequired": "Please enter a redirect target",
|
||||
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||
"deletePage": "Delete Page",
|
||||
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||
"savePage": "Save Page",
|
||||
"pageCreatedSuccess": "Page created successfully",
|
||||
"pageUpdatedSuccess": "Page updated successfully",
|
||||
"pageDeletedSuccess": "Page deleted successfully",
|
||||
"uploadFiles": "Upload Files",
|
||||
"uploadPath": "Upload Path",
|
||||
"uploadPathHint": "/ (root) or /assets/images/",
|
||||
"uploadPathRequired": "Please enter an upload path",
|
||||
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||
"percentCompleted": "{}% completed",
|
||||
"filesToUpload": "{} files to upload",
|
||||
"fileSizeKb": "Size: {} KB",
|
||||
"uploadingEllipsis": "Uploading...",
|
||||
"uploadFilesCount": {
|
||||
"one": "Upload {} File",
|
||||
"other": "Upload {} Files"
|
||||
},
|
||||
"allUploadsCompleted": "All uploads completed",
|
||||
"someUploadsFailed": "Some uploads failed",
|
||||
"uploadingInProgress": "Uploading in progress",
|
||||
"readyToUpload": "Ready to upload",
|
||||
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||
"dropToShare": "Drop to share"
|
||||
}
|
||||
BIN
assets/icons/icon-tray.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/images/oidc/spotify.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
1
assets/images/oidc/steam.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="2471" height="2500" viewBox="0 0 256 259" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M127.779 0C60.42 0 5.24 52.412 0 119.014l68.724 28.674a35.812 35.812 0 0 1 20.426-6.366c.682 0 1.356.019 2.02.056l30.566-44.71v-.626c0-26.903 21.69-48.796 48.353-48.796 26.662 0 48.352 21.893 48.352 48.796 0 26.902-21.69 48.804-48.352 48.804-.37 0-.73-.009-1.098-.018l-43.593 31.377c.028.582.046 1.163.046 1.735 0 20.204-16.283 36.636-36.294 36.636-17.566 0-32.263-12.658-35.584-29.412L4.41 164.654c15.223 54.313 64.673 94.132 123.369 94.132 70.818 0 128.221-57.938 128.221-129.393C256 57.93 198.597 0 127.779 0zM80.352 196.332l-15.749-6.568c2.787 5.867 7.621 10.775 14.033 13.47 13.857 5.83 29.836-.803 35.612-14.799a27.555 27.555 0 0 0 .046-21.035c-2.768-6.79-7.999-12.086-14.706-14.909-6.67-2.795-13.811-2.694-20.085-.304l16.275 6.79c10.222 4.3 15.056 16.145 10.794 26.46-4.253 10.314-15.998 15.195-26.22 10.895zm121.957-100.29c0-17.925-14.457-32.52-32.217-32.52-17.769 0-32.226 14.595-32.226 32.52 0 17.926 14.457 32.512 32.226 32.512 17.76 0 32.217-14.586 32.217-32.512zm-56.37-.055c0-13.488 10.84-24.42 24.2-24.42 13.368 0 24.208 10.932 24.208 24.42 0 13.488-10.84 24.421-24.209 24.421-13.359 0-24.2-10.933-24.2-24.42z" fill="#1A1918"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,6 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- drift: true
|
||||
- provider: true
|
||||
- shared_preferences: true
|
||||
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
15
ios/Podfile
@@ -1,4 +1,3 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '15.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
@@ -32,6 +31,8 @@ target 'Runner' do
|
||||
use_modular_headers!
|
||||
|
||||
pod 'Alamofire'
|
||||
pod 'Kingfisher', '~> 8.0'
|
||||
pod 'KingfisherWebP'
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
|
||||
@@ -41,8 +42,6 @@ target 'Runner' do
|
||||
|
||||
target 'SolianNotificationService' do
|
||||
inherit! :search_paths
|
||||
pod 'Kingfisher', '~> 8.0'
|
||||
pod 'Alamofire'
|
||||
end
|
||||
|
||||
target 'SolianShareExtension' do
|
||||
@@ -50,6 +49,16 @@ target 'Runner' do
|
||||
end
|
||||
end
|
||||
|
||||
target 'Solian Watch App' do
|
||||
platform :watchos, '11.0'
|
||||
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
pod 'Kingfisher', '~> 8.0'
|
||||
pod 'KingfisherWebP'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
|
||||
215
ios/Podfile.lock
@@ -42,83 +42,83 @@ PODS:
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/CoreOnly (12.2.0):
|
||||
- FirebaseCore (~> 12.2.0)
|
||||
- Firebase/Crashlytics (12.2.0):
|
||||
- Firebase/CoreOnly (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- Firebase/Crashlytics (12.4.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseCrashlytics (~> 12.2.0)
|
||||
- Firebase/Messaging (12.2.0):
|
||||
- FirebaseCrashlytics (~> 12.4.0)
|
||||
- Firebase/Messaging (12.4.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.2.0)
|
||||
- firebase_analytics (12.0.2):
|
||||
- FirebaseMessaging (~> 12.4.0)
|
||||
- firebase_analytics (12.0.4):
|
||||
- firebase_core
|
||||
- FirebaseAnalytics (= 12.2.0)
|
||||
- FirebaseAnalytics (= 12.4.0)
|
||||
- Flutter
|
||||
- firebase_core (4.1.1):
|
||||
- Firebase/CoreOnly (= 12.2.0)
|
||||
- firebase_core (4.2.1):
|
||||
- Firebase/CoreOnly (= 12.4.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (5.0.2):
|
||||
- Firebase/Crashlytics (= 12.2.0)
|
||||
- firebase_crashlytics (5.0.5):
|
||||
- Firebase/Crashlytics (= 12.4.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_messaging (16.0.2):
|
||||
- Firebase/Messaging (= 12.2.0)
|
||||
- firebase_messaging (16.0.4):
|
||||
- Firebase/Messaging (= 12.4.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (12.2.0):
|
||||
- FirebaseAnalytics/Default (= 12.2.0)
|
||||
- FirebaseCore (~> 12.2.0)
|
||||
- FirebaseInstallations (~> 12.2.0)
|
||||
- FirebaseAnalytics (12.4.0):
|
||||
- FirebaseAnalytics/Default (= 12.4.0)
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/Default (12.2.0):
|
||||
- FirebaseCore (~> 12.2.0)
|
||||
- FirebaseInstallations (~> 12.2.0)
|
||||
- GoogleAppMeasurement/Default (= 12.2.0)
|
||||
- FirebaseAnalytics/Default (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- GoogleAppMeasurement/Default (= 12.4.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (12.2.0):
|
||||
- FirebaseCoreInternal (~> 12.2.0)
|
||||
- FirebaseCore (12.4.0):
|
||||
- FirebaseCoreInternal (~> 12.4.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- FirebaseCoreExtension (12.2.0):
|
||||
- FirebaseCore (~> 12.2.0)
|
||||
- FirebaseCoreInternal (12.2.0):
|
||||
- FirebaseCoreExtension (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseCoreInternal (12.4.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- FirebaseCrashlytics (12.2.0):
|
||||
- FirebaseCore (~> 12.2.0)
|
||||
- FirebaseInstallations (~> 12.2.0)
|
||||
- FirebaseRemoteConfigInterop (~> 12.2.0)
|
||||
- FirebaseSessions (~> 12.2.0)
|
||||
- FirebaseCrashlytics (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- FirebaseRemoteConfigInterop (~> 12.4.0)
|
||||
- FirebaseSessions (~> 12.4.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseInstallations (12.2.0):
|
||||
- FirebaseCore (~> 12.2.0)
|
||||
- FirebaseInstallations (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (12.2.0):
|
||||
- FirebaseCore (~> 12.2.0)
|
||||
- FirebaseInstallations (~> 12.2.0)
|
||||
- FirebaseMessaging (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Reachability (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseRemoteConfigInterop (12.2.0)
|
||||
- FirebaseSessions (12.2.0):
|
||||
- FirebaseCore (~> 12.2.0)
|
||||
- FirebaseCoreExtension (~> 12.2.0)
|
||||
- FirebaseInstallations (~> 12.2.0)
|
||||
- FirebaseRemoteConfigInterop (12.4.0)
|
||||
- FirebaseSessions (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseCoreExtension (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
@@ -140,42 +140,41 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_platform_alert (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- flutter_timezone (0.0.1):
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- KeychainAccess
|
||||
- flutter_webrtc (1.2.0):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 137.7151.04)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAdsOnDeviceConversion (2.3.0):
|
||||
- GoogleAdsOnDeviceConversion (3.1.0):
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Core (12.2.0):
|
||||
- GoogleAppMeasurement/Core (12.4.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Default (12.2.0):
|
||||
- GoogleAdsOnDeviceConversion (= 2.3.0)
|
||||
- GoogleAppMeasurement/Core (= 12.2.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (= 12.2.0)
|
||||
- GoogleAppMeasurement/Default (12.4.0):
|
||||
- GoogleAdsOnDeviceConversion (~> 3.1.0)
|
||||
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (= 12.4.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (12.2.0):
|
||||
- GoogleAppMeasurement/Core (= 12.2.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (12.4.0):
|
||||
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
@@ -215,8 +214,24 @@ PODS:
|
||||
- Flutter
|
||||
- irondash_engine_context (0.0.1):
|
||||
- Flutter
|
||||
- Kingfisher (8.5.0)
|
||||
- livekit_client (2.5.0):
|
||||
- KeychainAccess (4.2.2)
|
||||
- Kingfisher (8.6.2)
|
||||
- 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.4):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 137.7151.04)
|
||||
@@ -247,14 +262,15 @@ PODS:
|
||||
- PromisesObjC (2.4.0)
|
||||
- PromisesSwift (2.4.0):
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- protocol_handler_ios (0.0.1):
|
||||
- Flutter
|
||||
- receive_sharing_intent (1.8.1):
|
||||
- Flutter
|
||||
- record_ios (1.1.0):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- SDWebImage (5.21.2):
|
||||
- SDWebImage/Core (= 5.21.2)
|
||||
- SDWebImage/Core (5.21.2)
|
||||
- SDWebImage (5.21.3):
|
||||
- SDWebImage/Core (= 5.21.3)
|
||||
- SDWebImage/Core (5.21.3)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -293,9 +309,9 @@ PODS:
|
||||
- super_native_extensions (0.0.1):
|
||||
- Flutter
|
||||
- SwiftyGif (5.4.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- syncfusion_flutter_pdfviewer (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
@@ -318,7 +334,6 @@ DEPENDENCIES:
|
||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
@@ -327,6 +342,7 @@ DEPENDENCIES:
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
- Kingfisher (~> 8.0)
|
||||
- KingfisherWebP
|
||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
@@ -336,6 +352,7 @@ DEPENDENCIES:
|
||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
@@ -344,8 +361,8 @@ DEPENDENCIES:
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- 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`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -367,12 +384,14 @@ SPEC REPOS:
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- KeychainAccess
|
||||
- Kingfisher
|
||||
- KingfisherWebP
|
||||
- libwebp
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- SAMKeychain
|
||||
- SDWebImage
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
@@ -409,8 +428,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_platform_alert:
|
||||
:path: ".symlinks/plugins/flutter_platform_alert/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_timezone:
|
||||
@@ -443,6 +460,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
pointer_interceptor_ios:
|
||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||
protocol_handler_ios:
|
||||
:path: ".symlinks/plugins/protocol_handler_ios/ios"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
record_ios:
|
||||
@@ -459,10 +478,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||
super_native_extensions:
|
||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||
syncfusion_flutter_pdfviewer:
|
||||
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
@@ -475,40 +494,42 @@ SPEC CHECKSUMS:
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
|
||||
firebase_analytics: 8c78ce6224e0623152379d6cc7ef3d9098477b7e
|
||||
firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d
|
||||
firebase_crashlytics: e55dcf895eed0dd87c447dd5aff8db7f1bb8bbdb
|
||||
firebase_messaging: 38c66c1184695b0c87abe51d40fc590718abed1a
|
||||
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
|
||||
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
|
||||
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
|
||||
FirebaseCoreInternal: 56ea29f3dad2894f81b060f706f9d53509b6ed3b
|
||||
FirebaseCrashlytics: f83cbf176d5c637ade108c0aacf1ccbd5ec499bf
|
||||
FirebaseInstallations: 3e884b01feabdf67582a80f3250425a00979b4ed
|
||||
FirebaseMessaging: 43ec73bbfedd0c385a849bb91593ab4ad4b9e48e
|
||||
FirebaseRemoteConfigInterop: 0896fd52ab72586a355c8f389ff85aaa9e5375e1
|
||||
FirebaseSessions: f4692789e770bec66ce17d772c0e9561c4f11737
|
||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
||||
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||
firebase_crashlytics: c039028126cb45e32f4c217aa392408b0963d081
|
||||
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
||||
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6
|
||||
FirebaseCrashlytics: a6ece278a837c7e88de2d9b5da0a3542f2342395
|
||||
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2
|
||||
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5
|
||||
FirebaseRemoteConfigInterop: 1e31ec72b89c9924367c59bfb5ec9ab60d1d6766
|
||||
FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe
|
||||
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
|
||||
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||
Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
|
||||
livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4
|
||||
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||
Kingfisher: 23d18f54677d973b713e54ce6a8f5eef6e7056ba
|
||||
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
livekit_client: 53ca658779b78710fb458cccee28b53a13356c15
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
@@ -517,27 +538,27 @@ SPEC CHECKSUMS:
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
|
||||
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||
|
||||
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f
|
||||
PODFILE CHECKSUM: 585198f58dca90ac6492607c83a8d17045ab3852
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
5D8143680678FCD1D1827271 /* Pods_Solian_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */; };
|
||||
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
|
||||
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, ); }; };
|
||||
@@ -58,6 +60,17 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
7310A7DE2EB10963002C0FD3 /* Embed Watch Content */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 12;
|
||||
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */,
|
||||
);
|
||||
name = "Embed Watch Content";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -84,6 +97,8 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0ECC3D56D018DD87FC342699 /* Pods-Solian Watch App.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Solian Watch App.profile.xcconfig"; path = "Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.profile.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@@ -91,15 +106,18 @@
|
||||
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
1C14F71D23E4371602065522 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
2440CEDEAAD6D51FDA95FA62 /* Pods-Solian Watch App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Solian Watch App.release.xcconfig"; path = "Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.release.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Solian Watch App.debug.xcconfig"; path = "Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Solian Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
|
||||
@@ -111,6 +129,7 @@
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.debug.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -120,10 +139,12 @@
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.release.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.release.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Solian_Watch_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
F6D834CA86410B09796B312B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.debug.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -162,6 +183,13 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = "Solian Watch App";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
73268D272DEB012A0076E970 /* Services */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
@@ -205,6 +233,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7310A7D12EB10962002C0FD3 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5D8143680678FCD1D1827271 /* Pods_Solian_Watch_App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73ACDFA82E3D0E6100B63535 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -258,6 +294,7 @@
|
||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
|
||||
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
|
||||
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
|
||||
C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -280,6 +317,12 @@
|
||||
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */,
|
||||
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */,
|
||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */,
|
||||
86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */,
|
||||
A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */,
|
||||
103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */,
|
||||
31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */,
|
||||
2440CEDEAAD6D51FDA95FA62 /* Pods-Solian Watch App.release.xcconfig */,
|
||||
0ECC3D56D018DD87FC342699 /* Pods-Solian Watch App.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -303,6 +346,7 @@
|
||||
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
||||
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
||||
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
91E124CE95BCB4DCD890160D /* Pods */,
|
||||
@@ -319,6 +363,7 @@
|
||||
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
|
||||
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
|
||||
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -363,6 +408,28 @@
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
7310A7D32EB10962002C0FD3 /* Solian Watch App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */;
|
||||
buildPhases = (
|
||||
DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */,
|
||||
7310A7D02EB10962002C0FD3 /* Sources */,
|
||||
7310A7D12EB10962002C0FD3 /* Frameworks */,
|
||||
7310A7D22EB10962002C0FD3 /* Resources */,
|
||||
E29ECA5954168075BDB000DC /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */,
|
||||
);
|
||||
name = "Solian Watch App";
|
||||
productName = "WatchRunner Watch App";
|
||||
productReference = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */;
|
||||
@@ -434,6 +501,7 @@
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
|
||||
7310A7DE2EB10963002C0FD3 /* Embed Watch Content */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
@@ -463,7 +531,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1640;
|
||||
LastSwiftUpdateCheck = 2600;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
@@ -471,6 +539,9 @@
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
7310A7D32EB10962002C0FD3 = {
|
||||
CreatedOnToolsVersion = 26.0.1;
|
||||
};
|
||||
73ACDFAA2E3D0E6100B63535 = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
};
|
||||
@@ -504,6 +575,7 @@
|
||||
73CDD6792DEC00480059D95D /* SolianNotificationService */,
|
||||
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
||||
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||
7310A7D32EB10962002C0FD3 /* Solian Watch App */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -516,6 +588,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7310A7D22EB10962002C0FD3 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73ACDFA92E3D0E6100B63535 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -683,6 +762,45 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Solian Watch App-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E29ECA5954168075BDB000DC /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -734,6 +852,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7310A7D02EB10962002C0FD3 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73ACDFA72E3D0E6100B63535 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -873,6 +998,7 @@
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
EXCLUDED_SOURCE_FILE_NAMES = "";
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
@@ -883,10 +1009,12 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
@@ -894,6 +1022,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -902,6 +1031,8 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -913,6 +1044,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -921,6 +1053,8 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
@@ -930,6 +1064,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -938,11 +1073,162 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
7310A7E02EB10963002C0FD3 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian;
|
||||
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7310A7E12EB10963002C0FD3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 2440CEDEAAD6D51FDA95FA62 /* Pods-Solian Watch App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian;
|
||||
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "watchsimulator watchos";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
7310A7E22EB10963002C0FD3 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 0ECC3D56D018DD87FC342699 /* Pods-Solian Watch App.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian;
|
||||
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "watchsimulator watchos";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
73ACDFC42E3D0E6100B63535 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -976,6 +1262,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -1016,6 +1303,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -1054,6 +1342,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -1095,6 +1384,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
@@ -1138,6 +1428,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -1179,6 +1470,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -1428,6 +1720,7 @@
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
EXCLUDED_SOURCE_FILE_NAMES = "";
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
@@ -1443,6 +1736,7 @@
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -1457,6 +1751,7 @@
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
EXCLUDED_SOURCE_FILE_NAMES = "";
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
@@ -1465,12 +1760,15 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -1487,6 +1785,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7310A7E02EB10963002C0FD3 /* Debug */,
|
||||
7310A7E12EB10963002C0FD3 /* Release */,
|
||||
7310A7E22EB10963002C0FD3 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -20,6 +20,20 @@
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
|
||||
BuildableName = "Solian Watch App.app"
|
||||
BlueprintName = "Solian Watch App"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import WatchConnectivity
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
let notifyDelegate = NotifyDelegate()
|
||||
private static var sharedWatchConnectivityService: WatchConnectivityService?
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
@@ -12,7 +14,7 @@ import UIKit
|
||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||
|
||||
let replyableMessageCategory = UNNotificationCategory(
|
||||
identifier: "REPLYABLE_MESSAGE",
|
||||
identifier: "CHAT_MESSAGE",
|
||||
actions: [
|
||||
UNTextInputNotificationAction(
|
||||
identifier: "reply_action",
|
||||
@@ -23,11 +25,85 @@ import UIKit
|
||||
intentIdentifiers: [],
|
||||
options: []
|
||||
)
|
||||
|
||||
UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
|
||||
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
|
||||
// Always initialize and retain a strong reference
|
||||
if WCSession.isSupported() {
|
||||
AppDelegate.sharedWatchConnectivityService = WatchConnectivityService.shared
|
||||
} else {
|
||||
print("[iOS] WCSession not supported on this device.")
|
||||
}
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
|
||||
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
||||
static let shared = WatchConnectivityService()
|
||||
private let session: WCSession = .default
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
print("[iOS] Activating WCSession...")
|
||||
session.delegate = self
|
||||
session.activate()
|
||||
}
|
||||
|
||||
// MARK: - WCSessionDelegate
|
||||
|
||||
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||
if let error = error {
|
||||
print("[iOS] WCSession activation failed: \(error.localizedDescription)")
|
||||
} else {
|
||||
print("[iOS] WCSession activated with state: \(activationState.rawValue)")
|
||||
if activationState == .activated {
|
||||
sendDataToWatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sessionDidBecomeInactive(_ session: WCSession) {}
|
||||
|
||||
func sessionDidDeactivate(_ session: WCSession) {
|
||||
session.activate()
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
|
||||
print("[iOS] Received message: \(message)")
|
||||
if let request = message["request"] as? String, request == "data" {
|
||||
let token = UserDefaults.standard.getFlutterToken()
|
||||
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||
|
||||
var data: [String: Any] = ["serverUrl": serverUrl ?? ""]
|
||||
if let token = token {
|
||||
data["token"] = token
|
||||
}
|
||||
|
||||
print("[iOS] Replying with data: \(data)")
|
||||
replyHandler(data)
|
||||
}
|
||||
}
|
||||
|
||||
func sendDataToWatch() {
|
||||
guard session.activationState == .activated else {
|
||||
return
|
||||
}
|
||||
|
||||
let token = UserDefaults.standard.getFlutterToken()
|
||||
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||
|
||||
var data: [String: Any] = ["serverUrl": serverUrl ?? ""]
|
||||
if let token = token {
|
||||
data["token"] = token
|
||||
}
|
||||
|
||||
do {
|
||||
try session.updateApplicationContext(data)
|
||||
print("[iOS] Sent application context: \(data)")
|
||||
} catch {
|
||||
print("[iOS] Failed to send application context: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,334 @@
|
||||
{"images":[{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@2x.png","scale":"2x","platform":"ios"},{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@3x.png","scale":"3x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@2x.png","scale":"2x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@3x.png","scale":"3x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@2x.png","scale":"2x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@3x.png","scale":"3x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@2x.png","scale":"2x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@3x.png","scale":"3x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@2x.png","scale":"2x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@3x.png","scale":"3x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@2x.png","scale":"2x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@3x.png","scale":"3x","platform":"ios"},{"size":"68x68","idiom":"universal","filename":"Icon-App-68x68@2x.png","scale":"2x","platform":"ios"},{"size":"76x76","idiom":"universal","filename":"Icon-App-76x76@2x.png","scale":"2x","platform":"ios"},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x","platform":"ios"},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-1024x1024@1x.png","scale":"1x","platform":"ios"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"68x68","idiom":"universal","filename":"Icon-App-Dark-68x68@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"76x76","idiom":"universal","filename":"Icon-App-Dark-76x76@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-Dark-83.5x83.5@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-Dark-1024x1024@1x.png","scale":"1x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]}],"info":{"version":1,"author":"xcode"}}
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-38x38@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-38x38@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-64x64@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-64x64@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-68x68@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "68x68"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-20x20@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-20x20@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-29x29@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-29x29@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-38x38@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-38x38@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-40x40@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-40x40@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-60x60@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-60x60@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-64x64@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-64x64@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-68x68@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "68x68"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-76x76@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-83.5x83.5@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Icon-App-Dark-1024x1024@1x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 762 B |
@@ -36,6 +36,16 @@
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
@@ -50,7 +60,8 @@
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue
|
||||
your action quickly.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
@@ -87,6 +98,8 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
func getAttachmentUrl(for identifier: String) -> String {
|
||||
let serverBaseUrl = "https://api.solian.app"
|
||||
let serverBaseUrl = UserDefaults.standard.getServerUrl()
|
||||
|
||||
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
|
||||
}
|
||||
|
||||
@@ -26,6 +26,6 @@ extension UserDefaults {
|
||||
}
|
||||
|
||||
func getServerUrl(forKey key: String = "app_server_url") -> String {
|
||||
return self.getFlutterValue(forKey: key) ?? "https://nt.solian.app"
|
||||
return self.getFlutterValue(forKey: key) ?? "https://api.solian.app"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/Solian Watch App/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
ios/Solian 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/Solian Watch App/Assets.xcassets/Logo.imageset/icon.png
vendored
Normal file
|
After Width: | Height: | Size: 70 KiB |
58
ios/Solian Watch App/ContentView.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Placeholder Implementations for Preview ---
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
88
ios/Solian 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/Solian Watch App/Models/Models.swift
Normal file
@@ -0,0 +1,365 @@
|
||||
// Models.swift
|
||||
// WatchRunner Watch App
|
||||
//
|
||||
// Created by LittleSheep on 2025/10/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Models
|
||||
|
||||
struct AppToken: Codable {
|
||||
let token: String
|
||||
}
|
||||
|
||||
struct SnActivity: Codable, Identifiable {
|
||||
let id: String
|
||||
let type: String
|
||||
let data: ActivityData?
|
||||
let createdAt: Date
|
||||
}
|
||||
|
||||
enum ActivityData: Codable {
|
||||
case post(SnPost)
|
||||
case discovery(DiscoveryData)
|
||||
case unknown
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let post = try? container.decode(SnPost.self) {
|
||||
self = .post(post)
|
||||
return
|
||||
}
|
||||
if let discoveryData = try? container.decode(DiscoveryData.self) {
|
||||
self = .discovery(discoveryData)
|
||||
return
|
||||
}
|
||||
self = .unknown
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
// Not needed for decoding
|
||||
}
|
||||
}
|
||||
|
||||
struct SnPost: Codable, Identifiable {
|
||||
let id: String
|
||||
let title: String?
|
||||
let content: String?
|
||||
let publisher: SnPublisher
|
||||
let attachments: [SnCloudFile]
|
||||
let tags: [SnPostTag]
|
||||
}
|
||||
|
||||
struct DiscoveryData: Codable {
|
||||
let items: [DiscoveryItem]
|
||||
}
|
||||
|
||||
struct DiscoveryItem: Codable, Identifiable {
|
||||
var id = UUID()
|
||||
let type: String
|
||||
let data: DiscoveryItemData
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type, data
|
||||
}
|
||||
}
|
||||
|
||||
enum DiscoveryItemData: Codable {
|
||||
case realm(SnRealm)
|
||||
case publisher(SnPublisher)
|
||||
case article(SnWebArticle)
|
||||
case unknown
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let realm = try? container.decode(SnRealm.self) {
|
||||
self = .realm(realm)
|
||||
return
|
||||
}
|
||||
if let publisher = try? container.decode(SnPublisher.self) {
|
||||
self = .publisher(publisher)
|
||||
return
|
||||
}
|
||||
if let article = try? container.decode(SnWebArticle.self) {
|
||||
self = .article(article)
|
||||
return
|
||||
}
|
||||
self = .unknown
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
// Not needed for decoding
|
||||
}
|
||||
}
|
||||
|
||||
struct SnRealm: Codable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let description: String?
|
||||
}
|
||||
|
||||
struct SnPublisher: Codable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let nick: String?
|
||||
let description: String?
|
||||
let picture: SnCloudFile?
|
||||
}
|
||||
|
||||
struct SnCloudFile: Codable, Identifiable {
|
||||
let id: String
|
||||
let mimeType: String?
|
||||
}
|
||||
|
||||
struct SnPostTag: Codable, Identifiable {
|
||||
let id: String
|
||||
let slug: String
|
||||
let name: String?
|
||||
}
|
||||
|
||||
struct SnWebArticle: Codable, Identifiable {
|
||||
let id: String
|
||||
let title: String
|
||||
let url: String
|
||||
}
|
||||
|
||||
struct SnNotification: Codable, Identifiable {
|
||||
let id: String
|
||||
let topic: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let content: String
|
||||
let meta: [String: AnyCodable]?
|
||||
let priority: Int
|
||||
let viewedAt: Date?
|
||||
let accountId: String
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let deletedAt: Date?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case topic
|
||||
case title
|
||||
case subtitle
|
||||
case content
|
||||
case meta
|
||||
case priority
|
||||
case viewedAt = "viewedAt"
|
||||
case accountId = "accountId"
|
||||
case createdAt = "createdAt"
|
||||
case updatedAt = "updatedAt"
|
||||
case deletedAt = "deletedAt"
|
||||
}
|
||||
}
|
||||
|
||||
struct AnyCodable: Codable {
|
||||
let value: Any
|
||||
|
||||
init(_ value: Any) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let intValue = try? container.decode(Int.self) {
|
||||
value = intValue
|
||||
} else if let doubleValue = try? container.decode(Double.self) {
|
||||
value = doubleValue
|
||||
} else if let boolValue = try? container.decode(Bool.self) {
|
||||
value = boolValue
|
||||
} else if let stringValue = try? container.decode(String.self) {
|
||||
value = stringValue
|
||||
} else if let arrayValue = try? container.decode([AnyCodable].self) {
|
||||
value = arrayValue
|
||||
} else if let dictValue = try? container.decode([String: AnyCodable].self) {
|
||||
value = dictValue
|
||||
} else {
|
||||
value = NSNull()
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch value {
|
||||
case let intValue as Int:
|
||||
try container.encode(intValue)
|
||||
case let doubleValue as Double:
|
||||
try container.encode(doubleValue)
|
||||
case let boolValue as Bool:
|
||||
try container.encode(boolValue)
|
||||
case let stringValue as String:
|
||||
try container.encode(stringValue)
|
||||
case let arrayValue as [AnyCodable]:
|
||||
try container.encode(arrayValue)
|
||||
case let dictValue as [String: AnyCodable]:
|
||||
try container.encode(dictValue)
|
||||
default:
|
||||
try container.encodeNil()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationResponse {
|
||||
let notifications: [SnNotification]
|
||||
let total: Int
|
||||
let hasMore: Bool
|
||||
}
|
||||
|
||||
struct ActivityResponse {
|
||||
let activities: [SnActivity]
|
||||
let hasMore: Bool
|
||||
let nextCursor: String?
|
||||
}
|
||||
|
||||
struct SnAccount: Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
let nick: String
|
||||
let profile: SnUserProfile
|
||||
let createdAt: Date
|
||||
}
|
||||
|
||||
struct SnUserProfile: Codable {
|
||||
let bio: String?
|
||||
let picture: SnCloudFile?
|
||||
let background: SnCloudFile?
|
||||
let level: Int
|
||||
let experience: Int
|
||||
let levelingProgress: Double
|
||||
}
|
||||
|
||||
struct SnAccountStatus: Codable {
|
||||
let id: String
|
||||
let attitude: Int
|
||||
let isOnline: Bool
|
||||
let isInvisible: Bool
|
||||
let isNotDisturb: Bool
|
||||
let isCustomized: Bool
|
||||
let label: String
|
||||
let meta: [String: AnyCodable]?
|
||||
let clearedAt: Date?
|
||||
let accountId: String
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let deletedAt: Date?
|
||||
}
|
||||
|
||||
// MARK: - Chat Models
|
||||
|
||||
struct SnChatRoom: Codable, Identifiable {
|
||||
let id: String
|
||||
let name: String?
|
||||
let description: String?
|
||||
let type: Int
|
||||
let isPublic: Bool
|
||||
let isCommunity: Bool
|
||||
let picture: SnCloudFile?
|
||||
let background: SnCloudFile?
|
||||
let realmId: String?
|
||||
let realm: SnRealm?
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let deletedAt: Date?
|
||||
let members: [SnChatMember]?
|
||||
}
|
||||
|
||||
struct SnChatMessage: Codable, Identifiable {
|
||||
let id: String
|
||||
let type: String
|
||||
let content: String?
|
||||
let nonce: String?
|
||||
let meta: [String: AnyCodable]
|
||||
let membersMentioned: [String]?
|
||||
let editedAt: Date?
|
||||
let attachments: [SnCloudFile]
|
||||
let reactions: [SnChatReaction]
|
||||
let repliedMessageId: String?
|
||||
let forwardedMessageId: String?
|
||||
let senderId: String
|
||||
let sender: SnChatMember
|
||||
let chatRoomId: String
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let deletedAt: Date?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, type, content, nonce, meta, membersMentioned, editedAt, attachments, reactions, repliedMessageId, forwardedMessageId, senderId, sender, chatRoomId, createdAt, updatedAt, deletedAt
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
type = try container.decode(String.self, forKey: .type)
|
||||
content = try container.decodeIfPresent(String.self, forKey: .content)
|
||||
nonce = try container.decodeIfPresent(String.self, forKey: .nonce)
|
||||
meta = try container.decode([String: AnyCodable].self, forKey: .meta)
|
||||
membersMentioned = try container.decodeIfPresent([String].self, forKey: .membersMentioned) ?? []
|
||||
editedAt = try container.decodeIfPresent(Date.self, forKey: .editedAt)
|
||||
attachments = try container.decode([SnCloudFile].self, forKey: .attachments)
|
||||
reactions = try container.decode([SnChatReaction].self, forKey: .reactions)
|
||||
repliedMessageId = try container.decodeIfPresent(String.self, forKey: .repliedMessageId)
|
||||
forwardedMessageId = try container.decodeIfPresent(String.self, forKey: .forwardedMessageId)
|
||||
senderId = try container.decode(String.self, forKey: .senderId)
|
||||
sender = try container.decode(SnChatMember.self, forKey: .sender)
|
||||
chatRoomId = try container.decode(String.self, forKey: .chatRoomId)
|
||||
createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
|
||||
deletedAt = try container.decodeIfPresent(Date.self, forKey: .deletedAt)
|
||||
}
|
||||
}
|
||||
|
||||
struct SnChatReaction: Codable, Identifiable {
|
||||
let id: String
|
||||
let messageId: String
|
||||
let senderId: String
|
||||
let sender: SnChatMember
|
||||
let symbol: String
|
||||
let attitude: Int
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let deletedAt: Date?
|
||||
}
|
||||
|
||||
struct SnChatMember: Codable, Identifiable {
|
||||
let id: String
|
||||
let chatRoomId: String
|
||||
let chatRoom: SnChatRoom?
|
||||
let accountId: String
|
||||
let account: SnAccount
|
||||
let nick: String?
|
||||
let role: Int
|
||||
let notify: Int
|
||||
let joinedAt: Date?
|
||||
let breakUntil: Date?
|
||||
let timeoutUntil: Date?
|
||||
let isBot: Bool
|
||||
let status: SnAccountStatus?
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let deletedAt: Date?
|
||||
}
|
||||
|
||||
struct SnChatSummary: Codable {
|
||||
let unreadCount: Int
|
||||
let lastMessage: SnChatMessage?
|
||||
}
|
||||
|
||||
struct ChatRoomsResponse {
|
||||
let rooms: [SnChatRoom]
|
||||
}
|
||||
|
||||
struct ChatInvitesResponse {
|
||||
let invites: [SnChatMember]
|
||||
}
|
||||
|
||||
struct MessageSyncResponse: Codable {
|
||||
let messages: [SnChatMessage]
|
||||
let currentTimestamp: Date
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case messages
|
||||
case currentTimestamp = "current_timestamp"
|
||||
}
|
||||
}
|
||||
95
ios/Solian 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()
|
||||
}
|
||||
}
|
||||
643
ios/Solian Watch App/Services/NetworkService.swift
Normal file
@@ -0,0 +1,643 @@
|
||||
//
|
||||
// NetworkService.swift
|
||||
// WatchRunner Watch App
|
||||
//
|
||||
// Created by LittleSheep on 2025/10/29. //
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
// MARK: - WebSocket Data Structures
|
||||
|
||||
enum WebSocketState: Equatable {
|
||||
case connected
|
||||
case connecting
|
||||
case disconnected
|
||||
case serverDown
|
||||
case duplicateDevice
|
||||
case error(String)
|
||||
|
||||
// Equatable conformance
|
||||
static func == (lhs: WebSocketState, rhs: WebSocketState) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.connected, .connected),
|
||||
(.connecting, .connecting),
|
||||
(.disconnected, .disconnected),
|
||||
(.serverDown, .serverDown),
|
||||
(.duplicateDevice, .duplicateDevice):
|
||||
return true
|
||||
case let (.error(a), .error(b)):
|
||||
return a == b
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebSocketPacket {
|
||||
let type: String
|
||||
let data: [String: Any]?
|
||||
let endpoint: String?
|
||||
let errorMessage: String?
|
||||
}
|
||||
|
||||
// MARK: - Network Service
|
||||
|
||||
class NetworkService {
|
||||
private let session: URLSession
|
||||
|
||||
init() {
|
||||
let config = URLSessionConfiguration.ephemeral
|
||||
config.waitsForConnectivity = true
|
||||
session = URLSession(configuration: config)
|
||||
}
|
||||
|
||||
// Add a serial queue for WebSocket operations
|
||||
private let webSocketQueue = DispatchQueue(label: "com.solian.websocketQueue")
|
||||
|
||||
func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> ActivityResponse {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
var components = URLComponents(url: baseURL.appendingPathComponent("/sphere/activities"), resolvingAgainstBaseURL: false)!
|
||||
var queryItems = [URLQueryItem(name: "take", value: "20")]
|
||||
if filter.lowercased() != "explore" {
|
||||
queryItems.append(URLQueryItem(name: "filter", value: filter.lowercased()))
|
||||
}
|
||||
if let cursor = cursor {
|
||||
queryItems.append(URLQueryItem(name: "cursor", value: cursor))
|
||||
}
|
||||
components.queryItems = queryItems
|
||||
|
||||
var request = URLRequest(url: components.url!)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, _) = try await session.data(for: request)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let activities = try decoder.decode([SnActivity].self, from: data)
|
||||
|
||||
let hasMore = (activities.first?.type ?? "empty") != "empty"
|
||||
let nextCursor = activities.isEmpty ? nil : activities.map { $0.createdAt }.min()?.ISO8601Format()
|
||||
|
||||
return ActivityResponse(activities: activities, hasMore: hasMore, nextCursor: nextCursor)
|
||||
}
|
||||
|
||||
func createPost(title: String, content: String, token: String, serverUrl: String) async throws {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/sphere/posts")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let body: [String: Any] = ["title": title, "content": content]
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 {
|
||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||
print("[watchOS] createPost failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func fetchNotifications(offset: Int = 0, take: Int = 20, token: String, serverUrl: String) async throws -> NotificationResponse {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
var components = URLComponents(url: baseURL.appendingPathComponent("/ring/notifications"), resolvingAgainstBaseURL: false)!
|
||||
let queryItems = [URLQueryItem(name: "offset", value: String(offset)), URLQueryItem(name: "take", value: String(take))]
|
||||
components.queryItems = queryItems
|
||||
|
||||
var request = URLRequest(url: components.url!)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let notifications = try decoder.decode([SnNotification].self, from: data)
|
||||
|
||||
let httpResponse = response as? HTTPURLResponse
|
||||
let total = Int(httpResponse?.value(forHTTPHeaderField: "X-Total") ?? "0") ?? 0
|
||||
let hasMore = offset + notifications.count < total
|
||||
|
||||
return NotificationResponse(notifications: notifications, total: total, hasMore: hasMore)
|
||||
}
|
||||
|
||||
func fetchUserProfile(token: String, serverUrl: String) async throws -> SnAccount {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/pass/accounts/me")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, _) = try await session.data(for: request)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
return try decoder.decode(SnAccount.self, from: data)
|
||||
}
|
||||
|
||||
func fetchAccountStatus(token: String, serverUrl: String) async throws -> SnAccountStatus? {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
||||
return nil
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
return try decoder.decode(SnAccountStatus.self, from: data)
|
||||
}
|
||||
|
||||
func createOrUpdateStatus(attitude: Int, isInvisible: Bool, isNotDisturb: Bool, label: String?, token: String, serverUrl: String) async throws -> SnAccountStatus {
|
||||
// Check if there\'s already a customized status
|
||||
let existingStatus = try? await fetchAccountStatus(token: token, serverUrl: serverUrl)
|
||||
let method = (existingStatus?.isCustomized == true) ? "PATCH" : "POST"
|
||||
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
var body: [String: Any] = [
|
||||
"attitude": attitude,
|
||||
"is_invisible": isInvisible,
|
||||
"is_not_disturb": isNotDisturb,
|
||||
]
|
||||
|
||||
if let label = label, !label.isEmpty {
|
||||
body["label"] = label
|
||||
}
|
||||
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 && httpResponse.statusCode != 200 {
|
||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||
print("[watchOS] createOrUpdateStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
return try decoder.decode(SnAccountStatus.self, from: data)
|
||||
}
|
||||
|
||||
func clearStatus(token: String, serverUrl: String) async throws {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "DELETE"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 {
|
||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||
print("[watchOS] clearStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Chat API Methods
|
||||
|
||||
func fetchChatRooms(token: String, serverUrl: String) async throws -> ChatRoomsResponse {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/sphere/chat")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, _) = try await session.data(for: request)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let rooms = try decoder.decode([SnChatRoom].self, from: data)
|
||||
return ChatRoomsResponse(rooms: rooms)
|
||||
}
|
||||
|
||||
func fetchChatRoom(identifier: String, token: String, serverUrl: String) async throws -> SnChatRoom {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/sphere/chat/\(identifier)")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
||||
throw URLError(.resourceUnavailable)
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
return try decoder.decode(SnChatRoom.self, from: data)
|
||||
}
|
||||
|
||||
func fetchChatInvites(token: String, serverUrl: String) async throws -> ChatInvitesResponse {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/sphere/chat/invites")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, _) = try await session.data(for: request)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let invites = try decoder.decode([SnChatMember].self, from: data)
|
||||
return ChatInvitesResponse(invites: invites)
|
||||
}
|
||||
|
||||
func acceptChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/accept")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||
print("[watchOS] acceptChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func declineChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/decline")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||
print("[watchOS] declineChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Message API Methods
|
||||
|
||||
func fetchChatMessages(chatRoomId: String, token: String, serverUrl: String, before: Date? = nil, take: Int = 50) async throws -> [SnChatMessage] {
|
||||
guard let baseURL = URL(string: serverUrl) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
|
||||
// Try a different pattern: /sphere/chat/messages with roomId as query param
|
||||
var components = URLComponents(
|
||||
url: baseURL.appendingPathComponent("/sphere/chat/\(chatRoomId)/messages"),
|
||||
resolvingAgainstBaseURL: false
|
||||
)!
|
||||
var queryItems = [
|
||||
URLQueryItem(name: "take", value: String(take)),
|
||||
]
|
||||
if let before = before {
|
||||
queryItems.append(URLQueryItem(name: "before", value: ISO8601DateFormatter().string(from: before)))
|
||||
}
|
||||
components.queryItems = queryItems
|
||||
|
||||
var request = URLRequest(url: components.url!)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
_ = String(data: data, encoding: .utf8) ?? "Unable to decode response body"
|
||||
|
||||
if httpResponse.statusCode != 200 {
|
||||
print("[watchOS] fetchChatMessages failed with status \(httpResponse.statusCode)")
|
||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||
}
|
||||
}
|
||||
|
||||
// Check if data is empty
|
||||
if data.isEmpty {
|
||||
print("[watchOS] fetchChatMessages received empty response data")
|
||||
return []
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
do {
|
||||
let messages = try decoder.decode([SnChatMessage].self, from: data)
|
||||
print("[watchOS] fetchChatMessages successfully decoded \(messages.count) messages")
|
||||
return messages
|
||||
} catch {
|
||||
print("error: ", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - WebSocket
|
||||
|
||||
private var webSocketTask: URLSessionWebSocketTask?
|
||||
private var heartbeatTimer: Timer?
|
||||
private var reconnectTimer: Timer?
|
||||
private var isDisconnectingManually = false
|
||||
|
||||
private var lastToken: String?
|
||||
private var lastServerUrl: String?
|
||||
|
||||
private var heartbeatAt: Date?
|
||||
var heartbeatDelay: TimeInterval?
|
||||
|
||||
private let connectLock = NSLock()
|
||||
|
||||
private let packetSubject = PassthroughSubject<WebSocketPacket, Error>()
|
||||
private let stateSubject = CurrentValueSubject<WebSocketState, Never>(.disconnected) // Changed to CurrentValueSubject
|
||||
|
||||
private var currentConnectionState: WebSocketState = .disconnected { // New property
|
||||
didSet {
|
||||
// Only send updates if the state has actually changed
|
||||
if oldValue != currentConnectionState {
|
||||
stateSubject.send(currentConnectionState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var packetStream: AnyPublisher<WebSocketPacket, Error> {
|
||||
packetSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var stateStream: AnyPublisher<WebSocketState, Never> {
|
||||
stateSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func connectWebSocket(token: String, serverUrl: String) {
|
||||
webSocketQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.connectLock.lock()
|
||||
defer { self.connectLock.unlock() }
|
||||
|
||||
// Prevent redundant connection attempts
|
||||
if self.currentConnectionState == .connecting || self.currentConnectionState == .connected {
|
||||
print("[WebSocket] Already connecting or connected, ignoring new connect request.")
|
||||
return
|
||||
}
|
||||
|
||||
self.currentConnectionState = .connecting
|
||||
|
||||
// Ensure any existing task is cancelled before starting a new one
|
||||
self.webSocketTask?.cancel(with: .goingAway, reason: nil)
|
||||
self.webSocketTask = nil
|
||||
|
||||
self.isDisconnectingManually = false // Reset this flag for a new connection attempt
|
||||
|
||||
self.lastToken = token
|
||||
self.lastServerUrl = serverUrl
|
||||
|
||||
guard var urlComponents = URLComponents(string: serverUrl) else {
|
||||
self.currentConnectionState = .error("Invalid server URL")
|
||||
return
|
||||
}
|
||||
|
||||
urlComponents.scheme = urlComponents.scheme?.replacingOccurrences(of: "http", with: "ws")
|
||||
urlComponents.path = "/ws"
|
||||
urlComponents.queryItems = [URLQueryItem(name: "deviceAlt", value: "watch")]
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
self.currentConnectionState = .error("Invalid WebSocket URL")
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
print("[WebSocket] Trying connecting to \(url)")
|
||||
|
||||
self.webSocketTask = self.session.webSocketTask(with: request)
|
||||
self.webSocketTask?.resume()
|
||||
|
||||
self.listenForWebSocketMessages()
|
||||
self.scheduleHeartbeat()
|
||||
self.currentConnectionState = .connected
|
||||
}
|
||||
}
|
||||
|
||||
private func listenForWebSocketMessages() {
|
||||
// Ensure webSocketTask is still valid before attempting to receive
|
||||
guard let task = webSocketTask else {
|
||||
print("[WebSocket] listenForWebSocketMessages: webSocketTask is nil, stopping listen.")
|
||||
return
|
||||
}
|
||||
|
||||
task.receive { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
print("[WebSocket] Error in receiving message: \(error)")
|
||||
// Only attempt to reconnect if not manually disconnecting
|
||||
if !self.isDisconnectingManually {
|
||||
self.currentConnectionState = .error(error.localizedDescription)
|
||||
self.scheduleReconnect()
|
||||
} else {
|
||||
// If manually disconnecting, just ensure state is disconnected
|
||||
self.currentConnectionState = .disconnected
|
||||
}
|
||||
case .success(let message):
|
||||
switch message {
|
||||
case .string(let text):
|
||||
self.handleWebSocketMessage(text: text)
|
||||
case .data(let data):
|
||||
if let text = String(data: data, encoding: .utf8) {
|
||||
self.handleWebSocketMessage(text: text)
|
||||
}
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
// Continue listening for next message only if task is still valid
|
||||
if self.webSocketTask === task { // Check if it's the same task
|
||||
self.listenForWebSocketMessages()
|
||||
} else {
|
||||
print("[WebSocket] listenForWebSocketMessages: Task changed, stopping listen for old task.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleWebSocketMessage(text: String) {
|
||||
guard let data = text.data(using: .utf8) else {
|
||||
print("[WebSocket] Could not convert message to data")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||
let type = json["type"] as? String
|
||||
{
|
||||
let packet = WebSocketPacket(
|
||||
type: type,
|
||||
data: json["data"] as? [String: Any],
|
||||
endpoint: json["endpoint"] as? String,
|
||||
errorMessage: json["errorMessage"] as? String
|
||||
)
|
||||
|
||||
print("[WebSocket] Received packet: \(packet.type) \(packet.errorMessage ?? "")")
|
||||
|
||||
if packet.type == "error.dupe" {
|
||||
self.currentConnectionState = .duplicateDevice
|
||||
self.disconnectWebSocket()
|
||||
return
|
||||
}
|
||||
|
||||
if packet.type == "pong" {
|
||||
if let beatAt = self.heartbeatAt {
|
||||
let now = Date()
|
||||
self.heartbeatDelay = now.timeIntervalSince(beatAt)
|
||||
print("[WebSocket] Server respond last heartbeat for \((self.heartbeatDelay ?? 0) * 1000) ms")
|
||||
}
|
||||
}
|
||||
|
||||
self.packetSubject.send(packet)
|
||||
}
|
||||
} catch {
|
||||
print("[WebSocket] Could not parse message json: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleReconnect() {
|
||||
reconnectTimer?.invalidate()
|
||||
reconnectTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||
guard let self = self, let token = self.lastToken, let serverUrl = self.lastServerUrl else { return }
|
||||
print("[WebSocket] Attempting to reconnect...")
|
||||
|
||||
// No need to call disconnectWebSocket here, connectWebSocket will handle cancelling old task
|
||||
self.isDisconnectingManually = false // Reset for the new connection attempt
|
||||
|
||||
self.connectWebSocket(token: token, serverUrl: serverUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleHeartbeat() {
|
||||
heartbeatTimer?.invalidate()
|
||||
heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in
|
||||
self?.beatTheHeart()
|
||||
}
|
||||
}
|
||||
|
||||
private func beatTheHeart() {
|
||||
heartbeatAt = Date()
|
||||
print("[WebSocket] We\'re beating the heart! \(String(describing: self.heartbeatAt))")
|
||||
sendWebSocketMessage(message: "{\"type\":\"ping\"}")
|
||||
}
|
||||
|
||||
func sendWebSocketMessage(message: String) {
|
||||
webSocketTask?.send(.string(message)) { error in
|
||||
if let error = error {
|
||||
print("[WebSocket] Error sending message: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectWebSocket() {
|
||||
isDisconnectingManually = true
|
||||
reconnectTimer?.invalidate()
|
||||
heartbeatTimer?.invalidate()
|
||||
|
||||
// Cancel the task and then nil it out
|
||||
webSocketTask?.cancel(with: .goingAway, reason: nil)
|
||||
webSocketTask = nil // Set to nil immediately after cancelling
|
||||
|
||||
self.currentConnectionState = .disconnected
|
||||
}
|
||||
}
|
||||
58
ios/Solian Watch App/State/AppState.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// AppState.swift
|
||||
// WatchRunner Watch App
|
||||
//
|
||||
// Created by LittleSheep on 2025/10/29.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
// MARK: - App State
|
||||
|
||||
@MainActor
|
||||
class AppState: ObservableObject {
|
||||
@Published var token: String? = nil
|
||||
@Published var serverUrl: String? = nil
|
||||
@Published var isReady = false
|
||||
@Published var errorMessage: String? = nil
|
||||
|
||||
let networkService = NetworkService()
|
||||
private var wcService = WatchConnectivityService()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var hasAttemptedConnection = false
|
||||
|
||||
init() {
|
||||
wcService.$token.combineLatest(wcService.$serverUrl, wcService.$isFetched, wcService.$errorMessage)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] (token: String?, serverUrl: String?, isFetched: Bool?, errorMessage: String?) in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.token = token
|
||||
self.serverUrl = serverUrl
|
||||
self.errorMessage = errorMessage
|
||||
|
||||
if let token = token, let serverUrl = serverUrl, !token.isEmpty, !serverUrl.isEmpty {
|
||||
self.isReady = true
|
||||
// Only connect once when we have valid credentials and tried fetch from phone
|
||||
if !self.hasAttemptedConnection && isFetched == true {
|
||||
self.hasAttemptedConnection = true
|
||||
print("[AppState] Connecting WebSocket to server: \(serverUrl)")
|
||||
self.networkService.connectWebSocket(token: token, serverUrl: serverUrl)
|
||||
}
|
||||
} else {
|
||||
self.isReady = false
|
||||
if self.hasAttemptedConnection {
|
||||
self.hasAttemptedConnection = false
|
||||
// Disconnect WebSocket if token or serverUrl become invalid
|
||||
self.networkService.disconnectWebSocket()
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func requestData() {
|
||||
wcService.requestDataFromPhone()
|
||||
}
|
||||
}
|
||||
113
ios/Solian Watch App/State/WatchConnectivityService.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
import WatchConnectivity
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
class WatchConnectivityService: NSObject, WCSessionDelegate, ObservableObject {
|
||||
@Published var token: String?
|
||||
@Published var serverUrl: String?
|
||||
@Published var isFetched: Bool?
|
||||
@Published var errorMessage: String?
|
||||
|
||||
private let session: WCSession
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let tokenKey = "token"
|
||||
private let serverUrlKey = "serverUrl"
|
||||
|
||||
override init() {
|
||||
self.session = .default
|
||||
super.init()
|
||||
print("[watchOS] Activating WCSession")
|
||||
self.session.delegate = self
|
||||
self.session.activate()
|
||||
|
||||
// Load cached data
|
||||
self.token = userDefaults.string(forKey: tokenKey)
|
||||
self.serverUrl = userDefaults.string(forKey: serverUrlKey)
|
||||
self.isFetched = false
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||
if let error = error {
|
||||
print("[watchOS] WCSession activation failed with error: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
self.errorMessage = "WCSession activation failed: \(error.localizedDescription)"
|
||||
}
|
||||
return
|
||||
}
|
||||
print("[watchOS] WCSession activated with state: \(activationState.rawValue)")
|
||||
if activationState == .activated {
|
||||
requestDataFromPhone()
|
||||
}
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
|
||||
print("[watchOS] Received application context: \(applicationContext)")
|
||||
DispatchQueue.main.async {
|
||||
if let token = applicationContext["token"] as? String {
|
||||
self.token = token
|
||||
self.userDefaults.set(token, forKey: self.tokenKey)
|
||||
}
|
||||
if let serverUrl = applicationContext["serverUrl"] as? String {
|
||||
self.serverUrl = serverUrl
|
||||
self.userDefaults.set(serverUrl, forKey: self.serverUrlKey)
|
||||
}
|
||||
self.isFetched = true
|
||||
self.errorMessage = nil
|
||||
}
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
|
||||
print("[watchOS] Received message: \(message)")
|
||||
DispatchQueue.main.async {
|
||||
if let token = message["token"] as? String {
|
||||
self.token = token
|
||||
self.userDefaults.set(token, forKey: self.tokenKey)
|
||||
}
|
||||
if let serverUrl = message["serverUrl"] as? String {
|
||||
self.serverUrl = serverUrl
|
||||
self.userDefaults.set(serverUrl, forKey: self.serverUrlKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requestDataFromPhone() {
|
||||
// Check if we already have valid data to avoid unnecessary requests
|
||||
if let token = self.token, let serverUrl = self.serverUrl, !token.isEmpty, !serverUrl.isEmpty {
|
||||
print("[watchOS] Skipped fetch - already have valid data")
|
||||
self.isFetched = true
|
||||
return
|
||||
}
|
||||
|
||||
guard session.activationState == .activated else {
|
||||
print("[watchOS] Session not activated yet, state: \(session.activationState.rawValue)")
|
||||
DispatchQueue.main.async {
|
||||
self.errorMessage = "Session not ready yet"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
print("[watchOS] Requesting data from phone")
|
||||
session.sendMessage(["request": "data"]) { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
print("[watchOS] Received reply: \(response)")
|
||||
DispatchQueue.main.async {
|
||||
self.isFetched = true
|
||||
if let token = response["token"] as? String {
|
||||
self.token = token
|
||||
self.userDefaults.set(token, forKey: self.tokenKey)
|
||||
}
|
||||
if let serverUrl = response["serverUrl"] as? String {
|
||||
self.serverUrl = serverUrl
|
||||
self.userDefaults.set(serverUrl, forKey: self.serverUrlKey)
|
||||
}
|
||||
self.errorMessage = nil // Clear any previous errors
|
||||
}
|
||||
} errorHandler: { error in
|
||||
print("[watchOS] sendMessage failed with error: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
self.errorMessage = "Failed to get data from phone: \(error.localizedDescription)"
|
||||
// Don't set isFetched = true on error - allow retry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
ios/Solian 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/Solian 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
|
||||
}
|
||||
}
|
||||
35
ios/Solian Watch App/ViewModels/ComposePostViewModel.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// ComposePostViewModel.swift
|
||||
// WatchRunner Watch App
|
||||
//
|
||||
// Created by LittleSheep on 2025/10/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
class ComposePostViewModel: ObservableObject {
|
||||
@Published var title = ""
|
||||
@Published var content = ""
|
||||
@Published var isPosting = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var didPost = false
|
||||
|
||||
private let networkService = NetworkService()
|
||||
|
||||
func createPost(token: String, serverUrl: String) async {
|
||||
guard !isPosting else { return }
|
||||
isPosting = true
|
||||
errorMessage = nil
|
||||
|
||||
do {
|
||||
try await networkService.createPost(title: title, content: content, token: token, serverUrl: serverUrl)
|
||||
didPost = true
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
isPosting = false
|
||||
}
|
||||
}
|
||||
284
ios/Solian 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/Solian 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/Solian 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/Solian 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/Solian 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/Solian Watch App/Views/ChatViews.swift
Normal file
@@ -0,0 +1,785 @@
|
||||
//
|
||||
// ChatView.swift
|
||||
// WatchRunner Watch App
|
||||
//
|
||||
// Created by LittleSheep on 2025/10/30.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var selectedTab = 0
|
||||
@State private var chatRooms: [SnChatRoom] = []
|
||||
@State private var chatInvites: [SnChatMember] = []
|
||||
@State private var isLoading = false
|
||||
@State private var error: Error?
|
||||
@State private var showingInvites = false
|
||||
|
||||
private let tabs = ["All", "Direct", "Group"]
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
ForEach(0..<tabs.count, id: \.self) { index in
|
||||
VStack {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
} else if error != nil {
|
||||
VStack {
|
||||
Text("Error loading chats")
|
||||
.font(.caption)
|
||||
Button("Retry") {
|
||||
Task {
|
||||
await loadChatRooms()
|
||||
}
|
||||
}
|
||||
.font(.caption2)
|
||||
}
|
||||
} else {
|
||||
ChatRoomListView(
|
||||
chatRooms: filteredChatRooms(for: index),
|
||||
selectedTab: index
|
||||
)
|
||||
}
|
||||
}
|
||||
.tabItem {
|
||||
Text(tabs[index])
|
||||
}
|
||||
.tag(index)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page)
|
||||
.navigationTitle("Chat")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
showingInvites = true
|
||||
} label: {
|
||||
ZStack {
|
||||
Image(systemName: "envelope")
|
||||
if !chatInvites.isEmpty {
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 8, height: 8)
|
||||
.offset(x: 8, y: -8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingInvites) {
|
||||
ChatInvitesView(invites: $chatInvites, appState: appState)
|
||||
}
|
||||
.onAppear {
|
||||
Task.detached {
|
||||
await loadChatRooms()
|
||||
await loadChatInvites()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func filteredChatRooms(for tabIndex: Int) -> [SnChatRoom] {
|
||||
switch tabIndex {
|
||||
case 0: // All
|
||||
return chatRooms
|
||||
case 1: // Direct
|
||||
return chatRooms.filter { $0.type == 1 }
|
||||
case 2: // Group
|
||||
return chatRooms.filter { $0.type != 1 }
|
||||
default:
|
||||
return chatRooms
|
||||
}
|
||||
}
|
||||
|
||||
private func loadChatRooms() async {
|
||||
guard let token = appState.token, let serverUrl = appState.serverUrl else { return }
|
||||
|
||||
isLoading = true
|
||||
error = nil
|
||||
|
||||
do {
|
||||
let response = try await appState.networkService.fetchChatRooms(token: token, serverUrl: serverUrl)
|
||||
chatRooms = response.rooms
|
||||
} catch {
|
||||
self.error = error
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private func loadChatInvites() async {
|
||||
guard let token = appState.token, let serverUrl = appState.serverUrl else { return }
|
||||
|
||||
do {
|
||||
let response = try await appState.networkService.fetchChatInvites(token: token, serverUrl: serverUrl)
|
||||
chatInvites = response.invites
|
||||
} catch {
|
||||
// Handle error silently for invites
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatRoomListView: View {
|
||||
let chatRooms: [SnChatRoom]
|
||||
let selectedTab: Int
|
||||
|
||||
var body: some View {
|
||||
if chatRooms.isEmpty {
|
||||
VStack {
|
||||
Image(systemName: "message")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No chats yet")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
List(chatRooms) { room in
|
||||
ChatRoomListItem(room: room)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatRoomListItem: View {
|
||||
let room: SnChatRoom
|
||||
@EnvironmentObject var appState: AppState
|
||||
@StateObject private var avatarLoader = ImageLoader()
|
||||
|
||||
private var displayName: String {
|
||||
if room.type == 1, let members = room.members, !members.isEmpty {
|
||||
// For direct messages, show the other member's name
|
||||
return members[0].account.nick
|
||||
} else {
|
||||
// For group chats, show room name or fallback
|
||||
return room.name ?? "Group Chat"
|
||||
}
|
||||
}
|
||||
|
||||
private var subtitle: String {
|
||||
if room.type == 1, let members = room.members, members.count > 1 {
|
||||
// For direct messages, show member usernames
|
||||
return members.map { "@\($0.account.name)" }.joined(separator: ", ")
|
||||
} else if let description = room.description {
|
||||
// For group chats with description
|
||||
return description
|
||||
} else {
|
||||
// Fallback
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private var avatarPictureId: String? {
|
||||
if room.type == 1, let members = room.members, !members.isEmpty {
|
||||
// For direct messages, use the other member's avatar
|
||||
return members[0].account.profile.picture?.id
|
||||
} else {
|
||||
// For group chats, use room picture
|
||||
return room.picture?.id
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(
|
||||
destination: ChatRoomView(room: room)
|
||||
.environmentObject(appState)
|
||||
) {
|
||||
HStack {
|
||||
// Avatar using ImageLoader pattern
|
||||
Group {
|
||||
if avatarLoader.isLoading {
|
||||
ProgressView()
|
||||
.frame(width: 32, height: 32)
|
||||
} else if let image = avatarLoader.image {
|
||||
image
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(Circle())
|
||||
} else if avatarLoader.errorMessage != nil {
|
||||
// Error state - show fallback
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 32, height: 32)
|
||||
.overlay(
|
||||
Text(displayName.prefix(1).uppercased())
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.primary)
|
||||
)
|
||||
} else {
|
||||
// No image available - show initial
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 32, height: 32)
|
||||
.overlay(
|
||||
Text(displayName.prefix(1).uppercased())
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
.task(id: avatarPictureId) {
|
||||
if let serverUrl = appState.serverUrl,
|
||||
let pictureId = avatarPictureId,
|
||||
let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl),
|
||||
let token = appState.token {
|
||||
await avatarLoader.loadImage(from: imageUrl, token: token)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(displayName)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.lineLimit(1)
|
||||
|
||||
if !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Unread count badge placeholder
|
||||
// In a full implementation, this would show unread count
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct ChatRoomView: View {
|
||||
let room: SnChatRoom
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var messages: [SnChatMessage] = []
|
||||
@State private var isLoading = false
|
||||
@State private var error: Error?
|
||||
@State private var wsState: WebSocketState = .disconnected // New state for WebSocket status
|
||||
@State private var hasLoadedMessages = false // Track if messages have been loaded
|
||||
@State private var messageText = "" // Text input for sending messages
|
||||
@State private var isSending = false // Track sending state
|
||||
@State private var isInputHidden = false // Track if input should be hidden during scrolling
|
||||
@State private var scrollTimer: Timer? // Timer to show input after scrolling stops
|
||||
|
||||
@State private var cancellables = Set<AnyCancellable>() // For managing subscriptions
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// Display WebSocket connection status
|
||||
if (wsState != .connected)
|
||||
{
|
||||
Text(webSocketStatusMessage)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.vertical, 2)
|
||||
.animation(.easeInOut, value: wsState) // Animate status changes
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
} else if error != nil {
|
||||
VStack {
|
||||
Text("Error loading messages")
|
||||
.font(.caption)
|
||||
Button("Retry") {
|
||||
Task {
|
||||
await loadMessages()
|
||||
}
|
||||
}
|
||||
.font(.caption2)
|
||||
}
|
||||
} else if messages.isEmpty {
|
||||
VStack {
|
||||
Image(systemName: "bubble.left")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No messages yet")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
ScrollViewReader { scrollView in
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(messages) { message in
|
||||
ChatMessageItem(message: message)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.onAppear {
|
||||
// Scroll to bottom when messages load
|
||||
if let lastMessage = messages.last {
|
||||
scrollView.scrollTo(lastMessage.id, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
.onChange(of: messages.count) { _, _ in
|
||||
// Scroll to bottom when new messages arrive
|
||||
if let lastMessage = messages.last {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(lastMessage.id, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onScrollPhaseChange { _, phase in
|
||||
switch phase {
|
||||
case .interacting:
|
||||
if !isInputHidden {
|
||||
withAnimation(.easeOut(duration: 0.2)) {
|
||||
isInputHidden = true
|
||||
}
|
||||
}
|
||||
case .idle:
|
||||
withAnimation(.easeIn(duration: 0.3)) {
|
||||
isInputHidden = false
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Message input area
|
||||
if !isInputHidden {
|
||||
HStack(spacing: 8) {
|
||||
TextField("Send message...", text: $messageText)
|
||||
.font(.system(size: 14))
|
||||
.disabled(isSending)
|
||||
.frame(height: 40)
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await sendMessage()
|
||||
}
|
||||
} label: {
|
||||
if isSending {
|
||||
ProgressView()
|
||||
.frame(width: 20, height: 20)
|
||||
} else {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
.buttonStyle(.automatic)
|
||||
.disabled(messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSending)
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.navigationTitle(room.name ?? "Chat")
|
||||
.task {
|
||||
await loadMessages()
|
||||
}
|
||||
.onAppear {
|
||||
setupWebSocketListeners()
|
||||
}
|
||||
.onDisappear {
|
||||
cancellables.forEach { $0.cancel() }
|
||||
cancellables.removeAll()
|
||||
scrollTimer?.invalidate()
|
||||
scrollTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
private var webSocketStatusMessage: String {
|
||||
switch wsState {
|
||||
case .connected: return "Connected"
|
||||
case .connecting: return "Connecting..."
|
||||
case .disconnected: return "Disconnected"
|
||||
case .serverDown: return "Server Down"
|
||||
case .duplicateDevice: return "Duplicate Device"
|
||||
case .error(let msg): return "Error: \(msg)"
|
||||
}
|
||||
}
|
||||
|
||||
private func loadMessages() async {
|
||||
// Prevent reloading if already loaded
|
||||
guard !hasLoadedMessages else { return }
|
||||
|
||||
guard let token = appState.token, let serverUrl = appState.serverUrl else {
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
error = nil
|
||||
|
||||
do {
|
||||
let messages = try await appState.networkService.fetchChatMessages(
|
||||
chatRoomId: room.id,
|
||||
token: token,
|
||||
serverUrl: serverUrl
|
||||
)
|
||||
// Sort with newest messages first (for flipped list, newest will appear at bottom)
|
||||
self.messages = messages.sorted { $0.createdAt < $1.createdAt }
|
||||
hasLoadedMessages = true
|
||||
} catch {
|
||||
print("[watchOS] Error loading messages: \(error.localizedDescription)")
|
||||
self.error = error
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private func sendMessage() async {
|
||||
let content = messageText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !content.isEmpty,
|
||||
let token = appState.token,
|
||||
let serverUrl = appState.serverUrl else { return }
|
||||
|
||||
isSending = true
|
||||
|
||||
do {
|
||||
// Generate a nonce for the message
|
||||
let nonce = UUID().uuidString
|
||||
|
||||
// Prepare the request data
|
||||
let messageData: [String: Any] = [
|
||||
"content": content,
|
||||
"attachments_id": [], // Empty for now, can be extended for attachments
|
||||
"meta": [:],
|
||||
"nonce": nonce
|
||||
]
|
||||
|
||||
// Create the URL
|
||||
guard let url = URL(string: "\(serverUrl)/sphere/chat/\(room.id)/messages") else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
|
||||
// Create the request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: messageData, options: [])
|
||||
|
||||
// Send the request
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
// Parse the response to get the sent message
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let sentMessage = try decoder.decode(SnChatMessage.self, from: data)
|
||||
|
||||
// Add the message to the local list
|
||||
messages.append(sentMessage)
|
||||
|
||||
// Clear the input
|
||||
messageText = ""
|
||||
|
||||
} catch {
|
||||
print("[watchOS] Error sending message: \(error.localizedDescription)")
|
||||
// Could show an error alert here
|
||||
}
|
||||
|
||||
isSending = false
|
||||
}
|
||||
|
||||
private func sendReadReceipt() {
|
||||
let data: [String: Any] = ["chat_room_id": room.id]
|
||||
let packet: [String: Any] = ["type": "messages.read", "data": data, "endpoint": "sphere"]
|
||||
if let jsonData = try? JSONSerialization.data(withJSONObject: packet, options: []),
|
||||
let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||
appState.networkService.sendWebSocketMessage(message: jsonString)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupWebSocketListeners() {
|
||||
// Listen for WebSocket packets (new messages)
|
||||
appState.networkService.packetStream
|
||||
.receive(on: DispatchQueue.main) // Ensure UI updates on main thread
|
||||
.sink(receiveCompletion: { completion in
|
||||
if case .failure(let err) = completion {
|
||||
print("[ChatRoomView] WebSocket packet stream error: \(err.localizedDescription)")
|
||||
}
|
||||
}, receiveValue: { packet in
|
||||
if ["messages.new", "messages.update", "messages.delete"].contains(packet.type),
|
||||
let messageData = packet.data {
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: messageData, options: [])
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let message = try decoder.decode(SnChatMessage.self, from: jsonData)
|
||||
|
||||
if message.chatRoomId == room.id {
|
||||
switch packet.type {
|
||||
case "messages.new":
|
||||
if message.type.hasPrefix("call") {
|
||||
// TODO: Handle ongoing call
|
||||
}
|
||||
if !messages.contains(where: { $0.id == message.id }) {
|
||||
messages.append(message)
|
||||
}
|
||||
sendReadReceipt()
|
||||
case "messages.update":
|
||||
if let index = messages.firstIndex(where: { $0.id == message.id }) {
|
||||
messages[index] = message
|
||||
}
|
||||
case "messages.delete":
|
||||
messages.removeAll(where: { $0.id == message.id })
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("[ChatRoomView] Error decoding message from websocket: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Listen for WebSocket connection state changes
|
||||
appState.networkService.stateStream
|
||||
.receive(on: DispatchQueue.main) // Ensure UI updates on main thread
|
||||
.sink { state in
|
||||
wsState = state
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatMessageItem: View {
|
||||
let message: SnChatMessage
|
||||
@EnvironmentObject var appState: AppState
|
||||
@StateObject private var avatarLoader = ImageLoader()
|
||||
|
||||
private var avatarPictureId: String? {
|
||||
message.sender.account.profile.picture?.id
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
// Avatar
|
||||
Group {
|
||||
if avatarLoader.isLoading {
|
||||
ProgressView()
|
||||
.frame(width: 24, height: 24)
|
||||
} else if let image = avatarLoader.image {
|
||||
image
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 24, height: 24)
|
||||
.overlay(
|
||||
Text(message.sender.account.nick.prefix(1).uppercased())
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
.task(id: avatarPictureId) {
|
||||
if let serverUrl = appState.serverUrl,
|
||||
let pictureId = avatarPictureId,
|
||||
let imageUrl = getAttachmentUrl(for: pictureId, serverUrl: serverUrl),
|
||||
let token = appState.token {
|
||||
await avatarLoader.loadImage(from: imageUrl, token: token)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(message.sender.account.nick)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
Spacer()
|
||||
Text(message.createdAt, style: .time)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if let content = message.content, !content.isEmpty {
|
||||
Text(content)
|
||||
.font(.system(size: 14))
|
||||
.lineLimit(nil)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if !message.attachments.isEmpty {
|
||||
AttachmentView(attachment: message.attachments[0])
|
||||
if message.attachments.count > 1 {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "paperclip.circle.fill")
|
||||
.frame(width: 12, height: 12)
|
||||
.foregroundStyle(.gray)
|
||||
Text("\(message.attachments.count - 1)+ attachments")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatInvitesView: View {
|
||||
@Binding var invites: [SnChatMember]
|
||||
let appState: AppState
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var isLoading = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack {
|
||||
if invites.isEmpty {
|
||||
VStack {
|
||||
Image(systemName: "envelope.open")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No invites")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
List(invites) { invite in
|
||||
ChatInviteItem(invite: invite, appState: appState, invites: $invites)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Invites")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatInviteItem: View {
|
||||
let invite: SnChatMember
|
||||
let appState: AppState
|
||||
@Binding var invites: [SnChatMember]
|
||||
@State private var isAccepting = false
|
||||
@State private var isDeclining = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 24, height: 24)
|
||||
.overlay(
|
||||
Text((invite.chatRoom?.name ?? "C").prefix(1).uppercased())
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.primary)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(invite.chatRoom?.name ?? "Unknown Chat")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text(invite.role == 100 ? "Owner" : invite.role >= 50 ? "Moderator" : "Member")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if invite.chatRoom?.type == 1 {
|
||||
Text("Direct")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.blue)
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button {
|
||||
Task {
|
||||
await acceptInvite()
|
||||
}
|
||||
} label: {
|
||||
if isAccepting {
|
||||
ProgressView()
|
||||
.frame(width: 20, height: 20)
|
||||
} else {
|
||||
Image(systemName: "checkmark")
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
}
|
||||
.disabled(isAccepting || isDeclining)
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await declineInvite()
|
||||
}
|
||||
} label: {
|
||||
if isDeclining {
|
||||
ProgressView()
|
||||
.frame(width: 20, height: 20)
|
||||
} else {
|
||||
Image(systemName: "xmark")
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
}
|
||||
.disabled(isAccepting || isDeclining)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
private func acceptInvite() async {
|
||||
guard let token = appState.token,
|
||||
let serverUrl = appState.serverUrl,
|
||||
let chatRoomId = invite.chatRoom?.id else { return }
|
||||
|
||||
isAccepting = true
|
||||
|
||||
do {
|
||||
try await appState.networkService.acceptChatInvite(chatRoomId: chatRoomId, token: token, serverUrl: serverUrl)
|
||||
// Remove from invites list
|
||||
invites.removeAll { $0.id == invite.id }
|
||||
} catch {
|
||||
// Handle error - could show alert
|
||||
print("Failed to accept invite: \(error)")
|
||||
}
|
||||
|
||||
isAccepting = false
|
||||
}
|
||||
|
||||
private func declineInvite() async {
|
||||
guard let token = appState.token,
|
||||
let serverUrl = appState.serverUrl,
|
||||
let chatRoomId = invite.chatRoom?.id else { return }
|
||||
|
||||
isDeclining = true
|
||||
|
||||
do {
|
||||
try await appState.networkService.declineChatInvite(chatRoomId: chatRoomId, token: token, serverUrl: serverUrl)
|
||||
// Remove from invites list
|
||||
invites.removeAll { $0.id == invite.id }
|
||||
} catch {
|
||||
// Handle error - could show alert
|
||||
print("Failed to decline invite: \(error)")
|
||||
}
|
||||
|
||||
isDeclining = false
|
||||
}
|
||||
}
|
||||
53
ios/Solian 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/Solian Watch App/Views/DiscoveryViews.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// DiscoveryViews.swift
|
||||
// WatchRunner Watch App
|
||||
//
|
||||
// Created by LittleSheep on 2025/10/29.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DiscoveryView: View {
|
||||
let discoveryData: DiscoveryData
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: DiscoveryDetailView(discoveryData: discoveryData)) {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Discovery")
|
||||
.font(.headline)
|
||||
Text("\(discoveryData.items.count) new items to discover")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DiscoveryDetailView: View {
|
||||
let discoveryData: DiscoveryData
|
||||
|
||||
var body: some View {
|
||||
List(discoveryData.items) { item in
|
||||
NavigationLink(destination: destinationView(for: item)) {
|
||||
itemView(for: item)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Discovery")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func itemView(for item: DiscoveryItem) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
switch item.data {
|
||||
case .realm(let realm):
|
||||
Text("Realm").font(.headline)
|
||||
Text(realm.name).foregroundColor(.secondary)
|
||||
case .publisher(let publisher):
|
||||
Text("Publisher").font(.headline)
|
||||
Text(publisher.name).foregroundColor(.secondary)
|
||||
case .article(let article):
|
||||
Text("Article").font(.headline)
|
||||
Text(article.title).foregroundColor(.secondary)
|
||||
case .unknown:
|
||||
Text("Unknown item")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func destinationView(for item: DiscoveryItem) -> some View {
|
||||
switch item.data {
|
||||
case .realm(let realm):
|
||||
RealmDetailView(realm: realm)
|
||||
case .publisher(let publisher):
|
||||
PublisherDetailView(publisher: publisher)
|
||||
case .article(let article):
|
||||
ArticleDetailView(article: article)
|
||||
case .unknown:
|
||||
Text("Detail view not available")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RealmDetailView: View {
|
||||
let realm: SnRealm
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(realm.name).font(.headline)
|
||||
if let description = realm.description {
|
||||
Text(description).font(.body)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Realm")
|
||||
}
|
||||
}
|
||||
|
||||
struct PublisherDetailView: View {
|
||||
let publisher: SnPublisher
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(publisher.name).font(.headline)
|
||||
if let description = publisher.description {
|
||||
Text(description).font(.body)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Publisher")
|
||||
}
|
||||
}
|
||||
|
||||
struct ArticleDetailView: View {
|
||||
let article: SnWebArticle
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(article.title).font(.headline)
|
||||
Text(article.url).font(.caption).foregroundColor(.secondary)
|
||||
}
|
||||
.navigationTitle("Article")
|
||||
}
|
||||
}
|
||||
67
ios/Solian Watch App/Views/ExploreView.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// ExploreView.swift
|
||||
// WatchRunner Watch App
|
||||
//
|
||||
// Created by LittleSheep on 2025/10/29.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// The main view with the TabView for filtering.
|
||||
struct ExploreView: View {
|
||||
@EnvironmentObject private var appState: AppState
|
||||
@State private var isComposing = false
|
||||
@State private var selectedTab: String = "Explore"
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
if appState.isReady {
|
||||
TabView(selection: $selectedTab) {
|
||||
ActivityListView(filter: "Explore")
|
||||
.tag("Explore")
|
||||
.tabItem {
|
||||
Label("Explore", systemImage: "safari")
|
||||
}
|
||||
.labelStyle(.titleOnly)
|
||||
|
||||
ActivityListView(filter: "Subscriptions")
|
||||
.tag("Subscriptions")
|
||||
.tabItem {
|
||||
Label("Subscriptions", systemImage: "star")
|
||||
}
|
||||
.labelStyle(.titleOnly)
|
||||
|
||||
ActivityListView(filter: "Friends")
|
||||
.tag("Friends")
|
||||
.tabItem {
|
||||
Label("Friends", systemImage: "person.2")
|
||||
}
|
||||
.labelStyle(.titleOnly)
|
||||
}
|
||||
.navigationTitle(selectedTab)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(action: { isComposing = true }) {
|
||||
Label("Compose", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
ProgressView { Text("Syncing...") }
|
||||
Button("Retry") {
|
||||
appState.requestData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isComposing) {
|
||||
ComposePostView()
|
||||
}
|
||||
.alert("Error", isPresented: .constant(appState.errorMessage != nil), actions: {
|
||||
Button("OK") { appState.errorMessage = nil }
|
||||
}, message: {
|
||||
Text(appState.errorMessage ?? "")
|
||||
})
|
||||
}
|
||||
}
|
||||
34
ios/Solian 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)
|
||||
}
|
||||
}
|
||||