Compare commits
1126 commits
Author | SHA1 | Date | |
---|---|---|---|
|
27b3856e05 | ||
|
93beb2fbd9 | ||
|
04344ba834 | ||
|
f220837309 | ||
|
97c9a1fcf4 | ||
|
b4900989fa | ||
|
8b89eb61cc | ||
|
90d97885cd | ||
|
974d3f1804 | ||
|
f0c91784d0 | ||
|
9fbd0a6a2f | ||
|
70eb781407 | ||
|
56f0afb062 | ||
|
3be69aa4a7 | ||
|
6e766a3fa8 | ||
|
9af485b2be | ||
|
71027e2cc3 | ||
|
4338e0a355 | ||
|
df19135e19 | ||
|
12b6c1af36 | ||
|
db7155423a | ||
|
7eeaf4d902 | ||
|
b19a5706e2 | ||
|
a1ce85b188 | ||
|
009150bc3e | ||
|
23960d6d0f | ||
|
4dd4968bb2 | ||
|
b0846c9a3d | ||
|
7cef1c5786 | ||
|
a167bcdf28 | ||
|
d2eca1d646 | ||
|
9ec6a4ef66 | ||
|
576b52e8c8 | ||
|
46f481aabd | ||
|
81d5d4396f | ||
|
f610c3f047 | ||
|
7a35779b29 | ||
|
e4d1196301 | ||
|
93421c56d9 | ||
|
0b7c6a799f | ||
|
8f71f6649a | ||
|
122e57d8ac | ||
|
594fb3ea07 | ||
|
2f1632b950 | ||
|
c1dfb0a085 | ||
|
ae1bfbeb45 | ||
|
6d289ebd09 | ||
|
be2939b13c | ||
|
491090e373 | ||
|
7c5f2aea81 | ||
|
24f8982bd4 | ||
|
7b67ba0294 | ||
|
698edc4d58 | ||
|
9d5d341764 | ||
|
896c031ab0 | ||
|
c0b0a3ee1c | ||
|
30b9a95cac | ||
|
0a15f7ff1c | ||
|
8f7df06d21 | ||
|
6297a428a3 | ||
|
4cce9d6333 | ||
|
435f28dda9 | ||
|
904cd3dbd7 | ||
|
ce3e2d2344 | ||
|
5080a536db | ||
|
55cbd9ea6f | ||
|
24f77a99e1 | ||
|
45628b5a5f | ||
|
1b7720c9c4 | ||
|
199749b809 | ||
|
e0eed97bcf | ||
|
f80fca91e2 | ||
|
dc85557d40 | ||
|
412f475d1d | ||
|
dfbbb84e9d | ||
|
83bf872cca | ||
|
8c6d2bee7f | ||
|
824b2de23f | ||
|
48febd628f | ||
|
dd1a4585e0 | ||
|
1ad4a245f3 | ||
|
9d11814e49 | ||
|
56869e3a2f | ||
|
a737ac19e5 | ||
|
456d85eda2 | ||
|
4158f7c959 | ||
|
113c4f1c84 | ||
|
77bec1fae3 | ||
|
6f608efa7f | ||
|
6c9d9161dc | ||
|
a72f290038 | ||
|
2a3da72239 | ||
|
9fa19aa132 | ||
|
123f05538a | ||
|
719c023369 | ||
|
0338d54d81 | ||
|
6fa4ac6f79 | ||
|
cf494fd07a | ||
|
6766ed496d | ||
|
33c2646ea1 | ||
|
9af98c3921 | ||
|
4f9cb2e86a | ||
|
4c1ba2168d | ||
|
be02b2ea76 | ||
|
a3326c3fc2 | ||
|
563213d98f | ||
|
82338f815a | ||
|
7a9b6cc0e0 | ||
|
339e2ab1c3 | ||
|
f18f3e0e84 | ||
|
fc6b2129dd | ||
|
54768772b5 | ||
|
3f2fbeeec4 | ||
|
47b5fdf92e | ||
|
d320caaa4f | ||
|
fbff1d6dfe | ||
|
3c5c9adc03 | ||
|
5969e8a166 | ||
|
8a33b6c0d0 | ||
|
d7429c078f | ||
|
6f3f8e9dd0 | ||
|
b98a90ced6 | ||
|
fdb213e4bf | ||
|
4b8d7113f1 | ||
|
7b9cfc2863 | ||
|
478a788f87 | ||
|
2d04433783 | ||
|
59a333cc20 | ||
|
9bc9961f34 | ||
|
0af3732ea9 | ||
|
5960014da9 | ||
|
2ab52d3d3e | ||
|
cd60e0ce1a | ||
|
ff1d5733a0 | ||
|
9887a81ef0 | ||
|
f2ba08e1cc | ||
|
2b8bc2ecd3 | ||
|
513c686b64 | ||
|
aaeb9eaa36 | ||
|
02a8cb12e9 | ||
|
be54a58ae6 | ||
|
8e86e6d205 | ||
|
283e537c44 | ||
|
551697eb2c | ||
|
ab99ef9a0a | ||
|
02d73de113 | ||
|
375ea665b4 | ||
|
a88b9a7fd9 | ||
|
520315d50f | ||
|
1bbbdc8194 | ||
|
d36930b7af | ||
|
c06e3b59e4 | ||
|
8cca261e43 | ||
|
f40aeb9cac | ||
|
1578896b3e | ||
|
ba3d8b1882 | ||
|
04af087c4b | ||
|
a9398c25af | ||
|
13d721912b | ||
|
e3d4e693d2 | ||
|
86c053344b | ||
|
a996aace80 | ||
|
18a1d17230 | ||
|
69cb9a20f9 | ||
|
bab2b4be9c | ||
|
bb005386df | ||
|
c77bb992b4 | ||
|
7caf00d07d | ||
|
6ed760a775 | ||
|
ecd149b3d2 | ||
|
9aaf0b2350 | ||
|
2d6cce6b01 | ||
|
48faddebea | ||
|
a8039df22d | ||
|
e21ec0bd1f | ||
|
9c42a3d7cc | ||
|
54a16b2c9a | ||
|
a6f3068728 | ||
|
f04258ec04 | ||
|
8468e51c17 | ||
|
e9a2d3e151 | ||
|
1f56fa1b9b | ||
|
ccad00a094 | ||
|
51fecb01f5 | ||
|
c29de44d8c | ||
|
1d79832544 | ||
|
a37316c56f | ||
|
189e10f2b4 | ||
|
24d5ecd119 | ||
|
ee6f003073 | ||
|
7328c00006 | ||
|
a6fd8d1137 | ||
|
ea31cda3c2 | ||
|
8ab7b5ac69 | ||
|
7aebe530dd | ||
|
a2afd4f58f | ||
|
88218cd6ec | ||
|
c4dee39efe | ||
|
73651cb7f1 | ||
|
dd1615f0e3 | ||
|
6bd14e0f8d | ||
|
1ca4a74ff0 | ||
|
c3edabb183 | ||
|
ba4cc899f8 | ||
|
5a93184c6d | ||
|
66754ecc7c | ||
|
e857439a02 | ||
|
ed620e86ca | ||
|
936bc96ff7 | ||
|
37b441a43d | ||
|
07af494dcb | ||
|
49a5c6a56a | ||
|
4e4d903c44 | ||
|
abcd4cc321 | ||
|
6a7df1065d | ||
|
c0b855ea55 | ||
|
4c3047b0b9 | ||
|
899b92e390 | ||
|
e71c55b488 | ||
|
361b5f1d84 | ||
|
ad61600328 | ||
|
5f1f71068c | ||
|
3782300b27 | ||
|
7d47834903 | ||
|
65a83fa636 | ||
|
8038e8e6af | ||
|
eb82a67671 | ||
|
bc5bb8272a | ||
|
d2ead5b6d1 | ||
|
d22370959c | ||
|
2c9b841f30 | ||
|
2e3cf4aace | ||
|
7de563a6eb | ||
|
3aae2e6623 | ||
|
5c32c24ae5 | ||
|
bb56047ee2 | ||
|
924ada6606 | ||
|
e6f96d1899 | ||
|
058500f91e | ||
|
fd3d9fc2bc | ||
|
9a7e6b7cb0 | ||
|
bc2a09891a | ||
|
7c343eb4e9 | ||
|
15d7d1dabd | ||
|
f4ec69a37f | ||
|
732a253c7a | ||
|
9c67af8451 | ||
|
b56da94a7c | ||
|
e612fbdf7c | ||
|
f46a0cee17 | ||
|
4a90d979e3 | ||
|
9e4323f317 | ||
|
24ce872849 | ||
|
1f858414d8 | ||
|
2d988d48c1 | ||
|
21d9fd7b59 | ||
|
cca6472a32 | ||
|
c769e80bb6 | ||
|
2986d2b177 | ||
|
29312d1be2 | ||
|
9ddf0e65fc | ||
|
bc74a50a6a | ||
|
d55d6a0371 | ||
|
773fdc318b | ||
|
7423aba92a | ||
|
77aa50ef19 | ||
|
dfc213a19a | ||
|
20900f573f | ||
|
046a41e8ef | ||
|
fcd56ab7a0 | ||
|
923927cddd | ||
|
219703ecc7 | ||
|
0739264005 | ||
|
6f8bec4737 | ||
|
d8e6e6cfb1 | ||
|
e7bc857231 | ||
|
35d249f7c9 | ||
|
7b7e65bf31 | ||
|
9542002534 | ||
|
3020d831e4 | ||
|
a0e022b8de | ||
|
b9b3d0e727 | ||
|
4bf476daea | ||
|
d1fd97794a | ||
|
f14ca6e529 | ||
|
75bb4f43dd | ||
|
cfd6eed159 | ||
|
d10adf1fd9 | ||
|
3b07d56b1d | ||
|
b4dbda8722 | ||
|
827765f251 | ||
|
e7702e1ad0 | ||
|
70f58aa08d | ||
|
cf81054366 | ||
|
f67163e4b0 | ||
|
9bd967cddf | ||
|
551e6b1412 | ||
|
1c76d50bde | ||
|
2a6afb4092 | ||
|
b348f37f1a | ||
|
de757c58f8 | ||
|
7268b5a38e | ||
|
b8cf446406 | ||
|
6e497fae5b | ||
|
586e4f525e | ||
|
7f689bbb9c | ||
|
9dfd9c27c7 | ||
|
9cf16b2f30 | ||
|
1299202bba | ||
|
f16f0d514b | ||
|
096996c242 | ||
|
c7bd5a1d94 | ||
|
20f4eb9c71 | ||
|
74590542bc | ||
|
49b1b0e96c | ||
|
3eec5c0eec | ||
|
016e4d5d57 | ||
|
ba071eb4c8 | ||
|
d2014d3aec | ||
|
621f0d0864 | ||
|
eeff60bf98 | ||
|
245d35db82 | ||
|
62eeba5334 | ||
|
46b8fbde29 | ||
|
9a8568d3fa | ||
|
a6ccdc029b | ||
|
ed9a4a598d | ||
|
13af2d7e3f | ||
|
2b446833da | ||
|
0b96b76641 | ||
|
78eee1e855 | ||
|
b7937e3580 | ||
|
9320b2f114 | ||
|
ad7bc999d3 | ||
|
b41fd2d6ce | ||
|
a79a181d6f | ||
|
fb944f9c48 | ||
|
3c82af0273 | ||
|
3577254f08 | ||
|
abff6218a4 | ||
|
1be9a7b941 | ||
|
18381e22e2 | ||
|
e2273c436a | ||
|
2296dd4658 | ||
|
92662665b9 | ||
|
17387626b8 | ||
|
e00fd49d89 | ||
|
97798b2c35 | ||
|
90a2a19bb1 | ||
|
21d54cc546 | ||
|
76638911ee | ||
|
0b7fed2e9a | ||
|
328ee2d090 | ||
|
ebdd5b9feb | ||
|
f79117eff1 | ||
|
709dd79e25 | ||
|
bf7cdc3712 | ||
|
f12d0600f7 | ||
|
b6f11e4e08 | ||
|
76a8f45478 | ||
|
e03747aa45 | ||
|
8568d6cc59 | ||
|
1a0b52d268 | ||
|
0dea624060 | ||
|
a4927fd30c | ||
|
b8be6b79af | ||
|
ddaf4f9fde | ||
|
d9f115ba67 | ||
|
a8f8933e11 | ||
|
35f6dc8d14 | ||
|
70eea46aef | ||
|
788fab930b | ||
|
7c8ea23ae9 | ||
|
54dd7521c0 | ||
|
5eccb5c294 | ||
|
254962c5c0 | ||
|
a737c61d15 | ||
|
27da37e9ec | ||
|
6d12f2528d | ||
|
231b622f4e | ||
|
73a02db57f | ||
|
ab69a0683b | ||
|
34d8d8bb46 | ||
|
79d062f168 | ||
|
49e47f3dfd | ||
|
c55259efbb | ||
|
99c8ea6f99 | ||
|
a11b6ac2a5 | ||
|
8c68aa711d | ||
|
68b7d469f5 | ||
|
fab23cbafc | ||
|
2f5307bfc7 | ||
|
801b6c5682 | ||
|
802eae750d | ||
|
afa31411e1 | ||
|
af2a69fdb1 | ||
|
0da8228e61 | ||
|
6246d7b0a5 | ||
|
b6c3b07ad6 | ||
|
71495181c6 | ||
|
d3558b761a | ||
|
f758b672f1 | ||
|
1c1a612d56 | ||
|
8ea3fa73e5 | ||
|
5c1f113c54 | ||
|
b7e8f63e86 | ||
|
e7864f7089 | ||
|
3a173a8cae | ||
|
b2be3778c1 | ||
|
bda192fdc4 | ||
|
adca09dfcb | ||
|
3e67113c19 | ||
|
a4cdc6fc18 | ||
|
c847de8f47 | ||
|
7ac9a750cb | ||
|
c86d627cee | ||
|
916c0d9831 | ||
|
14b25830ff | ||
|
556eb15fb4 | ||
|
8e8713886a | ||
|
e79ead5efe | ||
|
5d24c4d2e8 | ||
|
e725b6be4d | ||
|
6af0c36740 | ||
|
4ebe486816 | ||
|
753a0574b1 | ||
|
9e1b1780c9 | ||
|
738180665e | ||
|
334b09ebe3 | ||
|
6f42ed8de0 | ||
|
c09f9727f1 | ||
|
cd63c9ddff | ||
|
c4c86e1434 | ||
|
2c7ca2ca81 | ||
|
7a7066baa4 | ||
|
71d12aec15 | ||
|
d378341914 | ||
|
5ca5dfbd24 | ||
|
5ce4d0b41d | ||
|
75987d74aa | ||
|
6aae6f7e40 | ||
|
e6c2146217 | ||
|
6e981a99fc | ||
|
27ce7fe916 | ||
|
d952601528 | ||
|
34a482f01f | ||
|
8e8737b040 | ||
|
a80d36227e | ||
|
ca9dd5b469 | ||
|
7eb382c052 | ||
|
d65510493a | ||
|
9ade571f53 | ||
|
c24403094c | ||
|
36cc3d5207 | ||
|
9329bdf19b | ||
|
0058ce36b7 | ||
|
8a3c540967 | ||
|
e5bb521502 | ||
|
6435b40a51 | ||
|
1297331407 | ||
|
8c8c551686 | ||
|
bb55154b75 | ||
|
e4f7a6954b | ||
|
73882b0806 | ||
|
e473981841 | ||
|
0916a80a2e | ||
|
a0ff3596cb | ||
|
f401d4094d | ||
|
ef204cf6fd | ||
|
91f0df0f26 | ||
|
a3f29aa15b | ||
|
b3af5f1c45 | ||
|
0501ce9828 | ||
|
bfc1f61e4b | ||
|
8152db745d | ||
|
1f6e8e8d18 | ||
|
d07427b919 | ||
|
f674c2fa46 | ||
|
fcf00796b8 | ||
|
d94e816d63 | ||
|
f428118fa0 | ||
|
3e968525ac | ||
|
e6a4bd383c | ||
|
72b4a92bfe | ||
|
e61a2a32e4 | ||
|
6bb6a02912 | ||
|
c0a78ef007 | ||
|
80a22e55fa | ||
|
c3adb37da0 | ||
|
d399c18a82 | ||
|
3d29c9e600 | ||
|
9ec9c94c9a | ||
|
60ade66251 | ||
|
469b99f3c9 | ||
|
fd190378c6 | ||
|
6e9bff575d | ||
|
3a3cae21b0 | ||
|
3229bf0cb5 | ||
|
c43d1d0dda | ||
|
e733dc3f2a | ||
|
0e9a006483 | ||
|
8459224ab1 | ||
|
d46d47f97d | ||
|
6babd50d6e | ||
|
72f3af7255 | ||
|
2eb15b48d4 | ||
|
ad4995ad70 | ||
|
75a61cb534 | ||
|
1bdd31e848 | ||
|
2e23b08b88 | ||
|
6854df4b89 | ||
|
73323f8460 | ||
|
f39005c118 | ||
|
326d6e5d50 | ||
|
6cc14f8249 | ||
|
1eb33466ca | ||
|
f699c33dfb | ||
|
f6b7b9807f | ||
|
245f13d59f | ||
|
442a7938ca | ||
|
c4de2d6784 | ||
|
632b3f5734 | ||
|
924e1b6057 | ||
|
983c22886a | ||
|
f19ab2b130 | ||
|
13e87b41e9 | ||
|
c4b85679a2 | ||
|
0c13cbd61f | ||
|
aee6459bcf | ||
|
f235ebb720 | ||
|
47436daaf2 | ||
|
1e7c25993a | ||
|
6f1896caf3 | ||
|
4fee875fa7 | ||
|
e41dcd6976 | ||
|
bd51dfc0b6 | ||
|
ce845cd6b3 | ||
|
f0061b36ca | ||
|
8ee5da319c | ||
|
8c72b627df | ||
|
b10ee3091c | ||
|
b93df71431 | ||
|
b6317d7324 | ||
|
7222d530dd | ||
|
b4757621f2 | ||
|
9b70519798 | ||
|
a85c701f50 | ||
|
7add850fe6 | ||
|
acccdb8041 | ||
|
3a721d3280 | ||
|
e694dd5529 | ||
|
0497191acf | ||
|
781121d1d4 | ||
|
fe66acbd39 | ||
|
631707a798 | ||
|
6ea4888ae5 | ||
|
b0cc02541e | ||
|
8a2861b37f | ||
|
176e4feaf8 | ||
|
b4013e39c0 | ||
|
c328c6c0be | ||
|
84898c3b8e | ||
|
2bdef66da0 | ||
|
bb39f07503 | ||
|
6359349a40 | ||
|
c9dc24d02a | ||
|
59bd8a437a | ||
|
67969f595a | ||
|
59c0b841c2 | ||
|
f39f9e1363 | ||
|
5209ab80fc | ||
|
89c060aeea | ||
|
1a366c7bd7 | ||
|
d04f6d34ce | ||
|
4ba8d004d1 | ||
|
83e752ce63 | ||
|
32119e67c1 | ||
|
52d726f9b4 | ||
|
19715bb1f6 | ||
|
7b484fc8e1 | ||
|
dac9fc55e9 | ||
|
dcd63cfd54 | ||
|
a1093c8052 | ||
|
01cd65e6ac | ||
|
f761fa7117 | ||
|
6850fcd928 | ||
|
b6370aef98 | ||
|
b83f7e9a55 | ||
|
00e35be2d5 | ||
|
f3043b608c | ||
|
8f0548f45d | ||
|
fa4603e77c | ||
|
a09b2fa95e | ||
|
d0c2cd4520 | ||
|
5c2148104c | ||
|
b0ba6c15da | ||
|
f79580f746 | ||
|
e0563122a7 | ||
|
aa41f24de9 | ||
|
506a158fa4 | ||
|
82d0e3e576 | ||
|
d065ae6aa8 | ||
|
44dbd379ba | ||
|
d300bee96f | ||
|
21ac4cfa21 | ||
|
bfa717bfa2 | ||
|
e53a3d0f61 | ||
|
2b16b10987 | ||
|
9a5457946b | ||
|
e6b3113090 | ||
|
8a0cf44834 | ||
|
1a3bded101 | ||
|
2e1652ef53 | ||
|
962c7c0295 | ||
|
590299d102 | ||
|
4de4c7c82a | ||
|
ede45a9d46 | ||
|
f3a48118f0 | ||
|
c4bff07c40 | ||
|
f326bbefe6 | ||
|
3ac1bf362b | ||
|
5f05248523 | ||
|
f04f5c701c | ||
|
3eb373550e | ||
|
2fdaed7df4 | ||
|
5a2478c791 | ||
|
ca13e61b53 | ||
|
260dbd351a | ||
|
d69696b726 | ||
|
9fb8d4e484 | ||
|
8ff3e22d9f | ||
|
2145bd5971 | ||
|
d755396119 | ||
|
5aa9f22b42 | ||
|
6f6e352baf | ||
|
1fa54afc3a | ||
|
4985e69200 | ||
|
f9da958047 | ||
|
a7f982e827 | ||
|
8f37465be0 | ||
|
81f4276596 | ||
|
038d029022 | ||
|
0d37cdf64b | ||
|
47326b3f7a | ||
|
8f31e34e1d | ||
|
98c732e6fb | ||
|
880277c6f3 | ||
|
1977b1a572 | ||
|
d8a686be51 | ||
|
e4df8a8b69 | ||
|
81ba1e9bee | ||
|
71f090552a | ||
|
8d7b6f382e | ||
|
232e031559 | ||
|
d31af12bb6 | ||
|
c11a31955c | ||
|
f3ef79b297 | ||
|
52208ab20e | ||
|
da6c5ed76c | ||
|
382ebd77e6 | ||
|
9aa64f261a | ||
|
9ab394272f | ||
|
052afd5931 | ||
|
fdcdf3453f | ||
|
6a34b4c9df | ||
|
51656794fc | ||
|
5941276145 | ||
|
774ba834bd | ||
|
9fe5994bb2 | ||
|
f2cd05968e | ||
|
4f9e23296f | ||
|
3d2171d716 | ||
|
a6f6aa3a02 | ||
|
2e350f5fce | ||
|
df1a44cc21 | ||
|
330aa93437 | ||
|
a86048de33 | ||
|
afcd49cb69 | ||
|
2ff724c268 | ||
|
4dbe04a5d4 | ||
|
28ab417b0a | ||
|
5c204fd06f | ||
|
fcaf48ce53 | ||
|
3840b8fb28 | ||
|
4a09989160 | ||
|
947a684ce3 | ||
|
76219f553b | ||
|
0166a892d8 | ||
|
bfc2994cfb | ||
|
8d54f1a359 | ||
|
ad2adadf87 | ||
|
56360ae821 | ||
|
bf65c386e6 | ||
|
b249b37612 | ||
|
f89b3d2761 | ||
|
719eb34701 | ||
|
885a134eaf | ||
|
1d04a51fb0 | ||
|
32be7d4460 | ||
|
28c8e4d60e | ||
|
222daae47c | ||
|
efe0bdcdad | ||
|
69d5f265fe | ||
|
94670762a4 | ||
|
12419a77e2 | ||
|
662f1002f5 | ||
|
f2606b4614 | ||
|
ab07fb5906 | ||
|
1f703fc1f4 | ||
|
2e2a9f5f14 | ||
|
d2f7ab1464 | ||
|
06a8ca67c3 | ||
|
de83b8ec90 | ||
|
ea5480ef46 | ||
|
98e8ffe4a3 | ||
|
1af75fbede | ||
|
3a23afed89 | ||
|
b3153289c4 | ||
|
47d54fd9e6 | ||
|
94172cef27 | ||
|
534b098ca6 | ||
|
cc03465956 | ||
|
18f95bdf92 | ||
|
71ab8d558a | ||
|
12d92ab1ec | ||
|
8bf36709ea | ||
|
d3b52b3206 | ||
|
1e35ffb82b | ||
|
c1c7c666cb | ||
|
11388757f3 | ||
|
59e5eba860 | ||
|
4b74532048 | ||
|
58d6a3b472 | ||
|
f451d7cb8c | ||
|
bf618d3c5f | ||
|
0c50071ae6 | ||
|
d30fcb8c9b | ||
|
33145eaafc | ||
|
32f96ac1ce | ||
|
4266ac4b42 | ||
|
6e1e83cace | ||
|
9e4b333981 | ||
|
a7ac559225 | ||
|
0424b62684 | ||
|
3e3c69c41c | ||
|
4c7a7986c5 | ||
|
b2933b8c75 | ||
|
b2550d28ac | ||
|
f68bc3e306 | ||
|
20ecc49e31 | ||
|
cb1f3dc548 | ||
|
db64dd726e | ||
|
bbce55e703 | ||
|
cd0e9c10ac | ||
|
df3d8e9ea3 | ||
|
8943588645 | ||
|
104c308cb2 | ||
|
4fa2f3a10b | ||
|
8a49409b26 | ||
|
e9e1992806 | ||
|
641853ed8d | ||
|
3f3ea4ff68 | ||
|
5a52eb50e9 | ||
|
2a936adca0 | ||
|
cf0f0fd891 | ||
|
494b0df0e3 | ||
|
434247f3ea | ||
|
07bfd8cd0e | ||
|
b257bfc576 | ||
|
1b228d504f | ||
|
e9b322e289 | ||
|
32cbb1699e | ||
|
ccae4e0e3d | ||
|
8ed6d548eb | ||
|
1743b3bc08 | ||
|
3cee46d4ef | ||
|
ff5ed48a6e | ||
|
fd55020533 | ||
|
017275ec69 | ||
|
f08c90f8a0 | ||
|
518e69d49d | ||
|
f6abd5ddf0 | ||
|
855fde2eb4 | ||
|
1bd9d15a8f | ||
|
ee725f15f7 | ||
|
89c611ed62 | ||
|
ee2dbf2965 | ||
|
31ae9cb952 | ||
|
6480014148 | ||
|
beec49a7e6 | ||
|
3fd9013dbd | ||
|
aa7c1b87e4 | ||
|
1275b09f20 | ||
|
1bf4d9e398 | ||
|
4bbfdcd256 | ||
|
e8cb090baf | ||
|
e3f7eb31e4 | ||
|
23a83d69cc | ||
|
d5896b95e9 | ||
|
0b5e764556 | ||
|
d32c5c004c | ||
|
1f44c502dd | ||
|
1f28595d39 | ||
|
717ef16628 | ||
|
46d4f3c4f4 | ||
|
e0663bf177 | ||
|
fd1ec73773 | ||
|
7efd8ed7cb | ||
|
cc32845134 | ||
|
4870b202d6 | ||
|
527d982dce | ||
|
cb1b5b69df | ||
|
0c4bde40af | ||
|
21782c9e02 | ||
|
7eec1b8439 | ||
|
60713101a7 | ||
|
90fc2907d3 | ||
|
4adbff1342 | ||
|
7589ab75f8 | ||
|
15f498037d | ||
|
90337bd3ea | ||
|
46df3bb7f9 | ||
|
75a00907ea | ||
|
e2f0863ff6 | ||
|
3743e6d870 | ||
|
6c23569d15 | ||
|
fd09276d49 | ||
|
f9c0355f1d | ||
|
625b7f8137 | ||
|
e6455304ac | ||
|
1b0ddf4fd9 | ||
|
bc554da678 | ||
|
8bb102cd67 | ||
|
cd253c7dc1 | ||
|
27102cbae3 | ||
|
bd3d2008c2 | ||
|
1ac25cc417 | ||
|
8b445324e0 | ||
|
379d2f36fb | ||
|
7388cc4a86 | ||
|
4189a59cf6 | ||
|
3853eff065 | ||
|
5b0f10f0a2 | ||
|
98035e8530 | ||
|
aaafac8e5a | ||
|
8a3c971402 | ||
|
584a0d0432 | ||
|
8e3584ee79 | ||
|
1bbb0dc82d | ||
|
53b442eb33 | ||
|
5f4fef859c | ||
|
1f46691279 | ||
|
59f2023497 | ||
|
462d2355f4 | ||
|
e921f2cdd4 | ||
|
bdce052dc7 | ||
|
9cffb7eda2 | ||
|
113b28db18 | ||
|
1901777eb0 | ||
|
519ef3d708 | ||
|
077b0d269d | ||
|
30f9da06c8 | ||
|
edf36d4b30 | ||
|
05d8ce1bba | ||
|
e027972d43 | ||
|
56b0010d6c | ||
|
5fb9ab1c5e | ||
|
5da1128a4d | ||
|
ad9552472b | ||
|
04b5804c96 | ||
|
27164fa399 | ||
|
44a83cb180 | ||
|
799ea7829c | ||
|
a719f20742 | ||
|
3b89caa7fe | ||
|
2dc01a09b9 | ||
|
fca92be1f3 | ||
|
a6bfac0846 | ||
|
4280764733 | ||
|
559e1d0c83 | ||
|
1b741d41f5 | ||
|
a3d497f48e | ||
|
0842f23d52 | ||
|
199f83c386 | ||
|
d7c3a54b56 | ||
|
69c1c7b11e | ||
|
aacbb6de09 | ||
|
b8af362b23 | ||
|
bff7bd591a | ||
|
c751cb87b8 | ||
|
2fcd5383de | ||
|
9f78f0d611 | ||
|
225821a8da | ||
|
287a7dd4d8 | ||
|
90b0e91c79 | ||
|
bb4453c811 | ||
|
3407f05003 | ||
|
fbff719066 | ||
|
8ace002e4a | ||
|
a2fe0511e0 | ||
|
5951bcec38 | ||
|
c4c705aa10 | ||
|
a2b0cc245f | ||
|
4a6f81f22e | ||
|
4e1205dca8 | ||
|
beb6d1c3c3 | ||
|
39ec8bb389 | ||
|
185f4b60e2 | ||
|
47cf849f21 | ||
|
4943a1708c | ||
|
ab834be2f3 | ||
|
3525d94f71 | ||
|
58debff490 | ||
|
67d1dede04 | ||
|
4168c64d20 | ||
|
2850c168b0 | ||
|
94b642f051 | ||
|
9e43dc3f6f | ||
|
9ed785db0e | ||
|
c0b34d352b | ||
|
d41eca867b | ||
|
ed5b3d7e0f | ||
|
10caea9d1a | ||
|
cf930faf41 | ||
|
3c75c40c77 | ||
|
da54c933f9 | ||
|
7a51c03a9b | ||
|
49a481f6e2 | ||
|
4372ccce2c | ||
|
a97868cab7 | ||
|
e33dcdf372 | ||
|
194e3aea74 | ||
|
8c97c9e1be | ||
|
0fa3fe9e6e | ||
|
399e807f60 | ||
|
7687a47efa | ||
|
42a07e3bdb | ||
|
5cba62dc89 | ||
|
0a47c546d8 | ||
|
69ab13297f | ||
|
386b34e065 | ||
|
abcd63a136 | ||
|
044d3b6b84 | ||
|
ffe9e7a714 | ||
|
eb2222cc11 | ||
|
a740fe8ca8 | ||
|
831ff08082 | ||
|
9e01e6b0f6 | ||
|
8eb9daac3e | ||
|
1870b80f4a | ||
|
2bb910aca5 | ||
|
5659961036 | ||
|
fd074f4ca3 | ||
|
ec854cad9e | ||
|
3f3129afc1 | ||
|
9897307c79 | ||
|
b48e921699 | ||
|
7e5c4ed92a | ||
|
67d5c9648f | ||
|
57c42869a7 | ||
|
6d6d2c5d08 | ||
|
852bc3c1af | ||
|
95469bcc9a | ||
|
57634652df | ||
|
b9a66552ec | ||
|
4619e73e56 | ||
|
dc0f72c742 | ||
|
7391c12644 | ||
|
4351cec117 | ||
|
0c7adca0cf | ||
|
2601764b28 | ||
|
ab5c6643b3 | ||
|
573497ed52 | ||
|
9ba18a8ca3 | ||
|
477dd520b9 | ||
|
ab51d57648 | ||
|
14e91a680e | ||
|
f728ea652e | ||
|
db81486f14 | ||
|
00ee8c2373 | ||
|
6af794dc72 | ||
|
59b16d86a7 | ||
|
d3e71cea74 | ||
|
eb3cbfe6f6 | ||
|
d3888d8c40 | ||
|
dda6ee8f8f | ||
|
501af24cf5 | ||
|
1f5960b257 | ||
|
6c304df78f | ||
|
c390a2ee10 | ||
|
92a30b7575 | ||
|
14d86d9b98 | ||
|
e1e32fdb97 | ||
|
9e347c75b9 | ||
|
59af600945 | ||
|
d759bf9ece | ||
|
409437841d | ||
|
9d23e05d49 | ||
|
c498ab9b2d | ||
|
942dfa1de7 | ||
|
9e0c77a48f | ||
|
efb255eb62 | ||
|
92e15fdcc9 | ||
|
69e5ba251c | ||
|
d35de0013c | ||
|
731ee4c3ac | ||
|
a0fa559d60 | ||
|
69e3f798bb | ||
|
2d5f096887 | ||
|
6ecc56f39a | ||
|
50ce63e800 | ||
|
3d63ea4642 | ||
|
3c26b7fceb | ||
|
0874be89f8 | ||
|
356b58bf67 | ||
|
65431dc727 | ||
|
b2f594f174 | ||
|
9746eb7674 | ||
|
f68e174aac | ||
|
776a51734f | ||
|
0ec94c6cc4 | ||
|
e32749851e | ||
|
a562c22aee | ||
|
c14bd26e3a | ||
|
6f0e466954 | ||
|
cc8d63e656 | ||
|
96811e5074 | ||
|
af245da273 | ||
|
e066ef8dc9 | ||
|
8f09841f96 | ||
|
eab69ce9fa | ||
|
3a3d0a7b55 | ||
|
4ffdd46a2a | ||
|
df55028836 | ||
|
9a6b2129b2 | ||
|
da0b92e13d | ||
|
a08587643d | ||
|
09dc56fcd3 | ||
|
e72abeebc6 | ||
|
7d8516474c | ||
|
68f1a3e47d | ||
|
072c980157 | ||
|
4d588e4a18 | ||
|
c5b4a0dd07 | ||
|
fa090f5663 | ||
|
5ef39e7a09 | ||
|
022b068ce8 | ||
|
d00b66034d | ||
|
ae51ea0c1c | ||
|
1a4d552e06 | ||
|
3e88d89de3 | ||
|
6e47f9410d | ||
|
8803f1524a | ||
|
aa72c35718 | ||
|
62438d669f | ||
|
e52a3c0416 | ||
|
7c118ade6c | ||
|
7f96d97d7c | ||
|
baf853f46e | ||
|
f263c57858 | ||
|
dca8937aba | ||
|
c818dbf744 | ||
|
ff9bfe9285 | ||
|
0e3d174625 | ||
|
4f6f98271a | ||
|
aa120a48f7 | ||
|
6c307aba63 | ||
|
f1267620be | ||
|
7d0c6671bb | ||
|
0989f25bff | ||
|
f0e9b0ca26 | ||
|
e52712383f | ||
|
705e5514dc | ||
|
5c9122a72c | ||
|
8b4f6dbd05 | ||
|
6f484fa6c5 | ||
|
c56ef64f55 | ||
|
0db1b889b4 | ||
|
0132e51509 | ||
|
f172d6d4a6 | ||
|
0d2454886e | ||
|
b574cdfbda | ||
|
77468d4560 | ||
|
45b9d2886e | ||
|
968792ad68 | ||
|
9057740162 | ||
|
c3d1c6d363 | ||
|
71f2617a30 | ||
|
8571800b31 | ||
|
8561374a84 | ||
|
fd8906ecd5 | ||
|
7b646c770a | ||
|
21363d1bdf | ||
|
cbe7d8e8af | ||
|
498d6b950e | ||
|
25925f11d7 | ||
|
58e4a8df49 | ||
|
b9793057e0 | ||
|
93d9ded447 | ||
|
c36b9083ce | ||
|
4001ffdbe5 | ||
|
82dfeffaec | ||
|
0709ce0958 | ||
|
15b704c97a | ||
|
5c69aa64bc | ||
|
4c7d0f3386 | ||
|
f6b987a18a | ||
|
c3c6899483 | ||
|
e7752feba0 | ||
|
9d1e077762 | ||
|
156279faac | ||
|
b3919702e0 | ||
|
21fd0b0541 | ||
|
58957c779a | ||
|
62fd7bae12 | ||
|
eaaa8cd1c9 | ||
|
0e3493bdb8 | ||
|
acad7ee65f | ||
|
4937ffb618 | ||
|
2ac615b0ba | ||
|
28230ba184 | ||
|
796f451f3a | ||
|
b0ee77caed | ||
|
af63692afa | ||
|
2a95678b4f | ||
|
cc71209254 |
11
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "swift" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
35
.github/workflows/validate_translations.yml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
name: Validate Translations
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [synchronize, opened, reopened, labeled, unlabeled, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
name: Validate Translations
|
||||||
|
runs-on: macOS-latest
|
||||||
|
steps:
|
||||||
|
- name: git checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: ruby versions
|
||||||
|
run: |
|
||||||
|
ruby --version
|
||||||
|
gem --version
|
||||||
|
bundler --version
|
||||||
|
|
||||||
|
- name: ruby setup
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.3
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
# additional steps here, if needed
|
||||||
|
|
||||||
|
- name: Clone SwiftPolyglot
|
||||||
|
run: git clone https://github.com/appdecostudio/SwiftPolyglot.git --branch 0.2.0
|
||||||
|
|
||||||
|
- name: Build and Run SwiftPolyglot
|
||||||
|
run: |
|
||||||
|
swift build --package-path ./SwiftPolyglot --configuration release
|
||||||
|
swift run --package-path ./SwiftPolyglot swiftpolyglot "en,eu,be,ca,zh-Hans,zh-Hant,nl,en-GB,fr,de,it,ja,ko,nb,pl,pt-BR,es,tr,uk"
|
|
@ -13,7 +13,7 @@ import Models
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
// Sample code was sending this from a thread to another, let asume @Sendable for this
|
// Sample code was sending this from a thread to another, let asume @Sendable for this
|
||||||
extension NSExtensionContext: @unchecked Sendable {}
|
extension NSExtensionContext: @unchecked @retroactive Sendable {}
|
||||||
|
|
||||||
final class ActionRequestHandler: NSObject, NSExtensionRequestHandling, Sendable {
|
final class ActionRequestHandler: NSObject, NSExtensionRequestHandling, Sendable {
|
||||||
enum Error: Swift.Error {
|
enum Error: Swift.Error {
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>OPENAI_SECRET</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<string>NICE_TRY</string>
|
<true/>
|
||||||
<key>DEEPL_SECRET</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<string>NICE_TRY_AGAIN</string>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
|
@ -1,12 +1,22 @@
|
||||||
{
|
{
|
||||||
|
"originHash" : "b7af8c2ab18771d4cebfbeb66d91559df500516a12027cd67834b2a576eb3df0",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "bodega",
|
"identity" : "bodega",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/mergesort/Bodega.git",
|
"location" : "https://github.com/mergesort/Bodega",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "3e7c1c58ad9a46aa8551cebfe87770003cdaaaca",
|
"revision" : "bfd8871e9c2590d31b200e54c75428a71483afdf",
|
||||||
"version" : "2.0.2"
|
"version" : "2.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "buttonkit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/Dean151/ButtonKit",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d567519b297777c38dee56ef10201fef4962ff75",
|
||||||
|
"version" : "0.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -14,8 +24,17 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/divadretlaw/EmojiText",
|
"location" : "https://github.com/divadretlaw/EmojiText",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "100661c947a2fa6b68c768ccae737a04476125fe",
|
"revision" : "174a7bc7bd75650ad1acb5679dbb754296093de0",
|
||||||
"version" : "2.1.0"
|
"version" : "4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "giphy-ios-sdk",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/Giphy/giphy-ios-sdk",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "fb61ec12738133eb3b9bf62ed11d1bf93d9b4b20",
|
||||||
|
"version" : "2.2.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -24,7 +43,16 @@
|
||||||
"location" : "https://github.com/evgenyneu/keychain-swift",
|
"location" : "https://github.com/evgenyneu/keychain-swift",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "master",
|
"branch" : "master",
|
||||||
"revision" : "e43f9b99b172ae6a7253047f8ba95c7a0b05b99f"
|
"revision" : "5e1b02b6a9dac2a759a1d5dbc175c86bd192a608"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "libwebp-xcode",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/libwebp-Xcode",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "b2b1d20a90b14d11f6ef4241da6b81c1d3f171e4",
|
||||||
|
"version" : "1.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -32,8 +60,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/nicklockwood/LRUCache",
|
"location" : "https://github.com/nicklockwood/LRUCache",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "6d2b5246c9c98dcd498552bb22f08d55b12a8371",
|
"revision" : "542f0449556327415409ededc9c43a4bd0a397dc",
|
||||||
"version" : "1.0.4"
|
"version" : "1.0.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -41,17 +69,26 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/kean/Nuke",
|
"location" : "https://github.com/kean/Nuke",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "nuke-12",
|
"revision" : "0ead44350d2737db384908569c012fe67c421e4d",
|
||||||
"revision" : "baccf1b00f458a77a9f9615e415bc4463029cf5e"
|
"version" : "12.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identity" : "purchases-ios",
|
"identity" : "purchases-ios",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/RevenueCat/purchases-ios.git",
|
"location" : "https://github.com/RevenueCat/purchases-ios",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "b5562ed1409c47b20c74da3f2a1c2d603ca37508",
|
"revision" : "7d55b964114a3d4a76791227cdc28577617596db",
|
||||||
"version" : "4.17.7"
|
"version" : "4.43.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sfsafesymbols",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c",
|
||||||
|
"version" : "4.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -59,8 +96,35 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/stephencelis/SQLite.swift.git",
|
"location" : "https://github.com/stephencelis/SQLite.swift.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "4d543d811ee644fa4cc4bfa0be996b4dd6ba0f54",
|
"revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8",
|
||||||
"version" : "0.13.3"
|
"version" : "0.15.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-cmark",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-cmark.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
|
||||||
|
"version" : "0.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-markdown",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-markdown",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "8f79cb175981458a0a27e76cb42fee8e17b1a993",
|
||||||
|
"version" : "0.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftsdk",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TelemetryDeck/SwiftSDK",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "13a26cf125b70d695913eb9bea9f9b9c29da5790",
|
||||||
|
"version" : "2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -68,28 +132,46 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "f707b8680cddb96dc1855632340a572ef37bbb98",
|
"revision" : "3c2c7e1e72b8abd96eafbae80323c5c1e5317437",
|
||||||
"version" : "2.5.3"
|
"version" : "2.7.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swiftui-introspect",
|
"identity" : "swiftui-introspect",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
"location" : "https://github.com/siteline/swiftui-introspect",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "f2616860a41f9d9932da412a8978fec79c06fe24",
|
"revision" : "668a65735751432b640260c56dfa621cec568368",
|
||||||
"version" : "0.1.4"
|
"version" : "1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swiftui-shimmer",
|
"identity" : "wishkit-ios",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/markiv/SwiftUI-Shimmer",
|
"location" : "https://github.com/wishkit/wishkit-ios.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "965a7cbcbf094cbcf22b9251a2323bdc3432e171",
|
"revision" : "2b5eb8d1fb13706f8c14767a5239e34e403375f1",
|
||||||
"version" : "1.1.0"
|
"version" : "4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "wishkit-ios-shared",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/wishkit/wishkit-ios-shared.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "118c9c482e4ad57c65d664283516425b98616483",
|
||||||
|
"version" : "1.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "wrappinghstack",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/dkk/WrappingHStack",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "425d9488ba55f58f0b34498c64c054c77fc2a44b",
|
||||||
|
"version" : "2.2.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 2
|
"version" : 3
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1600"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "E9DF41F929830FEC0003AAD2"
|
||||||
|
BuildableName = "IceCubesActionExtension.appex"
|
||||||
|
BlueprintName = "IceCubesActionExtension"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1420"
|
LastUpgradeVersion = "1600"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
BuildableName = "IceCubesApp.app"
|
BuildableName = "Ice Cubes.app"
|
||||||
BlueprintName = "IceCubesApp"
|
BlueprintName = "IceCubesApp"
|
||||||
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
BuildableName = "IceCubesApp.app"
|
BuildableName = "Ice Cubes.app"
|
||||||
BlueprintName = "IceCubesApp"
|
BlueprintName = "IceCubesApp"
|
||||||
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
BuildableName = "IceCubesApp.app"
|
BuildableName = "Ice Cubes.app"
|
||||||
BlueprintName = "IceCubesApp"
|
BlueprintName = "IceCubesApp"
|
||||||
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1600"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9F7788C42BE652B1004E6BEF"
|
||||||
|
BuildableName = "IceCubesAppWidgetsExtensionExtension.appex"
|
||||||
|
BlueprintName = "IceCubesAppWidgetsExtensionExtension"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetKind"
|
||||||
|
value = ""
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetDefaultView"
|
||||||
|
value = "timeline"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetFamily"
|
||||||
|
value = "systemMedium"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1600"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9F2A5415296AB631009B2D7C"
|
||||||
|
BuildableName = "IceCubesNotifications.appex"
|
||||||
|
BlueprintName = "IceCubesNotifications"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<RemoteRunnable
|
||||||
|
runnableDebuggingMode = "1"
|
||||||
|
BundleIdentifier = "com.thomasricouard.IceCubesApp"
|
||||||
|
RemotePath = "/Users/dimillian/Library/Developer/CoreSimulator/Devices/8EF923D0-4CF1-49B6-B287-5F05AD5440C1/data/Containers/Bundle/Application/C447A1D1-9BC9-49C9-8FA5-130E8403972F/Ice Cubes.app">
|
||||||
|
</RemoteRunnable>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1600"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FAD858729743F7400496AB1"
|
||||||
|
BuildableName = "IceCubesShareExtension.appex"
|
||||||
|
BlueprintName = "IceCubesShareExtension"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
BuildableName = "Ice Cubes.app"
|
||||||
|
BlueprintName = "IceCubesApp"
|
||||||
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -3,13 +3,15 @@ import AppAccount
|
||||||
import Conversations
|
import Conversations
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
import Explore
|
||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
import Lists
|
import Lists
|
||||||
|
import MediaUI
|
||||||
import Models
|
import Models
|
||||||
import Status
|
import Notifications
|
||||||
|
import StatusKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Timeline
|
import Timeline
|
||||||
import Explore
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
extension View {
|
extension View {
|
||||||
|
@ -22,6 +24,8 @@ extension View {
|
||||||
AccountDetailView(account: account)
|
AccountDetailView(account: account)
|
||||||
case let .accountSettingsWithAccount(account, appAccount):
|
case let .accountSettingsWithAccount(account, appAccount):
|
||||||
AccountSettingsView(account: account, appAccount: appAccount)
|
AccountSettingsView(account: account, appAccount: appAccount)
|
||||||
|
case let .accountMediaGridView(account, initialMedia):
|
||||||
|
AccountDetailMediaGridView(account: account, initialMediaStatuses: initialMedia)
|
||||||
case let .statusDetail(id):
|
case let .statusDetail(id):
|
||||||
StatusDetailView(statusId: id)
|
StatusDetailView(statusId: id)
|
||||||
case let .statusDetailWithStatus(status):
|
case let .statusDetailWithStatus(status):
|
||||||
|
@ -31,9 +35,20 @@ extension View {
|
||||||
case let .conversationDetail(conversation):
|
case let .conversationDetail(conversation):
|
||||||
ConversationDetailView(conversation: conversation)
|
ConversationDetailView(conversation: conversation)
|
||||||
case let .hashTag(tag, accountId):
|
case let .hashTag(tag, accountId):
|
||||||
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), scrollToTopSignal: .constant(0))
|
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)),
|
||||||
|
pinnedFilters: .constant([]),
|
||||||
|
selectedTagGroup: .constant(nil),
|
||||||
|
canFilterTimeline: false)
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
TimelineView(timeline: .constant(.list(list: list)), scrollToTopSignal: .constant(0))
|
TimelineView(timeline: .constant(.list(list: list)),
|
||||||
|
pinnedFilters: .constant([]),
|
||||||
|
selectedTagGroup: .constant(nil),
|
||||||
|
canFilterTimeline: false)
|
||||||
|
case let .linkTimeline(url, title):
|
||||||
|
TimelineView(timeline: .constant(.link(url: url, title: title)),
|
||||||
|
pinnedFilters: .constant([]),
|
||||||
|
selectedTagGroup: .constant(nil),
|
||||||
|
canFilterTimeline: false)
|
||||||
case let .following(id):
|
case let .following(id):
|
||||||
AccountsListView(mode: .following(accountId: id))
|
AccountsListView(mode: .following(accountId: id))
|
||||||
case let .followers(id):
|
case let .followers(id):
|
||||||
|
@ -45,9 +60,23 @@ extension View {
|
||||||
case let .accountsList(accounts):
|
case let .accountsList(accounts):
|
||||||
AccountsListView(mode: .accountsList(accounts: accounts))
|
AccountsListView(mode: .accountsList(accounts: accounts))
|
||||||
case .trendingTimeline:
|
case .trendingTimeline:
|
||||||
TimelineView(timeline: .constant(.trending), scrollToTopSignal: .constant(0))
|
TimelineView(timeline: .constant(.trending),
|
||||||
|
pinnedFilters: .constant([]),
|
||||||
|
selectedTagGroup: .constant(nil),
|
||||||
|
canFilterTimeline: false)
|
||||||
|
case let .trendingLinks(cards):
|
||||||
|
TrendingLinksListView(cards: cards)
|
||||||
case let .tagsList(tags):
|
case let .tagsList(tags):
|
||||||
TagsListView(tags: tags)
|
TagsListView(tags: tags)
|
||||||
|
case .notificationsRequests:
|
||||||
|
NotificationsRequestsListView()
|
||||||
|
case let .notificationForAccount(accountId):
|
||||||
|
NotificationsListView(lockedType: nil,
|
||||||
|
lockedAccountId: accountId)
|
||||||
|
case .blockedAccounts:
|
||||||
|
AccountsListView(mode: .blocked)
|
||||||
|
case .mutedAccounts:
|
||||||
|
AccountsListView(mode: .muted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,19 +85,31 @@ extension View {
|
||||||
sheet(item: sheetDestinations) { destination in
|
sheet(item: sheetDestinations) { destination in
|
||||||
switch destination {
|
switch destination {
|
||||||
case let .replyToStatusEditor(status):
|
case let .replyToStatusEditor(status):
|
||||||
StatusEditorView(mode: .replyTo(status: status))
|
StatusEditor.MainView(mode: .replyTo(status: status))
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
case let .newStatusEditor(visibility):
|
case let .newStatusEditor(visibility):
|
||||||
StatusEditorView(mode: .new(visibility: visibility))
|
StatusEditor.MainView(mode: .new(text: nil, visibility: visibility))
|
||||||
|
.withEnvironments()
|
||||||
|
case let .prefilledStatusEditor(text, visibility):
|
||||||
|
StatusEditor.MainView(mode: .new(text: text, visibility: visibility))
|
||||||
|
.withEnvironments()
|
||||||
|
case let .imageURL(urls, visibility):
|
||||||
|
StatusEditor.MainView(mode: .imageURL(urls: urls, visibility: visibility))
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
case let .editStatusEditor(status):
|
case let .editStatusEditor(status):
|
||||||
StatusEditorView(mode: .edit(status: status))
|
StatusEditor.MainView(mode: .edit(status: status))
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
case let .quoteStatusEditor(status):
|
case let .quoteStatusEditor(status):
|
||||||
StatusEditorView(mode: .quote(status: status))
|
StatusEditor.MainView(mode: .quote(status: status))
|
||||||
|
.withEnvironments()
|
||||||
|
case let .quoteLinkStatusEditor(link):
|
||||||
|
StatusEditor.MainView(mode: .quoteLink(link: link))
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
case let .mentionStatusEditor(account, visibility):
|
case let .mentionStatusEditor(account, visibility):
|
||||||
StatusEditorView(mode: .mention(account: account, visibility: visibility))
|
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
||||||
|
.withEnvironments()
|
||||||
|
case .listCreate:
|
||||||
|
ListCreateView()
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
case let .listEdit(list):
|
case let .listEdit(list):
|
||||||
ListEditView(list: list)
|
ListEditView(list: list)
|
||||||
|
@ -82,37 +123,71 @@ extension View {
|
||||||
case .addRemoteLocalTimeline:
|
case .addRemoteLocalTimeline:
|
||||||
AddRemoteTimelineView()
|
AddRemoteTimelineView()
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
|
case .addTagGroup:
|
||||||
|
EditTagGroupView()
|
||||||
|
.withEnvironments()
|
||||||
case let .statusEditHistory(status):
|
case let .statusEditHistory(status):
|
||||||
StatusEditHistoryView(statusId: status)
|
StatusEditHistoryView(statusId: status)
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
case .settings:
|
case .settings:
|
||||||
SettingsTabs(popToRootTab: .constant(.settings))
|
SettingsTabs(isModal: true)
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
.preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light)
|
.preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light)
|
||||||
case .accountPushNotficationsSettings:
|
case .accountPushNotficationsSettings:
|
||||||
if let subscription = PushNotificationsService.shared.subscriptions.first(where: { $0.account.token == AppAccountsManager.shared.currentAccount.oauthToken }) {
|
if let subscription = PushNotificationsService.shared.subscriptions.first(where: { $0.account.token == AppAccountsManager.shared.currentAccount.oauthToken }) {
|
||||||
PushNotificationsView(subscription: subscription)
|
NavigationSheet { PushNotificationsView(subscription: subscription) }
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
case .about:
|
||||||
|
NavigationSheet { AboutView() }
|
||||||
|
.withEnvironments()
|
||||||
|
case .support:
|
||||||
|
NavigationSheet { SupportAppView() }
|
||||||
|
.withEnvironments()
|
||||||
case let .report(status):
|
case let .report(status):
|
||||||
ReportView(status: status)
|
ReportView(status: status)
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
case let .shareImage(image, status):
|
case let .shareImage(image, status):
|
||||||
ActivityView(image: image, status: status)
|
ActivityView(image: image, status: status)
|
||||||
|
.withEnvironments()
|
||||||
|
case let .editTagGroup(tagGroup, onSaved):
|
||||||
|
EditTagGroupView(tagGroup: tagGroup, onSaved: onSaved)
|
||||||
|
.withEnvironments()
|
||||||
|
case .timelineContentFilter:
|
||||||
|
NavigationSheet { TimelineContentFilterView() }
|
||||||
|
.presentationDetents([.medium])
|
||||||
|
.presentationBackground(.thinMaterial)
|
||||||
|
.withEnvironments()
|
||||||
|
case .accountEditInfo:
|
||||||
|
EditAccountView()
|
||||||
|
.withEnvironments()
|
||||||
|
case .accountFiltersList:
|
||||||
|
FiltersListView()
|
||||||
|
.withEnvironments()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withEnvironments() -> some View {
|
func withEnvironments() -> some View {
|
||||||
environmentObject(CurrentAccount.shared)
|
environment(CurrentAccount.shared)
|
||||||
.environmentObject(UserPreferences.shared)
|
.environment(UserPreferences.shared)
|
||||||
.environmentObject(CurrentInstance.shared)
|
.environment(CurrentInstance.shared)
|
||||||
.environmentObject(Theme.shared)
|
.environment(Theme.shared)
|
||||||
.environmentObject(AppAccountsManager.shared)
|
.environment(AppAccountsManager.shared)
|
||||||
.environmentObject(PushNotificationsService.shared)
|
.environment(PushNotificationsService.shared)
|
||||||
.environmentObject(AppAccountsManager.shared.currentClient)
|
.environment(AppAccountsManager.shared.currentClient)
|
||||||
|
.environment(QuickLook.shared)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withModelContainer() -> some View {
|
||||||
|
modelContainer(for: [
|
||||||
|
Draft.self,
|
||||||
|
LocalTimeline.self,
|
||||||
|
TagGroup.self,
|
||||||
|
RecentTag.self,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,9 +224,15 @@ struct ActivityView: UIViewControllerRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUIViewController(context _: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
|
func makeUIViewController(context _: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
|
||||||
return UIActivityViewController(activityItems: [image, LinkDelegate(image: image, status: status)],
|
UIActivityViewController(activityItems: [image, LinkDelegate(image: image, status: status)],
|
||||||
applicationActivities: nil)
|
applicationActivities: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIViewController(_: UIActivityViewController, context _: UIViewControllerRepresentableContext<ActivityView>) {}
|
func updateUIViewController(_: UIActivityViewController, context _: UIViewControllerRepresentableContext<ActivityView>) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension URL: @retroactive Identifiable {
|
||||||
|
public var id: String {
|
||||||
|
absoluteString
|
||||||
|
}
|
||||||
|
}
|
36
IceCubesApp/App/IceCubesApp-release.entitlements
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>iCloud.icecubesapp</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.icloud-services</key>
|
||||||
|
<array>
|
||||||
|
<string>CloudKit</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.user-fonts</key>
|
||||||
|
<array>
|
||||||
|
<string>app-usage</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.usernotifications.communication</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.$(BUNDLE_ID_PREFIX).IceCubesApp</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)$(BUNDLE_ID_PREFIX).IceCubesApp</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -4,6 +4,14 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>aps-environment</key>
|
<key>aps-environment</key>
|
||||||
<string>development</string>
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>iCloud.icecubesapp</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.icloud-services</key>
|
||||||
|
<array>
|
||||||
|
<string>CloudKit</string>
|
||||||
|
</array>
|
||||||
<key>com.apple.developer.user-fonts</key>
|
<key>com.apple.developer.user-fonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>app-usage</string>
|
<string>app-usage</string>
|
||||||
|
@ -16,7 +24,9 @@
|
||||||
<array>
|
<array>
|
||||||
<string>group.$(BUNDLE_ID_PREFIX).IceCubesApp</string>
|
<string>group.$(BUNDLE_ID_PREFIX).IceCubesApp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
|
@ -1,305 +0,0 @@
|
||||||
import Account
|
|
||||||
import AppAccount
|
|
||||||
import AVFoundation
|
|
||||||
import DesignSystem
|
|
||||||
import Env
|
|
||||||
import KeychainSwift
|
|
||||||
import Network
|
|
||||||
import RevenueCat
|
|
||||||
import SwiftUI
|
|
||||||
import Timeline
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct IceCubesApp: App {
|
|
||||||
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
|
||||||
|
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
|
||||||
|
|
||||||
@StateObject private var appAccountsManager = AppAccountsManager.shared
|
|
||||||
@StateObject private var currentInstance = CurrentInstance.shared
|
|
||||||
@StateObject private var currentAccount = CurrentAccount.shared
|
|
||||||
@StateObject private var userPreferences = UserPreferences.shared
|
|
||||||
@StateObject private var pushNotificationsService = PushNotificationsService.shared
|
|
||||||
@StateObject private var watcher = StreamWatcher()
|
|
||||||
@StateObject private var quickLook = QuickLook()
|
|
||||||
@StateObject private var theme = Theme.shared
|
|
||||||
@StateObject private var sidebarRouterPath = RouterPath()
|
|
||||||
|
|
||||||
@State private var selectedTab: Tab = .timeline
|
|
||||||
@State private var popToRootTab: Tab = .other
|
|
||||||
@State private var sideBarLoadedTabs: Set<Tab> = Set()
|
|
||||||
@State private var isSupporter: Bool = false
|
|
||||||
|
|
||||||
private var availableTabs: [Tab] {
|
|
||||||
appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
appView
|
|
||||||
.applyTheme(theme)
|
|
||||||
.onAppear {
|
|
||||||
setNewClientsInEnv(client: appAccountsManager.currentClient)
|
|
||||||
setupRevenueCat()
|
|
||||||
refreshPushSubs()
|
|
||||||
}
|
|
||||||
.environmentObject(appAccountsManager)
|
|
||||||
.environmentObject(appAccountsManager.currentClient)
|
|
||||||
.environmentObject(quickLook)
|
|
||||||
.environmentObject(currentAccount)
|
|
||||||
.environmentObject(currentInstance)
|
|
||||||
.environmentObject(userPreferences)
|
|
||||||
.environmentObject(theme)
|
|
||||||
.environmentObject(watcher)
|
|
||||||
.environmentObject(pushNotificationsService)
|
|
||||||
.environment(\.isSupporter, isSupporter)
|
|
||||||
.fullScreenCover(item: $quickLook.url, content: { url in
|
|
||||||
QuickLookPreview(selectedURL: url, urls: quickLook.urls)
|
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
|
||||||
.background(TransparentBackground())
|
|
||||||
})
|
|
||||||
.onChange(of: pushNotificationsService.handledNotification) { notification in
|
|
||||||
if notification != nil {
|
|
||||||
pushNotificationsService.handledNotification = nil
|
|
||||||
if appAccountsManager.currentAccount.oauthToken?.accessToken != notification?.account.token.accessToken,
|
|
||||||
let account = appAccountsManager.availableAccounts.first(where:
|
|
||||||
{ $0.oauthToken?.accessToken == notification?.account.token.accessToken })
|
|
||||||
{
|
|
||||||
appAccountsManager.currentAccount = account
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
||||||
selectedTab = .notifications
|
|
||||||
pushNotificationsService.handledNotification = notification
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedTab = .notifications
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.commands {
|
|
||||||
appMenu
|
|
||||||
}
|
|
||||||
.onChange(of: scenePhase) { scenePhase in
|
|
||||||
handleScenePhase(scenePhase: scenePhase)
|
|
||||||
}
|
|
||||||
.onChange(of: appAccountsManager.currentClient) { newClient in
|
|
||||||
setNewClientsInEnv(client: newClient)
|
|
||||||
if newClient.isAuth {
|
|
||||||
watcher.watch(streams: [.user, .direct])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var appView: some View {
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
|
||||||
sidebarView
|
|
||||||
} else {
|
|
||||||
tabBarView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func badgeFor(tab: Tab) -> Int {
|
|
||||||
if tab == .notifications && selectedTab != tab,
|
|
||||||
let token = appAccountsManager.currentAccount.oauthToken
|
|
||||||
{
|
|
||||||
return watcher.unreadNotificationsCount + userPreferences.getNotificationsCount(for: token)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private var sidebarView: some View {
|
|
||||||
SideBarView(selectedTab: $selectedTab,
|
|
||||||
popToRootTab: $popToRootTab,
|
|
||||||
tabs: availableTabs,
|
|
||||||
routerPath: sidebarRouterPath) {
|
|
||||||
GeometryReader { _ in
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
ZStack {
|
|
||||||
if selectedTab == .profile {
|
|
||||||
ProfileTab(popToRootTab: $popToRootTab)
|
|
||||||
}
|
|
||||||
ForEach(availableTabs) { tab in
|
|
||||||
if tab == selectedTab || sideBarLoadedTabs.contains(tab) {
|
|
||||||
tab
|
|
||||||
.makeContentView(popToRootTab: $popToRootTab)
|
|
||||||
.opacity(tab == selectedTab ? 1 : 0)
|
|
||||||
.transition(.opacity)
|
|
||||||
.id("\(tab)\(appAccountsManager.currentAccount.id)")
|
|
||||||
.onAppear {
|
|
||||||
sideBarLoadedTabs.insert(tab)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if appAccountsManager.currentClient.isAuth,
|
|
||||||
userPreferences.showiPadSecondaryColumn
|
|
||||||
{
|
|
||||||
Divider().edgesIgnoringSafeArea(.all)
|
|
||||||
notificationsSecondaryColumn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onChange(of: $appAccountsManager.currentAccount.id) { _ in
|
|
||||||
sideBarLoadedTabs.removeAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var notificationsSecondaryColumn: some View {
|
|
||||||
NotificationsTab(popToRootTab: $popToRootTab, lockedType: nil)
|
|
||||||
.environment(\.isSecondaryColumn, true)
|
|
||||||
.frame(maxWidth: .secondaryColumnWidth)
|
|
||||||
.id(appAccountsManager.currentAccount.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var tabBarView: some View {
|
|
||||||
TabView(selection: .init(get: {
|
|
||||||
selectedTab
|
|
||||||
}, set: { newTab in
|
|
||||||
if newTab == selectedTab {
|
|
||||||
/// Stupid hack to trigger onChange binding in tab views.
|
|
||||||
popToRootTab = .other
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
|
||||||
popToRootTab = selectedTab
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedTab = newTab
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
HapticManager.shared.fireHaptic(of: .tabSelection)
|
|
||||||
SoundEffectManager.shared.playSound(of: .tabSelection)
|
|
||||||
|
|
||||||
if selectedTab == .notifications,
|
|
||||||
let token = appAccountsManager.currentAccount.oauthToken
|
|
||||||
{
|
|
||||||
userPreferences.setNotification(count: 0, token: token)
|
|
||||||
watcher.unreadNotificationsCount = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})) {
|
|
||||||
ForEach(availableTabs) { tab in
|
|
||||||
tab.makeContentView(popToRootTab: $popToRootTab)
|
|
||||||
.tabItem {
|
|
||||||
if userPreferences.showiPhoneTabLabel {
|
|
||||||
tab.label
|
|
||||||
.labelStyle(TitleAndIconLabelStyle())
|
|
||||||
} else {
|
|
||||||
tab.label
|
|
||||||
.labelStyle(IconOnlyLabelStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tag(tab)
|
|
||||||
.badge(badgeFor(tab: tab))
|
|
||||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .tabBar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.id(appAccountsManager.currentClient.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setNewClientsInEnv(client: Client) {
|
|
||||||
currentAccount.setClient(client: client)
|
|
||||||
currentInstance.setClient(client: client)
|
|
||||||
userPreferences.setClient(client: client)
|
|
||||||
Task {
|
|
||||||
await currentInstance.fetchCurrentInstance()
|
|
||||||
watcher.setClient(client: client, instanceStreamingURL: currentInstance.instance?.urls?.streamingApi)
|
|
||||||
watcher.watch(streams: [.user, .direct])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleScenePhase(scenePhase: ScenePhase) {
|
|
||||||
switch scenePhase {
|
|
||||||
case .background:
|
|
||||||
watcher.stopWatching()
|
|
||||||
case .active:
|
|
||||||
watcher.watch(streams: [.user, .direct])
|
|
||||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
|
||||||
Task {
|
|
||||||
await userPreferences.refreshServerPreferences()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupRevenueCat() {
|
|
||||||
Purchases.logLevel = .error
|
|
||||||
Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi")
|
|
||||||
Purchases.shared.getCustomerInfo { info, _ in
|
|
||||||
if info?.entitlements["Supporter"]?.isActive == true {
|
|
||||||
isSupporter = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshPushSubs() {
|
|
||||||
PushNotificationsService.shared.requestPushNotifications()
|
|
||||||
}
|
|
||||||
|
|
||||||
@CommandsBuilder
|
|
||||||
private var appMenu: some Commands {
|
|
||||||
CommandGroup(replacing: .newItem) {
|
|
||||||
Button("menu.new-post") {
|
|
||||||
sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CommandGroup(replacing: .textFormatting) {
|
|
||||||
Menu("menu.font") {
|
|
||||||
Button("menu.font.bigger") {
|
|
||||||
if theme.fontSizeScale < 1.5 {
|
|
||||||
theme.fontSizeScale += 0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button("menu.font.smaller") {
|
|
||||||
if theme.fontSizeScale > 0.5 {
|
|
||||||
theme.fontSizeScale -= 0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
|
||||||
let themeObserver = ThemeObserverViewController(nibName: nil, bundle: nil)
|
|
||||||
|
|
||||||
func application(_: UIApplication,
|
|
||||||
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
|
|
||||||
{
|
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
|
||||||
PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func application(_: UIApplication,
|
|
||||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
|
||||||
{
|
|
||||||
PushNotificationsService.shared.pushToken = deviceToken
|
|
||||||
Task {
|
|
||||||
PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts)
|
|
||||||
await PushNotificationsService.shared.updateSubscriptions(forceCreate: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
|
|
||||||
|
|
||||||
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
|
||||||
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
|
|
||||||
if connectingSceneSession.role == .windowApplication {
|
|
||||||
configuration.delegateClass = SceneDelegate.self
|
|
||||||
}
|
|
||||||
return configuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ThemeObserverViewController: UIViewController {
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
|
||||||
|
|
||||||
print(traitCollection.userInterfaceStyle.rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
180
IceCubesApp/App/Main/AppView.swift
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
import Account
|
||||||
|
import AppAccount
|
||||||
|
import AVFoundation
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import KeychainSwift
|
||||||
|
import MediaUI
|
||||||
|
import Network
|
||||||
|
import RevenueCat
|
||||||
|
import StatusKit
|
||||||
|
import SwiftUI
|
||||||
|
import Timeline
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct AppView: View {
|
||||||
|
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||||
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(StreamWatcher.self) private var watcher
|
||||||
|
|
||||||
|
@Environment(\.openWindow) var openWindow
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
|
@Binding var selectedTab: AppTab
|
||||||
|
@Binding var appRouterPath: RouterPath
|
||||||
|
|
||||||
|
@State var iosTabs = iOSTabs.shared
|
||||||
|
@State var sidebarTabs = SidebarTabs.shared
|
||||||
|
@State var selectedTabScrollToTop: Int = -1
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
#if os(visionOS)
|
||||||
|
tabBarView
|
||||||
|
#else
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
sidebarView
|
||||||
|
} else {
|
||||||
|
tabBarView
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var availableTabs: [AppTab] {
|
||||||
|
guard appAccountsManager.currentClient.isAuth else {
|
||||||
|
return AppTab.loggedOutTab()
|
||||||
|
}
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact {
|
||||||
|
return iosTabs.tabs
|
||||||
|
} else if UIDevice.current.userInterfaceIdiom == .vision {
|
||||||
|
return AppTab.visionOSTab()
|
||||||
|
}
|
||||||
|
return sidebarTabs.tabs.map { $0.tab }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var tabBarView: some View {
|
||||||
|
TabView(selection: .init(get: {
|
||||||
|
selectedTab
|
||||||
|
}, set: { newTab in
|
||||||
|
updateTab(with: newTab)
|
||||||
|
})) {
|
||||||
|
ForEach(availableTabs) { tab in
|
||||||
|
tab.makeContentView(selectedTab: $selectedTab)
|
||||||
|
.tabItem {
|
||||||
|
if userPreferences.showiPhoneTabLabel {
|
||||||
|
tab.label
|
||||||
|
.environment(\.symbolVariants, tab == selectedTab ? .fill : .none)
|
||||||
|
} else {
|
||||||
|
Image(systemName: tab.iconName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tag(tab)
|
||||||
|
.badge(badgeFor(tab: tab))
|
||||||
|
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .tabBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.id(appAccountsManager.currentClient.id)
|
||||||
|
.withSheetDestinations(sheetDestinations: $appRouterPath.presentedSheet)
|
||||||
|
.environment(\.selectedTabScrollToTop, selectedTabScrollToTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTab(with newTab: AppTab) {
|
||||||
|
if newTab == .post {
|
||||||
|
#if os(visionOS)
|
||||||
|
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||||
|
#else
|
||||||
|
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||||
|
#endif
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
HapticManager.shared.fireHaptic(.tabSelection)
|
||||||
|
SoundEffectManager.shared.playSound(.tabSelection)
|
||||||
|
|
||||||
|
if selectedTab == newTab {
|
||||||
|
selectedTabScrollToTop = newTab.rawValue
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
selectedTabScrollToTop = -1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedTabScrollToTop = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTab = newTab
|
||||||
|
}
|
||||||
|
|
||||||
|
private func badgeFor(tab: AppTab) -> Int {
|
||||||
|
if tab == .notifications, selectedTab != tab,
|
||||||
|
let token = appAccountsManager.currentAccount.oauthToken
|
||||||
|
{
|
||||||
|
return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(visionOS)
|
||||||
|
var sidebarView: some View {
|
||||||
|
SideBarView(selectedTab: .init(get: {
|
||||||
|
selectedTab
|
||||||
|
}, set: { newTab in
|
||||||
|
updateTab(with: newTab)
|
||||||
|
}), tabs: availableTabs)
|
||||||
|
{
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
baseTabView
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
.tabViewStyle(.sidebarAdaptable)
|
||||||
|
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
|
||||||
|
tabview.sidebar.isHidden = true
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
.tabViewStyle(.tabBarOnly)
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
baseTabView
|
||||||
|
}
|
||||||
|
if horizontalSizeClass == .regular,
|
||||||
|
appAccountsManager.currentClient.isAuth,
|
||||||
|
userPreferences.showiPadSecondaryColumn
|
||||||
|
{
|
||||||
|
Divider().edgesIgnoringSafeArea(.all)
|
||||||
|
notificationsSecondaryColumn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environment(appRouterPath)
|
||||||
|
.environment(\.selectedTabScrollToTop, selectedTabScrollToTop)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private var baseTabView: some View {
|
||||||
|
TabView(selection: $selectedTab) {
|
||||||
|
ForEach(availableTabs) { tab in
|
||||||
|
tab
|
||||||
|
.makeContentView(selectedTab: $selectedTab)
|
||||||
|
.toolbar(horizontalSizeClass == .regular ? .hidden : .visible, for: .tabBar)
|
||||||
|
.tabItem {
|
||||||
|
tab.label
|
||||||
|
}
|
||||||
|
.tag(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
|
||||||
|
tabview.tabBar.isHidden = horizontalSizeClass == .regular
|
||||||
|
tabview.customizableViewControllers = []
|
||||||
|
tabview.moreNavigationController.isNavigationBarHidden = true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationsSecondaryColumn: some View {
|
||||||
|
NotificationsTab(selectedTab: .constant(.notifications)
|
||||||
|
, lockedType: nil)
|
||||||
|
.environment(\.isSecondaryColumn, true)
|
||||||
|
.frame(maxWidth: .secondaryColumnWidth)
|
||||||
|
.id(appAccountsManager.currentAccount.id)
|
||||||
|
}
|
||||||
|
}
|
70
IceCubesApp/App/Main/IceCubesApp+Menu.swift
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension IceCubesApp {
|
||||||
|
@CommandsBuilder
|
||||||
|
var appMenu: some Commands {
|
||||||
|
CommandGroup(replacing: .appSettings) {
|
||||||
|
Button("menu.settings") {
|
||||||
|
appRouterPath.presentedSheet = .settings
|
||||||
|
}
|
||||||
|
.keyboardShortcut(",", modifiers: .command)
|
||||||
|
}
|
||||||
|
CommandGroup(replacing: .newItem) {
|
||||||
|
Button("menu.new-window") {
|
||||||
|
openWindow(id: "MainWindow")
|
||||||
|
}
|
||||||
|
.keyboardShortcut("n", modifiers: .shift)
|
||||||
|
Button("menu.new-post") {
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||||
|
#else
|
||||||
|
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.keyboardShortcut("n", modifiers: .command)
|
||||||
|
}
|
||||||
|
CommandGroup(replacing: .textFormatting) {
|
||||||
|
Menu("menu.font") {
|
||||||
|
Button("menu.font.bigger") {
|
||||||
|
if theme.fontSizeScale < 1.5 {
|
||||||
|
theme.fontSizeScale += 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("menu.font.smaller") {
|
||||||
|
if theme.fontSizeScale > 0.5 {
|
||||||
|
theme.fontSizeScale -= 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CommandMenu("tab.timeline") {
|
||||||
|
Button("timeline.latest") {
|
||||||
|
NotificationCenter.default.post(name: .refreshTimeline, object: nil)
|
||||||
|
}
|
||||||
|
.keyboardShortcut("r", modifiers: .command)
|
||||||
|
Button("timeline.home") {
|
||||||
|
NotificationCenter.default.post(name: .homeTimeline, object: nil)
|
||||||
|
}
|
||||||
|
.keyboardShortcut("h", modifiers: .shift)
|
||||||
|
Button("timeline.trending") {
|
||||||
|
NotificationCenter.default.post(name: .trendingTimeline, object: nil)
|
||||||
|
}
|
||||||
|
.keyboardShortcut("t", modifiers: .shift)
|
||||||
|
Button("timeline.federated") {
|
||||||
|
NotificationCenter.default.post(name: .federatedTimeline, object: nil)
|
||||||
|
}
|
||||||
|
.keyboardShortcut("f", modifiers: .shift)
|
||||||
|
Button("timeline.local") {
|
||||||
|
NotificationCenter.default.post(name: .localTimeline, object: nil)
|
||||||
|
}
|
||||||
|
.keyboardShortcut("l", modifiers: .shift)
|
||||||
|
}
|
||||||
|
CommandGroup(replacing: .help) {
|
||||||
|
Button("menu.help.github") {
|
||||||
|
let url = URL(string: "https://github.com/Dimillian/IceCubesApp/issues")!
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
169
IceCubesApp/App/Main/IceCubesApp+Scene.swift
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import AppIntents
|
||||||
|
import Env
|
||||||
|
import MediaUI
|
||||||
|
import StatusKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension IceCubesApp {
|
||||||
|
var appScene: some Scene {
|
||||||
|
WindowGroup(id: "MainWindow") {
|
||||||
|
AppView(selectedTab: $selectedTab, appRouterPath: $appRouterPath)
|
||||||
|
.applyTheme(theme)
|
||||||
|
.onAppear {
|
||||||
|
setNewClientsInEnv(client: appAccountsManager.currentClient)
|
||||||
|
setupRevenueCat()
|
||||||
|
refreshPushSubs()
|
||||||
|
}
|
||||||
|
.environment(appAccountsManager)
|
||||||
|
.environment(appAccountsManager.currentClient)
|
||||||
|
.environment(quickLook)
|
||||||
|
.environment(currentAccount)
|
||||||
|
.environment(currentInstance)
|
||||||
|
.environment(userPreferences)
|
||||||
|
.environment(theme)
|
||||||
|
.environment(watcher)
|
||||||
|
.environment(pushNotificationsService)
|
||||||
|
.environment(appIntentService)
|
||||||
|
.environment(\.isSupporter, isSupporter)
|
||||||
|
.sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
MediaUIView(selectedAttachment: selectedMediaAttachment,
|
||||||
|
attachments: quickLook.mediaAttachments)
|
||||||
|
.presentationBackground(.ultraThinMaterial)
|
||||||
|
.presentationCornerRadius(16)
|
||||||
|
.presentationSizing(.page)
|
||||||
|
.withEnvironments()
|
||||||
|
} else {
|
||||||
|
MediaUIView(selectedAttachment: selectedMediaAttachment,
|
||||||
|
attachments: quickLook.mediaAttachments)
|
||||||
|
.presentationBackground(.ultraThinMaterial)
|
||||||
|
.presentationCornerRadius(16)
|
||||||
|
.withEnvironments()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
|
||||||
|
if newValue != nil {
|
||||||
|
pushNotificationsService.handledNotification = nil
|
||||||
|
if appAccountsManager.currentAccount.oauthToken?.accessToken != newValue?.account.token.accessToken,
|
||||||
|
let account = appAccountsManager.availableAccounts.first(where:
|
||||||
|
{ $0.oauthToken?.accessToken == newValue?.account.token.accessToken })
|
||||||
|
{
|
||||||
|
appAccountsManager.currentAccount = account
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||||
|
selectedTab = .notifications
|
||||||
|
pushNotificationsService.handledNotification = newValue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedTab = .notifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: appIntentService.handledIntent) { _, _ in
|
||||||
|
if let intent = appIntentService.handledIntent?.intent {
|
||||||
|
handleIntent(intent)
|
||||||
|
appIntentService.handledIntent = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.withModelContainer()
|
||||||
|
}
|
||||||
|
.commands {
|
||||||
|
appMenu
|
||||||
|
}
|
||||||
|
.onChange(of: scenePhase) { _, newValue in
|
||||||
|
handleScenePhase(scenePhase: newValue)
|
||||||
|
}
|
||||||
|
.onChange(of: appAccountsManager.currentClient) { _, newValue in
|
||||||
|
setNewClientsInEnv(client: newValue)
|
||||||
|
if newValue.isAuth {
|
||||||
|
watcher.watch(streams: [.user, .direct])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
.windowResize()
|
||||||
|
#elseif os(visionOS)
|
||||||
|
.defaultSize(width: 800, height: 1200)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@SceneBuilder
|
||||||
|
var otherScenes: some Scene {
|
||||||
|
WindowGroup(for: WindowDestinationEditor.self) { destination in
|
||||||
|
Group {
|
||||||
|
switch destination.wrappedValue {
|
||||||
|
case let .newStatusEditor(visibility):
|
||||||
|
StatusEditor.MainView(mode: .new(text: nil, visibility: visibility))
|
||||||
|
case let .prefilledStatusEditor(text, visibility):
|
||||||
|
StatusEditor.MainView(mode: .new(text: text, visibility: visibility))
|
||||||
|
case let .editStatusEditor(status):
|
||||||
|
StatusEditor.MainView(mode: .edit(status: status))
|
||||||
|
case let .quoteStatusEditor(status):
|
||||||
|
StatusEditor.MainView(mode: .quote(status: status))
|
||||||
|
case let .replyToStatusEditor(status):
|
||||||
|
StatusEditor.MainView(mode: .replyTo(status: status))
|
||||||
|
case let .mentionStatusEditor(account, visibility):
|
||||||
|
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
||||||
|
case let .quoteLinkStatusEditor(link):
|
||||||
|
StatusEditor.MainView(mode: .quoteLink(link: link))
|
||||||
|
case .none:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.withEnvironments()
|
||||||
|
.environment(\.isCatalystWindow, true)
|
||||||
|
.environment(RouterPath())
|
||||||
|
.withModelContainer()
|
||||||
|
.applyTheme(theme)
|
||||||
|
.frame(minWidth: 300, minHeight: 400)
|
||||||
|
}
|
||||||
|
.defaultSize(width: 600, height: 800)
|
||||||
|
.windowResizability(.contentMinSize)
|
||||||
|
|
||||||
|
WindowGroup(for: WindowDestinationMedia.self) { destination in
|
||||||
|
Group {
|
||||||
|
switch destination.wrappedValue {
|
||||||
|
case let .mediaViewer(attachments, selectedAttachment):
|
||||||
|
MediaUIView(selectedAttachment: selectedAttachment,
|
||||||
|
attachments: attachments)
|
||||||
|
case .none:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.withEnvironments()
|
||||||
|
.withModelContainer()
|
||||||
|
.applyTheme(theme)
|
||||||
|
.environment(\.isCatalystWindow, true)
|
||||||
|
.frame(minWidth: 300, minHeight: 400)
|
||||||
|
}
|
||||||
|
.defaultSize(width: 1200, height: 1000)
|
||||||
|
.windowResizability(.contentMinSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleIntent(_: any AppIntent) {
|
||||||
|
if let postIntent = appIntentService.handledIntent?.intent as? PostIntent {
|
||||||
|
#if os(visionOS) || os(macOS)
|
||||||
|
openWindow(value: WindowDestinationEditor.prefilledStatusEditor(text: postIntent.content ?? "",
|
||||||
|
visibility: userPreferences.postVisibility))
|
||||||
|
#else
|
||||||
|
appRouterPath.presentedSheet = .prefilledStatusEditor(text: postIntent.content ?? "",
|
||||||
|
visibility: userPreferences.postVisibility)
|
||||||
|
#endif
|
||||||
|
} else if let tabIntent = appIntentService.handledIntent?.intent as? TabIntent {
|
||||||
|
selectedTab = tabIntent.tab.toAppTab
|
||||||
|
} else if let imageIntent = appIntentService.handledIntent?.intent as? PostImageIntent,
|
||||||
|
let urls = imageIntent.images?.compactMap({ $0.fileURL })
|
||||||
|
{
|
||||||
|
appRouterPath.presentedSheet = .imageURL(urls: urls,
|
||||||
|
visibility: userPreferences.postVisibility)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Scene {
|
||||||
|
func windowResize() -> some Scene {
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
return self.windowResizability(.contentSize)
|
||||||
|
} else {
|
||||||
|
return self.defaultSize(width: 1100, height: 1400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
128
IceCubesApp/App/Main/IceCubesApp.swift
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import Account
|
||||||
|
import AppAccount
|
||||||
|
import AVFoundation
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import KeychainSwift
|
||||||
|
import MediaUI
|
||||||
|
import Network
|
||||||
|
import RevenueCat
|
||||||
|
import StatusKit
|
||||||
|
import SwiftUI
|
||||||
|
import Timeline
|
||||||
|
import WishKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct IceCubesApp: App {
|
||||||
|
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
||||||
|
|
||||||
|
@Environment(\.scenePhase) var scenePhase
|
||||||
|
@Environment(\.openWindow) var openWindow
|
||||||
|
|
||||||
|
@State var appAccountsManager = AppAccountsManager.shared
|
||||||
|
@State var currentInstance = CurrentInstance.shared
|
||||||
|
@State var currentAccount = CurrentAccount.shared
|
||||||
|
@State var userPreferences = UserPreferences.shared
|
||||||
|
@State var pushNotificationsService = PushNotificationsService.shared
|
||||||
|
@State var appIntentService = AppIntentService.shared
|
||||||
|
@State var watcher = StreamWatcher.shared
|
||||||
|
@State var quickLook = QuickLook.shared
|
||||||
|
@State var theme = Theme.shared
|
||||||
|
|
||||||
|
@State var selectedTab: AppTab = .timeline
|
||||||
|
@State var appRouterPath = RouterPath()
|
||||||
|
|
||||||
|
@State var isSupporter: Bool = false
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
appScene
|
||||||
|
otherScenes
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNewClientsInEnv(client: Client) {
|
||||||
|
currentAccount.setClient(client: client)
|
||||||
|
currentInstance.setClient(client: client)
|
||||||
|
userPreferences.setClient(client: client)
|
||||||
|
Task {
|
||||||
|
await currentInstance.fetchCurrentInstance()
|
||||||
|
watcher.setClient(client: client, instanceStreamingURL: currentInstance.instance?.urls?.streamingApi)
|
||||||
|
watcher.watch(streams: [.user, .direct])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleScenePhase(scenePhase: ScenePhase) {
|
||||||
|
switch scenePhase {
|
||||||
|
case .background:
|
||||||
|
watcher.stopWatching()
|
||||||
|
case .active:
|
||||||
|
watcher.watch(streams: [.user, .direct])
|
||||||
|
UNUserNotificationCenter.current().setBadgeCount(0)
|
||||||
|
userPreferences.reloadNotificationsCount(tokens: appAccountsManager.availableAccounts.compactMap(\.oauthToken))
|
||||||
|
Task {
|
||||||
|
await userPreferences.refreshServerPreferences()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRevenueCat() {
|
||||||
|
Purchases.logLevel = .error
|
||||||
|
Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi")
|
||||||
|
Purchases.shared.getCustomerInfo { info, _ in
|
||||||
|
if info?.entitlements["Supporter"]?.isActive == true {
|
||||||
|
isSupporter = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshPushSubs() {
|
||||||
|
PushNotificationsService.shared.requestPushNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
func application(_: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
|
||||||
|
{
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts)
|
||||||
|
Telemetry.setup()
|
||||||
|
Telemetry.signal("app.launched")
|
||||||
|
WishKit.configure(with: "AF21AE07-3BA9-4FE2-BFB1-59A3B3941730")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_: UIApplication,
|
||||||
|
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||||
|
{
|
||||||
|
PushNotificationsService.shared.pushToken = deviceToken
|
||||||
|
Task {
|
||||||
|
PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts)
|
||||||
|
await PushNotificationsService.shared.updateSubscriptions(forceCreate: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
|
||||||
|
|
||||||
|
func application(_: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
|
||||||
|
UserPreferences.shared.reloadNotificationsCount(tokens: AppAccountsManager.shared.availableAccounts.compactMap(\.oauthToken))
|
||||||
|
return .noData
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||||
|
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
|
||||||
|
if connectingSceneSession.role == .windowApplication {
|
||||||
|
configuration.delegateClass = SceneDelegate.self
|
||||||
|
}
|
||||||
|
return configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
override func buildMenu(with builder: UIMenuBuilder) {
|
||||||
|
super.buildMenu(with: builder)
|
||||||
|
builder.remove(menu: .document)
|
||||||
|
builder.remove(menu: .toolbar)
|
||||||
|
builder.remove(menu: .sidebar)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,96 +0,0 @@
|
||||||
import QuickLook
|
|
||||||
import SwiftUI
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension URL: Identifiable {
|
|
||||||
public var id: String {
|
|
||||||
absoluteString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct QuickLookPreview: UIViewControllerRepresentable {
|
|
||||||
let selectedURL: URL
|
|
||||||
let urls: [URL]
|
|
||||||
|
|
||||||
func makeUIViewController(context _: Context) -> UIViewController {
|
|
||||||
return AppQLPreviewController(selectedURL: selectedURL, urls: urls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIViewController(
|
|
||||||
_: UIViewController, context _: Context
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppQLPreviewController: UIViewController {
|
|
||||||
let selectedURL: URL
|
|
||||||
let urls: [URL]
|
|
||||||
|
|
||||||
var qlController: QLPreviewController?
|
|
||||||
|
|
||||||
init(selectedURL: URL, urls: [URL]) {
|
|
||||||
self.selectedURL = selectedURL
|
|
||||||
self.urls = urls
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(*, unavailable)
|
|
||||||
required init?(coder _: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
if qlController == nil {
|
|
||||||
qlController = QLPreviewController()
|
|
||||||
qlController?.dataSource = self
|
|
||||||
qlController?.delegate = self
|
|
||||||
qlController?.currentPreviewItemIndex = urls.firstIndex(of: selectedURL) ?? 0
|
|
||||||
present(qlController!, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppQLPreviewController: QLPreviewControllerDataSource {
|
|
||||||
func numberOfPreviewItems(in _: QLPreviewController) -> Int {
|
|
||||||
return urls.count
|
|
||||||
}
|
|
||||||
|
|
||||||
func previewController(_: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
|
||||||
return urls[index] as QLPreviewItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppQLPreviewController: QLPreviewControllerDelegate {
|
|
||||||
func previewController(_: QLPreviewController, editingModeFor _: QLPreviewItem) -> QLPreviewItemEditingMode {
|
|
||||||
.createCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
func previewControllerWillDismiss(_: QLPreviewController) {
|
|
||||||
dismiss(animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func previewControllerDidDismiss(_: QLPreviewController) {
|
|
||||||
dismiss(animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TransparentBackground: UIViewControllerRepresentable {
|
|
||||||
public func makeUIViewController(context _: Context) -> UIViewController {
|
|
||||||
return TransparentController()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func updateUIViewController(_: UIViewController, context _: Context) {}
|
|
||||||
|
|
||||||
class TransparentController: UIViewController {
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
view.backgroundColor = .clear
|
|
||||||
}
|
|
||||||
|
|
||||||
override func willMove(toParent parent: UIViewController?) {
|
|
||||||
super.willMove(toParent: parent)
|
|
||||||
parent?.view?.backgroundColor = .clear
|
|
||||||
parent?.modalPresentationStyle = .overCurrentContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,14 +2,14 @@ import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Status
|
import StatusKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct ReportView: View {
|
public struct ReportView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var client: Client
|
@Environment(Client.self) private var client
|
||||||
|
|
||||||
let status: Status
|
let status: Status
|
||||||
@State private var commentText: String = ""
|
@State private var commentText: String = ""
|
||||||
|
@ -35,9 +35,11 @@ public struct ReportView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("report.title")
|
.navigationTitle("report.title")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
|
@ -63,13 +65,7 @@ public struct ReportView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
CancelToolbarItem()
|
||||||
Button {
|
|
||||||
dismiss()
|
|
||||||
} label: {
|
|
||||||
Text("action.cancel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,49 @@
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
import Models
|
||||||
|
import Observation
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import AppAccount
|
||||||
|
import WebKit
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func withSafariRouter() -> some View {
|
@MainActor func withSafariRouter() -> some View {
|
||||||
modifier(SafariRouter())
|
modifier(SafariRouter())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private struct SafariRouter: ViewModifier {
|
private struct SafariRouter: ViewModifier {
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@Environment(UserPreferences.self) private var preferences
|
||||||
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
@Environment(AppAccountsManager.self) private var appAccount
|
||||||
|
|
||||||
@StateObject private var safariManager = InAppSafariManager()
|
#if !os(visionOS)
|
||||||
|
@State private var safariManager = InAppSafariManager()
|
||||||
|
#endif
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.environment(\.openURL, OpenURLAction { url in
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
// Open internal URL.
|
// Open internal URL.
|
||||||
routerPath.handle(url: url)
|
guard !isSecondaryColumn else { return .discarded }
|
||||||
|
return routerPath.handle(url: url)
|
||||||
})
|
})
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
// Open external URL (from icecubesapp://)
|
// Open external URL (from icecubesapp://)
|
||||||
let urlString = url.absoluteString.replacingOccurrences(of: "icecubesapp://", with: "https://")
|
guard !isSecondaryColumn else { return }
|
||||||
|
if url.absoluteString == "icecubesapp://subclub" {
|
||||||
|
#if !os(visionOS)
|
||||||
|
safariManager.dismiss()
|
||||||
|
#endif
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let urlString = url.absoluteString.replacingOccurrences(of: AppInfo.scheme, with: "https://")
|
||||||
guard let url = URL(string: urlString), url.host != nil else { return }
|
guard let url = URL(string: urlString), url.host != nil else { return }
|
||||||
_ = routerPath.handle(url: url)
|
_ = routerPath.handleDeepLink(url: url)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routerPath.urlHandler = { url in
|
routerPath.urlHandler = { url in
|
||||||
|
@ -39,31 +56,56 @@ private struct SafariRouter: ViewModifier {
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
return .handled
|
return .handled
|
||||||
}
|
}
|
||||||
|
} else if url.query()?.contains("callback=") == false,
|
||||||
|
url.host() == AppInfo.premiumInstance,
|
||||||
|
let accountName = appAccount.currentAccount.accountName {
|
||||||
|
let newURL = url.appending(queryItems: [
|
||||||
|
.init(name: "callback", value: "icecubesapp://subclub"),
|
||||||
|
.init(name: "id", value: "@\(accountName)")
|
||||||
|
])
|
||||||
|
|
||||||
|
#if !os(visionOS)
|
||||||
|
return safariManager.open(newURL)
|
||||||
|
#else
|
||||||
|
return .systemAction
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
guard preferences.preferredBrowser == .inAppSafari, !ProcessInfo.processInfo.isiOSAppOnMac else { return .systemAction }
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
|
||||||
// SFSafariViewController only supports initial URLs with http:// or https:// schemes.
|
// SFSafariViewController only supports initial URLs with http:// or https:// schemes.
|
||||||
guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else {
|
guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else {
|
||||||
return .systemAction
|
return .systemAction
|
||||||
}
|
}
|
||||||
|
#if os(visionOS)
|
||||||
|
return .systemAction
|
||||||
|
#else
|
||||||
return safariManager.open(url)
|
return safariManager.open(url)
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
return .systemAction
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.background {
|
.background {
|
||||||
WindowReader { window in
|
WindowReader { window in
|
||||||
self.safariManager.windowScene = window.windowScene
|
safariManager.windowScene = window.windowScene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InAppSafariManager: NSObject, ObservableObject, SFSafariViewControllerDelegate {
|
#if !os(visionOS)
|
||||||
|
@MainActor
|
||||||
|
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
|
||||||
var windowScene: UIWindowScene?
|
var windowScene: UIWindowScene?
|
||||||
let viewController: UIViewController = .init()
|
let viewController: UIViewController = .init()
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func open(_ url: URL) -> OpenURLAction.Result {
|
func open(_ url: URL) -> OpenURLAction.Result {
|
||||||
guard let windowScene = windowScene else { return .systemAction }
|
guard let windowScene else { return .systemAction }
|
||||||
|
|
||||||
window = setupWindow(windowScene: windowScene)
|
window = setupWindow(windowScene: windowScene)
|
||||||
|
|
||||||
|
@ -82,8 +124,15 @@ private class InAppSafariManager: NSObject, ObservableObject, SFSafariViewContro
|
||||||
return .handled
|
return .handled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dismiss() {
|
||||||
|
viewController.presentedViewController?.dismiss(animated: true)
|
||||||
|
window?.resignKey()
|
||||||
|
window?.isHidden = false
|
||||||
|
window = nil
|
||||||
|
}
|
||||||
|
|
||||||
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
|
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
|
||||||
let window = self.window ?? UIWindow(windowScene: windowScene)
|
let window = window ?? UIWindow(windowScene: windowScene)
|
||||||
|
|
||||||
window.rootViewController = viewController
|
window.rootViewController = viewController
|
||||||
window.makeKeyAndVisible()
|
window.makeKeyAndVisible()
|
||||||
|
@ -99,12 +148,15 @@ private class InAppSafariManager: NSObject, ObservableObject, SFSafariViewContro
|
||||||
return window
|
return window
|
||||||
}
|
}
|
||||||
|
|
||||||
func safariViewControllerDidFinish(_: SFSafariViewController) {
|
nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) {
|
||||||
|
Task { @MainActor in
|
||||||
window?.resignKey()
|
window?.resignKey()
|
||||||
window?.isHidden = false
|
window?.isHidden = false
|
||||||
window = nil
|
window = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private struct WindowReader: UIViewRepresentable {
|
private struct WindowReader: UIViewRepresentable {
|
||||||
var onUpdate: (UIWindow) -> Void
|
var onUpdate: (UIWindow) -> Void
|
||||||
|
|
|
@ -4,40 +4,61 @@ import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftUIIntrospect
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct SideBarView<Content: View>: View {
|
struct SideBarView<Content: View>: View {
|
||||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
@Environment(\.openWindow) private var openWindow
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@EnvironmentObject private var theme: Theme
|
|
||||||
@EnvironmentObject private var watcher: StreamWatcher
|
|
||||||
@EnvironmentObject private var userPreferences: UserPreferences
|
|
||||||
|
|
||||||
@Binding var selectedTab: Tab
|
@Environment(AppAccountsManager.self) private var appAccounts
|
||||||
@Binding var popToRootTab: Tab
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
var tabs: [Tab]
|
@Environment(Theme.self) private var theme
|
||||||
@ObservedObject var routerPath = RouterPath()
|
@Environment(StreamWatcher.self) private var watcher
|
||||||
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
|
||||||
|
@Binding var selectedTab: AppTab
|
||||||
|
var tabs: [AppTab]
|
||||||
@ViewBuilder var content: () -> Content
|
@ViewBuilder var content: () -> Content
|
||||||
|
|
||||||
private func badgeFor(tab: Tab) -> Int {
|
@State private var sidebarTabs = SidebarTabs.shared
|
||||||
if tab == .notifications && selectedTab != tab,
|
|
||||||
|
private func badgeFor(tab: AppTab) -> Int {
|
||||||
|
if tab == .notifications, selectedTab != tab,
|
||||||
let token = appAccounts.currentAccount.oauthToken
|
let token = appAccounts.currentAccount.oauthToken
|
||||||
{
|
{
|
||||||
return watcher.unreadNotificationsCount + userPreferences.getNotificationsCount(for: token)
|
return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeIconForTab(tab: Tab) -> some View {
|
private func makeIconForTab(tab: AppTab) -> some View {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
|
HStack {
|
||||||
SideBarIcon(systemIconName: tab.iconName,
|
SideBarIcon(systemIconName: tab.iconName,
|
||||||
isSelected: tab == selectedTab)
|
isSelected: tab == selectedTab)
|
||||||
|
if userPreferences.isSidebarExpanded {
|
||||||
|
Text(tab.title)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24, height: 50)
|
||||||
|
.background(tab == selectedTab ? theme.primaryBackgroundColor : .clear,
|
||||||
|
in: RoundedRectangle(cornerRadius: 8))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.shadow(color: tab == selectedTab ? .black.opacity(0.2) : .clear, radius: 5)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.stroke(tab == selectedTab ? theme.labelColor.opacity(0.1) : .clear, lineWidth: 1)
|
||||||
|
)
|
||||||
let badge = badgeFor(tab: tab)
|
let badge = badgeFor(tab: tab)
|
||||||
if badge > 0 {
|
if badge > 0 {
|
||||||
makeBadgeView(count: badge)
|
makeBadgeView(count: badge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
|
||||||
.frame(width: .sidebarWidth, height: 50)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeBadgeView(count: Int) -> some View {
|
private func makeBadgeView(count: Int) -> some View {
|
||||||
|
@ -49,27 +70,32 @@ struct SideBarView<Content: View>: View {
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
}
|
}
|
||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
.offset(x: 14, y: -14)
|
.offset(x: 5, y: -5)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var postButton: some View {
|
private var postButton: some View {
|
||||||
Button {
|
Button {
|
||||||
|
#if targetEnvironment(macCatalyst) || os(visionOS)
|
||||||
|
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||||
|
#else
|
||||||
routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||||
|
#endif
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "square.and.pencil")
|
Image(systemName: "square.and.pencil")
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 20, height: 30)
|
.frame(width: 20, height: 30)
|
||||||
|
.offset(x: 2, y: -2)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.keyboardShortcut("n", modifiers: .command)
|
.help(AppTab.post.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View {
|
private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View {
|
||||||
Button {
|
Button {
|
||||||
if account.id == appAccounts.currentAccount.id {
|
if account.id == appAccounts.currentAccount.id {
|
||||||
selectedTab = .profile
|
selectedTab = .profile
|
||||||
SoundEffectManager.shared.playSound(of: .tabSelection)
|
SoundEffectManager.shared.playSound(.tabSelection)
|
||||||
} else {
|
} else {
|
||||||
var transation = Transaction()
|
var transation = Transaction()
|
||||||
transation.disablesAnimations = true
|
transation.disablesAnimations = true
|
||||||
|
@ -79,47 +105,69 @@ struct SideBarView<Content: View>: View {
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
AppAccountView(viewModel: .init(appAccount: account, isCompact: true))
|
if userPreferences.isSidebarExpanded {
|
||||||
if showBadge,
|
AppAccountView(viewModel: .init(appAccount: account,
|
||||||
|
isCompact: false,
|
||||||
|
isInSettings: false),
|
||||||
|
isParentPresented: .constant(false))
|
||||||
|
} else {
|
||||||
|
AppAccountView(viewModel: .init(appAccount: account,
|
||||||
|
isCompact: true,
|
||||||
|
isInSettings: false),
|
||||||
|
isParentPresented: .constant(false))
|
||||||
|
}
|
||||||
|
if !userPreferences.isSidebarExpanded,
|
||||||
|
showBadge,
|
||||||
let token = account.oauthToken,
|
let token = account.oauthToken,
|
||||||
userPreferences.getNotificationsCount(for: token) > 0
|
let notificationsCount = userPreferences.notificationsCount[token],
|
||||||
|
notificationsCount > 0
|
||||||
{
|
{
|
||||||
makeBadgeView(count: userPreferences.getNotificationsCount(for: token))
|
makeBadgeView(count: notificationsCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.leading, userPreferences.isSidebarExpanded ? 16 : 0)
|
||||||
}
|
}
|
||||||
.frame(width: .sidebarWidth, height: 50)
|
.help(accountButtonTitle(accountName: account.accountName))
|
||||||
|
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth, height: 50)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.background(selectedTab == .profile && account.id == appAccounts.currentAccount.id ?
|
.background(selectedTab == .profile && account.id == appAccounts.currentAccount.id ?
|
||||||
theme.secondaryBackgroundColor : .clear)
|
theme.secondaryBackgroundColor : .clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func accountButtonTitle(accountName: String?) -> LocalizedStringKey {
|
||||||
|
if let accountName {
|
||||||
|
"tab.profile-account-\(accountName)"
|
||||||
|
} else {
|
||||||
|
AppTab.profile.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var tabsView: some View {
|
private var tabsView: some View {
|
||||||
ForEach(tabs) { tab in
|
ForEach(tabs) { tab in
|
||||||
|
if tab != .profile && sidebarTabs.isEnabled(tab) {
|
||||||
Button {
|
Button {
|
||||||
if tab == selectedTab {
|
// ensure keyboard is always dismissed when selecting a tab
|
||||||
popToRootTab = .other
|
hideKeyboard()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
|
||||||
popToRootTab = tab
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedTab = tab
|
selectedTab = tab
|
||||||
SoundEffectManager.shared.playSound(of: .tabSelection)
|
SoundEffectManager.shared.playSound(.tabSelection)
|
||||||
if tab == .notifications {
|
if tab == .notifications {
|
||||||
if let token = appAccounts.currentAccount.oauthToken {
|
if let token = appAccounts.currentAccount.oauthToken {
|
||||||
userPreferences.setNotification(count: 0, token: token)
|
userPreferences.notificationsCount[token] = 0
|
||||||
}
|
}
|
||||||
watcher.unreadNotificationsCount = 0
|
watcher.unreadNotificationsCount = 0
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
makeIconForTab(tab: tab)
|
makeIconForTab(tab: tab)
|
||||||
}
|
}
|
||||||
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear)
|
.help(tab.title)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@Bindable var routerPath = routerPath
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
|
if horizontalSizeClass == .regular {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
if appAccounts.availableAccounts.isEmpty {
|
if appAccounts.availableAccounts.isEmpty {
|
||||||
|
@ -133,25 +181,38 @@ struct SideBarView<Content: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
postButton
|
|
||||||
.padding(.top, 12)
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: .sidebarWidth)
|
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(.thinMaterial)
|
.background(.thinMaterial)
|
||||||
Divider()
|
.safeAreaInset(edge: .bottom, content: {
|
||||||
.edgesIgnoringSafeArea(.top)
|
HStack(spacing: 16) {
|
||||||
|
postButton
|
||||||
|
.padding(.vertical, 24)
|
||||||
|
.padding(.leading, userPreferences.isSidebarExpanded ? 18 : 0)
|
||||||
|
if userPreferences.isSidebarExpanded {
|
||||||
|
Text("menu.new-post")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(theme.labelColor)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
|
||||||
|
.background(.thinMaterial)
|
||||||
|
})
|
||||||
|
Divider().edgesIgnoringSafeArea(.all)
|
||||||
|
}
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
.background(.thinMaterial)
|
.background(.thinMaterial)
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct SideBarIcon: View {
|
private struct SideBarIcon: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
let systemIconName: String
|
let systemIconName: String
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
|
@ -163,11 +224,20 @@ private struct SideBarIcon: View {
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(isSelected ? theme.tintColor : theme.labelColor)
|
.foregroundColor(isSelected ? theme.tintColor : theme.labelColor)
|
||||||
|
.symbolVariant(isSelected ? .fill : .none)
|
||||||
.scaleEffect(isHovered ? 0.8 : 1.0)
|
.scaleEffect(isHovered ? 0.8 : 1.0)
|
||||||
.onHover { isHovered in
|
.onHover { isHovered in
|
||||||
withAnimation(.interpolatingSpring(stiffness: 300, damping: 15)) {
|
withAnimation(.interpolatingSpring(stiffness: 300, damping: 15)) {
|
||||||
self.isHovered = isHovered
|
self.isHovered = isHovered
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(width: 50, height: 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
@MainActor func hideKeyboard() {
|
||||||
|
let resign = #selector(UIResponder.resignFirstResponder)
|
||||||
|
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,44 +4,29 @@ import Env
|
||||||
import Explore
|
import Explore
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Shimmer
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct ExploreTab: View {
|
struct ExploreTab: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@Environment(UserPreferences.self) private var preferences
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@EnvironmentObject private var client: Client
|
@Environment(Client.self) private var client
|
||||||
@StateObject private var routerPath = RouterPath()
|
@State private var routerPath = RouterPath()
|
||||||
@Binding var popToRootTab: Tab
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routerPath.path) {
|
NavigationStack(path: $routerPath.path) {
|
||||||
ExploreView()
|
ExploreView()
|
||||||
.withAppRouter()
|
.withAppRouter()
|
||||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
statusEditorToolbarItem(routerPath: routerPath,
|
ToolbarTab(routerPath: $routerPath)
|
||||||
visibility: preferences.postVisibility)
|
|
||||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
AppAccountsSelectorView(routerPath: routerPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad && !preferences.showiPadSecondaryColumn {
|
|
||||||
SecondaryColumnToolbarItem()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.withSafariRouter()
|
.withSafariRouter()
|
||||||
.environmentObject(routerPath)
|
.environment(routerPath)
|
||||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
.onChange(of: client.id) {
|
||||||
if popToRootTab == .explore {
|
|
||||||
routerPath.path = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: client.id) { _ in
|
|
||||||
routerPath.path = []
|
routerPath.path = []
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|
|
@ -5,17 +5,16 @@ import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Shimmer
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct MessagesTab: View {
|
struct MessagesTab: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var watcher: StreamWatcher
|
@Environment(StreamWatcher.self) private var watcher
|
||||||
@EnvironmentObject private var client: Client
|
@Environment(Client.self) private var client
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@EnvironmentObject private var appAccount: AppAccountsManager
|
@Environment(AppAccountsManager.self) private var appAccount
|
||||||
@StateObject private var routerPath = RouterPath()
|
@State private var routerPath = RouterPath()
|
||||||
@Binding var popToRootTab: Tab
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routerPath.path) {
|
NavigationStack(path: $routerPath.path) {
|
||||||
|
@ -23,27 +22,18 @@ struct MessagesTab: View {
|
||||||
.withAppRouter()
|
.withAppRouter()
|
||||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
ToolbarTab(routerPath: $routerPath)
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
AppAccountsSelectorView(routerPath: routerPath)
|
|
||||||
}
|
}
|
||||||
}
|
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||||
}
|
|
||||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
|
||||||
.id(client.id)
|
.id(client.id)
|
||||||
}
|
}
|
||||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
.onChange(of: client.id) {
|
||||||
if popToRootTab == .messages {
|
|
||||||
routerPath.path = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: client.id) { _ in
|
|
||||||
routerPath.path = []
|
routerPath.path = []
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routerPath.client = client
|
routerPath.client = client
|
||||||
}
|
}
|
||||||
.withSafariRouter()
|
.withSafariRouter()
|
||||||
.environmentObject(routerPath)
|
.environment(routerPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
IceCubesApp/App/Tabs/NavigationSheet.swift
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import AppAccount
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct NavigationSheet<Content: View>: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
var content: () -> Content
|
||||||
|
|
||||||
|
init(@ViewBuilder content: @escaping () -> Content) {
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
content()
|
||||||
|
.toolbar {
|
||||||
|
CloseToolbarItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
IceCubesApp/App/Tabs/NavigationTab.swift
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import AppAccount
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct NavigationTab<Content: View>: View {
|
||||||
|
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
||||||
|
|
||||||
|
@Environment(AppAccountsManager.self) private var appAccount
|
||||||
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(Client.self) private var client
|
||||||
|
|
||||||
|
var content: () -> Content
|
||||||
|
|
||||||
|
@State private var routerPath = RouterPath()
|
||||||
|
|
||||||
|
init(@ViewBuilder content: @escaping () -> Content) {
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack(path: $routerPath.path) {
|
||||||
|
content()
|
||||||
|
.withEnvironments()
|
||||||
|
.withAppRouter()
|
||||||
|
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||||
|
.withSafariRouter()
|
||||||
|
.toolbar {
|
||||||
|
ToolbarTab(routerPath: $routerPath)
|
||||||
|
}
|
||||||
|
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||||
|
.onChange(of: client.id) {
|
||||||
|
routerPath.path = []
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
routerPath.client = client
|
||||||
|
}
|
||||||
|
.withSafariRouter()
|
||||||
|
}
|
||||||
|
.environment(routerPath)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,19 +7,21 @@ import Notifications
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Timeline
|
import Timeline
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct NotificationsTab: View {
|
struct NotificationsTab: View {
|
||||||
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var client: Client
|
@Environment(Client.self) private var client
|
||||||
@EnvironmentObject private var watcher: StreamWatcher
|
@Environment(StreamWatcher.self) private var watcher
|
||||||
@EnvironmentObject private var appAccount: AppAccountsManager
|
@Environment(AppAccountsManager.self) private var appAccount
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@EnvironmentObject private var userPreferences: UserPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
@EnvironmentObject private var pushNotificationsService: PushNotificationsService
|
@Environment(PushNotificationsService.self) private var pushNotificationsService
|
||||||
@StateObject private var routerPath = RouterPath()
|
@State private var routerPath = RouterPath()
|
||||||
@Binding var popToRootTab: Tab
|
|
||||||
|
@Binding var selectedTab: AppTab
|
||||||
|
|
||||||
let lockedType: Models.Notification.NotificationType?
|
let lockedType: Models.Notification.NotificationType?
|
||||||
|
|
||||||
|
@ -29,70 +31,62 @@ struct NotificationsTab: View {
|
||||||
.withAppRouter()
|
.withAppRouter()
|
||||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
if !isSecondaryColumn {
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
statusEditorToolbarItem(routerPath: routerPath,
|
Button {
|
||||||
visibility: userPreferences.postVisibility)
|
routerPath.presentedSheet = .accountPushNotficationsSettings
|
||||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
} label: {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
Image(systemName: "bell")
|
||||||
AppAccountsSelectorView(routerPath: routerPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ToolbarTab(routerPath: $routerPath)
|
||||||
}
|
}
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||||
if (!isSecondaryColumn && !userPreferences.showiPadSecondaryColumn) || isSecondaryColumn {
|
|
||||||
SecondaryColumnToolbarItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
|
||||||
.id(client.id)
|
.id(client.id)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routerPath.client = client
|
routerPath.client = client
|
||||||
if isSecondaryColumn {
|
|
||||||
clearNotifications()
|
clearNotifications()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.withSafariRouter()
|
.withSafariRouter()
|
||||||
.environmentObject(routerPath)
|
.environment(routerPath)
|
||||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
.onChange(of: selectedTab) { _, _ in
|
||||||
if popToRootTab == .notifications {
|
clearNotifications()
|
||||||
routerPath.path = []
|
|
||||||
}
|
}
|
||||||
}
|
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
|
||||||
.onChange(of: pushNotificationsService.handledNotification) { notification in
|
if let newValue, let type = newValue.notification.supportedType {
|
||||||
if let notification, let type = notification.notification.supportedType {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
switch type {
|
switch type {
|
||||||
case .follow, .follow_request:
|
case .follow, .follow_request:
|
||||||
routerPath.navigate(to: .accountDetailWithAccount(account: notification.notification.account))
|
routerPath.navigate(to: .accountDetailWithAccount(account: newValue.notification.account))
|
||||||
default:
|
default:
|
||||||
if let status = notification.notification.status {
|
if let status = newValue.notification.status {
|
||||||
routerPath.navigate(to: .statusDetailWithStatus(status: status))
|
routerPath.navigate(to: .statusDetailWithStatus(status: status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase, perform: { scenePhase in
|
.onChange(of: scenePhase) { _, newValue in
|
||||||
switch scenePhase {
|
switch newValue {
|
||||||
case .active:
|
case .active:
|
||||||
clearNotifications()
|
clearNotifications()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.onChange(of: client.id) { _ in
|
.onChange(of: client.id) {
|
||||||
routerPath.path = []
|
routerPath.path = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func clearNotifications() {
|
private func clearNotifications() {
|
||||||
if isSecondaryColumn {
|
if selectedTab == .notifications || isSecondaryColumn {
|
||||||
if let token = appAccount.currentAccount.oauthToken {
|
if let token = appAccount.currentAccount.oauthToken, userPreferences.notificationsCount[token] ?? 0 > 0 {
|
||||||
userPreferences.setNotification(count: 0, token: token)
|
userPreferences.notificationsCount[token] = 0
|
||||||
}
|
}
|
||||||
|
if watcher.unreadNotificationsCount > 0 {
|
||||||
watcher.unreadNotificationsCount = 0
|
watcher.unreadNotificationsCount = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,15 @@ import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Shimmer
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct ProfileTab: View {
|
struct ProfileTab: View {
|
||||||
@EnvironmentObject private var appAccount: AppAccountsManager
|
@Environment(AppAccountsManager.self) private var appAccount
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var client: Client
|
@Environment(Client.self) private var client
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@StateObject private var routerPath = RouterPath()
|
@State private var routerPath = RouterPath()
|
||||||
@Binding var popToRootTab: Tab
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routerPath.path) {
|
NavigationStack(path: $routerPath.path) {
|
||||||
|
@ -22,25 +21,21 @@ struct ProfileTab: View {
|
||||||
AccountDetailView(account: account)
|
AccountDetailView(account: account)
|
||||||
.withAppRouter()
|
.withAppRouter()
|
||||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||||
.id(account.id)
|
.id(account.id)
|
||||||
} else {
|
} else {
|
||||||
AccountDetailView(account: .placeholder())
|
AccountDetailView(account: .placeholder())
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
.onChange(of: client.id) {
|
||||||
if popToRootTab == .profile {
|
|
||||||
routerPath.path = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: client.id) { _ in
|
|
||||||
routerPath.path = []
|
routerPath.path = []
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routerPath.client = client
|
routerPath.client = client
|
||||||
}
|
}
|
||||||
.withSafariRouter()
|
.withSafariRouter()
|
||||||
.environmentObject(routerPath)
|
.environment(routerPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
|
import Account
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct AboutView: View {
|
struct AboutView: View {
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(Client.self) private var client
|
||||||
|
|
||||||
|
@State private var dimillianAccount: AccountsListRowViewModel?
|
||||||
|
@State private var iceCubesAccount: AccountsListRowViewModel?
|
||||||
|
|
||||||
let versionNumber: String
|
let versionNumber: String
|
||||||
|
|
||||||
|
@ -17,45 +25,59 @@ struct AboutView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
List {
|
||||||
VStack(alignment: .leading) {
|
Section {
|
||||||
Divider()
|
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Image("icon0")
|
Image(uiImage: .init(named: "AppIconAlternate0-image") ?? .init())
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
Image("icon14")
|
Image(uiImage: .init(named: "AppIconAlternate46-image") ?? .init())
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
Image("icon17")
|
Image(uiImage: .init(named: "AppIconAlternate17-image") ?? .init())
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
Image("icon23")
|
Image(uiImage: .init(named: "AppIconAlternate23-image") ?? .init())
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.top, 10)
|
#endif
|
||||||
HStack {
|
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
|
||||||
Spacer()
|
Label("settings.support.privacy-policy", systemImage: "lock")
|
||||||
Text("\(versionNumber)©2023 Thomas Ricouard")
|
|
||||||
.font(.scaledFootnote)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
Divider()
|
|
||||||
Text("settings.about.built-with")
|
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!) {
|
||||||
.padding(.horizontal, 25)
|
Label("settings.support.terms-of-use", systemImage: "checkmark.shield")
|
||||||
.padding(.bottom, 10)
|
}
|
||||||
.font(.scaledSubheadline)
|
} footer: {
|
||||||
.foregroundColor(.gray)
|
Text("\(versionNumber)© 2024 Thomas Ricouard")
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
followAccountsSection
|
||||||
|
|
||||||
|
Section("Telemetry") {
|
||||||
|
Link(destination: .init(string: "https://telemetrydeck.com")!) {
|
||||||
|
Label("Telemetry by TelemetryDeck", systemImage: "link")
|
||||||
|
}
|
||||||
|
Link(destination: .init(string: "https://telemetrydeck.com/privacy/")!) {
|
||||||
|
Label("Privacy Policy", systemImage: "checkmark.shield")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Section {
|
||||||
Text("""
|
Text("""
|
||||||
• [EmojiText](https://github.com/divadretlaw/EmojiText)
|
• [EmojiText](https://github.com/divadretlaw/EmojiText)
|
||||||
|
|
||||||
|
@ -78,28 +100,82 @@ struct AboutView: View {
|
||||||
• [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
|
• [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
|
||||||
|
|
||||||
• [RevenueCat](https://github.com/RevenueCat/purchases-ios)
|
• [RevenueCat](https://github.com/RevenueCat/purchases-ios)
|
||||||
|
|
||||||
|
• [SFSafeSymbols](https://github.com/SFSafeSymbols/SFSafeSymbols)
|
||||||
""")
|
""")
|
||||||
.padding(.horizontal, 25)
|
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
.foregroundColor(.gray)
|
.foregroundStyle(.secondary)
|
||||||
|
} header: {
|
||||||
|
Text("settings.about.built-with")
|
||||||
|
.textCase(nil)
|
||||||
}
|
}
|
||||||
Divider()
|
#if !os(visionOS)
|
||||||
Spacer()
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
.task {
|
||||||
|
await fetchAccounts()
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
.navigationTitle(Text("settings.about.title"))
|
.navigationTitle(Text("settings.about.title"))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
.environment(\.openURL, OpenURLAction { url in
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
routerPath.handle(url: url)
|
routerPath.handle(url: url)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var followAccountsSection: some View {
|
||||||
|
if let iceCubesAccount, let dimillianAccount {
|
||||||
|
Section {
|
||||||
|
AccountsListRow(viewModel: iceCubesAccount)
|
||||||
|
AccountsListRow(viewModel: dimillianAccount)
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
Section {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchAccounts() async {
|
||||||
|
await withThrowingTaskGroup(of: Void.self) { group in
|
||||||
|
group.addTask {
|
||||||
|
let viewModel = try await fetchAccountViewModel(client, account: "dimillian@mastodon.social")
|
||||||
|
await MainActor.run {
|
||||||
|
dimillianAccount = viewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.addTask {
|
||||||
|
let viewModel = try await fetchAccountViewModel(client, account: "icecubesapp@mastodon.online")
|
||||||
|
await MainActor.run {
|
||||||
|
iceCubesAccount = viewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchAccountViewModel(_ client: Client, account: String) async throws -> AccountsListRowViewModel {
|
||||||
|
let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account))
|
||||||
|
let rel: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [dimillianAccount.id]))
|
||||||
|
return .init(account: dimillianAccount, relationShip: rel.first)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AboutView_Previews: PreviewProvider {
|
struct AboutView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
AboutView()
|
AboutView()
|
||||||
.environmentObject(Theme.shared)
|
.environment(Theme.shared)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,16 +11,16 @@ struct AccountSettingsView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
|
|
||||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
@Environment(PushNotificationsService.self) private var pushNotifications
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
@Environment(CurrentInstance.self) private var currentInstance
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||||
@EnvironmentObject private var client: Client
|
@Environment(Client.self) private var client
|
||||||
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
|
||||||
@State private var isEditingAccount: Bool = false
|
|
||||||
@State private var isEditingFilters: Bool = false
|
|
||||||
@State private var cachedPostsCount: Int = 0
|
@State private var cachedPostsCount: Int = 0
|
||||||
|
@State private var timelineCache = TimelineCache()
|
||||||
|
|
||||||
let account: Account
|
let account: Account
|
||||||
let appAccount: AppAccount
|
let appAccount: AppAccount
|
||||||
|
@ -29,7 +29,7 @@ struct AccountSettingsView: View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
isEditingAccount = true
|
routerPath.presentedSheet = .accountEditInfo
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.edit-info", systemImage: "pencil")
|
Label("account.action.edit-info", systemImage: "pencil")
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
@ -39,7 +39,7 @@ struct AccountSettingsView: View {
|
||||||
|
|
||||||
if currentInstance.isFiltersSupported {
|
if currentInstance.isFiltersSupported {
|
||||||
Button {
|
Button {
|
||||||
isEditingFilters = true
|
routerPath.presentedSheet = .accountFiltersList
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
@ -59,8 +59,8 @@ struct AccountSettingsView: View {
|
||||||
Label("settings.account.cached-posts-\(String(cachedPostsCount))", systemImage: "internaldrive")
|
Label("settings.account.cached-posts-\(String(cachedPostsCount))", systemImage: "internaldrive")
|
||||||
Button("settings.account.action.delete-cache", role: .destructive) {
|
Button("settings.account.action.delete-cache", role: .destructive) {
|
||||||
Task {
|
Task {
|
||||||
await TimelineCache.shared.clearCache(for: appAccountsManager.currentClient.id)
|
await timelineCache.clearCache(for: appAccountsManager.currentClient.id)
|
||||||
cachedPostsCount = await TimelineCache.shared.cachedPostsCount(for: appAccountsManager.currentClient.id)
|
cachedPostsCount = await timelineCache.cachedPostsCount(for: appAccountsManager.currentClient.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,41 +80,38 @@ struct AccountSettingsView: View {
|
||||||
if let token = appAccount.oauthToken {
|
if let token = appAccount.oauthToken {
|
||||||
Task {
|
Task {
|
||||||
let client = Client(server: appAccount.server, oauthToken: token)
|
let client = Client(server: appAccount.server, oauthToken: token)
|
||||||
await TimelineCache.shared.clearCache(for: client.id)
|
await timelineCache.clearCache(for: client.id)
|
||||||
if let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) {
|
if let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) {
|
||||||
await sub.deleteSubscription()
|
await sub.deleteSubscription()
|
||||||
}
|
}
|
||||||
appAccountsManager.delete(account: appAccount)
|
appAccountsManager.delete(account: appAccount)
|
||||||
|
Telemetry.signal("account.removed")
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text("account.action.logout")
|
Label("account.action.logout", systemImage: "trash")
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isEditingAccount, content: {
|
|
||||||
EditAccountView()
|
|
||||||
})
|
|
||||||
.sheet(isPresented: $isEditingFilters, content: {
|
|
||||||
FiltersListView()
|
|
||||||
})
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
HStack {
|
HStack {
|
||||||
AvatarView(url: account.avatar, size: .embed)
|
AvatarView(account.avatar, config: .embed)
|
||||||
Text(account.safeDisplayName)
|
Text(account.safeDisplayName)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
cachedPostsCount = await TimelineCache.shared.cachedPostsCount(for: appAccountsManager.currentClient.id)
|
cachedPostsCount = await timelineCache.cachedPostsCount(for: appAccountsManager.currentClient.id)
|
||||||
}
|
}
|
||||||
.navigationTitle(account.safeDisplayName)
|
.navigationTitle(account.safeDisplayName)
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import AppAccount
|
import AppAccount
|
||||||
|
import AuthenticationServices
|
||||||
import Combine
|
import Combine
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
@ -6,18 +7,20 @@ import Models
|
||||||
import Network
|
import Network
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import Shimmer
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct AddAccountView: View {
|
struct AddAccountView: View {
|
||||||
|
@Environment(\.webAuthenticationSession) private var webAuthenticationSession
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
@Environment(\.openURL) private var openURL
|
||||||
|
|
||||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
@Environment(CurrentInstance.self) private var currentInstance
|
||||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
@Environment(PushNotificationsService.self) private var pushNotifications
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
@State private var instanceName: String = ""
|
@State private var instanceName: String = ""
|
||||||
@State private var instance: Instance?
|
@State private var instance: Instance?
|
||||||
|
@ -25,36 +28,45 @@ struct AddAccountView: View {
|
||||||
@State private var signInClient: Client?
|
@State private var signInClient: Client?
|
||||||
@State private var instances: [InstanceSocial] = []
|
@State private var instances: [InstanceSocial] = []
|
||||||
@State private var instanceFetchError: LocalizedStringKey?
|
@State private var instanceFetchError: LocalizedStringKey?
|
||||||
@State private var oauthURL: URL?
|
@State private var instanceSocialClient = InstanceSocialClient()
|
||||||
|
@State private var searchingTask = Task<Void, Never> {}
|
||||||
|
@State private var getInstanceDetailTask = Task<Void, Never> {}
|
||||||
|
|
||||||
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
private var sanitizedName: String {
|
private var sanitizedName: String {
|
||||||
get {
|
|
||||||
var name = instanceName
|
var name = instanceName
|
||||||
.replacingOccurrences(of: "http://", with: "")
|
.replacingOccurrences(of: "http://", with: "")
|
||||||
.replacingOccurrences(of: "https://", with: "")
|
.replacingOccurrences(of: "https://", with: "")
|
||||||
|
|
||||||
if name.contains("@") {
|
if name.contains("@") {
|
||||||
let parts = name.components(separatedBy: "@")
|
let parts = name.components(separatedBy: "@")
|
||||||
name = parts[parts.count-1] // [@]username@server.address.com
|
name = parts[parts.count - 1] // [@]username@server.address.com
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@FocusState private var isInstanceURLFieldFocused: Bool
|
@FocusState private var isInstanceURLFieldFocused: Bool
|
||||||
|
|
||||||
|
private func cleanServerStr(_ server: String) -> String {
|
||||||
|
server.replacingOccurrences(of: " ", with: "")
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
TextField("instance.url", text: $instanceName)
|
TextField("instance.url", text: $instanceName)
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
.textContentType(.URL)
|
.textContentType(.URL)
|
||||||
.textInputAutocapitalization(.never)
|
.textInputAutocapitalization(.never)
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
.focused($isInstanceURLFieldFocused)
|
.focused($isInstanceURLFieldFocused)
|
||||||
|
.onChange(of: instanceName) { _, _ in
|
||||||
|
instanceName = cleanServerStr(instanceName)
|
||||||
|
}
|
||||||
if let instanceFetchError {
|
if let instanceFetchError {
|
||||||
Text(instanceFetchError)
|
Text(instanceFetchError)
|
||||||
}
|
}
|
||||||
|
@ -70,40 +82,52 @@ struct AddAccountView: View {
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
.navigationTitle("account.add.navigation-title")
|
.navigationTitle("account.add.navigation-title")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
if !appAccountsManager.availableAccounts.isEmpty {
|
CancelToolbarItem()
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button("action.cancel", action: { dismiss() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
isInstanceURLFieldFocused = true
|
isInstanceURLFieldFocused = true
|
||||||
let client = InstanceSocialClient()
|
let instanceName = instanceName
|
||||||
Task {
|
Task {
|
||||||
let instances = await client.fetchInstances()
|
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||||
withAnimation {
|
withAnimation {
|
||||||
self.instances = instances
|
self.instances = instances
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isSigninIn = false
|
isSigninIn = false
|
||||||
}
|
}
|
||||||
.onChange(of: instanceName) { newValue in
|
.onChange(of: instanceName) {
|
||||||
instanceNamePublisher.send(newValue)
|
searchingTask.cancel()
|
||||||
|
let instanceName = instanceName
|
||||||
|
let instanceSocialClient = instanceSocialClient
|
||||||
|
searchingTask = Task {
|
||||||
|
try? await Task.sleep(for: .seconds(0.1))
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||||
|
withAnimation {
|
||||||
|
self.instances = instances
|
||||||
}
|
}
|
||||||
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
|
}
|
||||||
//let newValue = newValue
|
|
||||||
// .replacingOccurrences(of: "http://", with: "")
|
getInstanceDetailTask.cancel()
|
||||||
// .replacingOccurrences(of: "https://", with: "")
|
getInstanceDetailTask = Task {
|
||||||
let client = Client(server: sanitizedName)
|
try? await Task.sleep(for: .seconds(0.1))
|
||||||
Task {
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// bare bones preflight for domain validity
|
// bare bones preflight for domain validity
|
||||||
if client.server.contains(".") && client.server.last != "." {
|
let instanceDetailClient = Client(server: sanitizedName)
|
||||||
let instance: Instance = try await client.get(endpoint: Instances.instance)
|
if
|
||||||
|
instanceDetailClient.server.contains("."),
|
||||||
|
instanceDetailClient.server.last != "."
|
||||||
|
{
|
||||||
|
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
|
||||||
withAnimation {
|
withAnimation {
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
|
self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
|
||||||
|
@ -113,35 +137,23 @@ struct AddAccountView: View {
|
||||||
instance = nil
|
instance = nil
|
||||||
instanceFetchError = nil
|
instanceFetchError = nil
|
||||||
}
|
}
|
||||||
} catch _ as DecodingError {
|
} catch _ as ServerError {
|
||||||
instance = nil
|
instance = nil
|
||||||
instanceFetchError = "account.add.error.instance-not-supported"
|
instanceFetchError = "account.add.error.instance-not-supported"
|
||||||
} catch {
|
} catch {
|
||||||
instance = nil
|
instance = nil
|
||||||
|
instanceFetchError = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase, perform: { scenePhase in
|
.onChange(of: scenePhase) { _, newValue in
|
||||||
switch scenePhase {
|
switch newValue {
|
||||||
case .active:
|
case .active:
|
||||||
isSigninIn = false
|
isSigninIn = false
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.onOpenURL(perform: { url in
|
|
||||||
Task {
|
|
||||||
await continueSignIn(url: url)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.onChange(of: oauthURL, perform: { newValue in
|
|
||||||
if newValue == nil {
|
|
||||||
isSigninIn = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sheet(item: $oauthURL, content: { url in
|
|
||||||
SafariView(url: url)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +182,9 @@ struct AddAccountView: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.tintColor)
|
.listRowBackground(theme.tintColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var instancesListView: some View {
|
private var instancesListView: some View {
|
||||||
|
@ -178,30 +192,65 @@ struct AddAccountView: View {
|
||||||
if instances.isEmpty {
|
if instances.isEmpty {
|
||||||
placeholderRow
|
placeholderRow
|
||||||
} else {
|
} else {
|
||||||
ForEach(sanitizedName.isEmpty ? instances : instances.filter { $0.name.contains(sanitizedName.lowercased()) }) { instance in
|
ForEach(instances) { instance in
|
||||||
Button {
|
Button {
|
||||||
self.instanceName = instance.name
|
instanceName = instance.name
|
||||||
} label: {
|
} label: {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
LazyImage(url: instance.thumbnail) { state in
|
||||||
|
if let image = state.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
} else {
|
||||||
|
Rectangle().fill(theme.tintColor.opacity(0.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 100)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
Text(instance.name)
|
Text(instance.name)
|
||||||
.font(.scaledHeadline)
|
.font(.scaledHeadline)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
Text(instance.info?.shortDescription ?? "")
|
Spacer()
|
||||||
.font(.scaledBody)
|
(Text("instance.list.users-\(formatAsNumber(instance.users))")
|
||||||
.foregroundColor(.gray)
|
|
||||||
(Text("instance.list.users-\(instance.users)")
|
|
||||||
+ Text(" ⸱ ")
|
+ Text(" ⸱ ")
|
||||||
+ Text("instance.list.posts-\(instance.statuses)"))
|
+ Text("instance.list.posts-\(formatAsNumber(instance.statuses))"))
|
||||||
|
.foregroundStyle(theme.tintColor)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
.lineLimit(10)
|
||||||
|
}
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
.foregroundColor(.gray)
|
.padding(10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
#if !os(visionOS)
|
||||||
|
.background(theme.primaryBackgroundColor)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.listRowInsets(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func formatAsNumber(_ string: String) -> String {
|
||||||
|
(Int(string) ?? 0)
|
||||||
|
.formatted(
|
||||||
|
.number
|
||||||
|
.notation(.compactName)
|
||||||
|
.locale(.current)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private var placeholderRow: some View {
|
private var placeholderRow: some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("placeholder.loading.short")
|
Text("placeholder.loading.short")
|
||||||
|
@ -209,27 +258,28 @@ struct AddAccountView: View {
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
Text("placeholder.loading.long")
|
Text("placeholder.loading.long")
|
||||||
.font(.scaledBody)
|
.font(.scaledBody)
|
||||||
.foregroundColor(.gray)
|
.foregroundStyle(.secondary)
|
||||||
Text("placeholder.loading.short")
|
Text("placeholder.loading.short")
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
.shimmering()
|
.allowsHitTesting(false)
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func signIn() async {
|
private func signIn() async {
|
||||||
do {
|
|
||||||
signInClient = .init(server: sanitizedName)
|
signInClient = .init(server: sanitizedName)
|
||||||
if let oauthURL = try await signInClient?.oauthURL() {
|
if let oauthURL = try? await signInClient?.oauthURL(),
|
||||||
self.oauthURL = oauthURL
|
let url = try? await webAuthenticationSession.authenticate(using: oauthURL,
|
||||||
|
callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: ""))
|
||||||
|
{
|
||||||
|
await continueSignIn(url: url)
|
||||||
} else {
|
} else {
|
||||||
isSigninIn = false
|
isSigninIn = false
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
isSigninIn = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func continueSignIn(url: URL) async {
|
private func continueSignIn(url: URL) async {
|
||||||
|
@ -238,10 +288,10 @@ struct AddAccountView: View {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
oauthURL = nil
|
|
||||||
let oauthToken = try await client.continueOauthFlow(url: url)
|
let oauthToken = try await client.continueOauthFlow(url: url)
|
||||||
let client = Client(server: client.server, oauthToken: oauthToken)
|
let client = Client(server: client.server, oauthToken: oauthToken)
|
||||||
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
|
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
|
||||||
|
Telemetry.signal("account.added")
|
||||||
appAccountsManager.add(account: AppAccount(server: client.server,
|
appAccountsManager.add(account: AppAccount(server: client.server,
|
||||||
accountName: "\(account.acct)@\(client.server)",
|
accountName: "\(account.acct)@\(client.server)",
|
||||||
oauthToken: oauthToken))
|
oauthToken: oauthToken))
|
||||||
|
@ -252,18 +302,7 @@ struct AddAccountView: View {
|
||||||
isSigninIn = false
|
isSigninIn = false
|
||||||
dismiss()
|
dismiss()
|
||||||
} catch {
|
} catch {
|
||||||
oauthURL = nil
|
|
||||||
isSigninIn = false
|
isSigninIn = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SafariView: UIViewControllerRepresentable {
|
|
||||||
let url: URL
|
|
||||||
|
|
||||||
func makeUIViewController(context _: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
|
|
||||||
SFSafariViewController(url: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIViewController(_: SFSafariViewController, context _: UIViewControllerRepresentableContext<SafariView>) {}
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,41 +5,61 @@ import Models
|
||||||
import Network
|
import Network
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Timeline
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct ContentSettingsView: View {
|
struct ContentSettingsView: View {
|
||||||
@EnvironmentObject private var userPreferences: UserPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
|
@State private var contentFilter = TimelineContentFilter.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@Bindable var userPreferences = userPreferences
|
||||||
Form {
|
Form {
|
||||||
Section("settings.content.boosts") {
|
|
||||||
Toggle(isOn: $userPreferences.suppressDupeReblogs) {
|
|
||||||
Text("settings.content.hide-repeated-boosts")
|
|
||||||
}
|
|
||||||
}.listRowBackground(theme.primaryBackgroundColor)
|
|
||||||
|
|
||||||
Section("settings.content.media") {
|
Section("settings.content.media") {
|
||||||
Toggle(isOn: $userPreferences.autoPlayVideo) {
|
Toggle(isOn: $userPreferences.autoPlayVideo) {
|
||||||
Text("settings.other.autoplay-video")
|
Text("settings.other.autoplay-video")
|
||||||
}
|
}
|
||||||
|
Toggle(isOn: $userPreferences.muteVideo) {
|
||||||
|
Text("settings.other.mute-video")
|
||||||
|
}
|
||||||
Toggle(isOn: $userPreferences.showAltTextForMedia) {
|
Toggle(isOn: $userPreferences.showAltTextForMedia) {
|
||||||
Text("settings.content.media.show.alt")
|
Text("settings.content.media.show.alt")
|
||||||
}
|
}
|
||||||
}.listRowBackground(theme.primaryBackgroundColor)
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Section("settings.content.sharing") {
|
||||||
|
Picker("settings.content.sharing.share-button-behavior", selection: $userPreferences.shareButtonBehavior) {
|
||||||
|
ForEach(PreferredShareButtonBehavior.allCases, id: \.rawValue) { option in
|
||||||
|
Text(option.title)
|
||||||
|
.tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
Section("settings.content.instance-settings") {
|
Section("settings.content.instance-settings") {
|
||||||
Toggle(isOn: $userPreferences.useInstanceContentSettings) {
|
Toggle(isOn: $userPreferences.useInstanceContentSettings) {
|
||||||
Text("settings.content.use-instance-settings")
|
Text("settings.content.use-instance-settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
.onChange(of: userPreferences.useInstanceContentSettings) { newVal in
|
#endif
|
||||||
|
.onChange(of: userPreferences.useInstanceContentSettings) { _, newVal in
|
||||||
if newVal {
|
if newVal {
|
||||||
userPreferences.appAutoExpandSpoilers = userPreferences.autoExpandSpoilers
|
userPreferences.appAutoExpandSpoilers = userPreferences.autoExpandSpoilers
|
||||||
userPreferences.appAutoExpandMedia = userPreferences.autoExpandMedia
|
userPreferences.appAutoExpandMedia = userPreferences.autoExpandMedia
|
||||||
userPreferences.appDefaultPostsSensitive = userPreferences.postIsSensitive
|
userPreferences.appDefaultPostsSensitive = userPreferences.postIsSensitive
|
||||||
userPreferences.appDefaultPostVisibility = userPreferences.postVisibility
|
userPreferences.appDefaultPostVisibility = userPreferences.postVisibility
|
||||||
|
userPreferences.appRequireAltText = userPreferences.appRequireAltText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +84,9 @@ struct ContentSettingsView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("settings.content.collapse-long-posts-hint")
|
Text("settings.content.collapse-long-posts-hint")
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
Section("settings.content.posting") {
|
Section("settings.content.posting") {
|
||||||
Picker("settings.content.default-visibility", selection: $userPreferences.appDefaultPostVisibility) {
|
Picker("settings.content.default-visibility", selection: $userPreferences.appDefaultPostVisibility) {
|
||||||
|
@ -74,16 +96,54 @@ struct ContentSettingsView: View {
|
||||||
}
|
}
|
||||||
.disabled(userPreferences.useInstanceContentSettings)
|
.disabled(userPreferences.useInstanceContentSettings)
|
||||||
|
|
||||||
|
Picker("settings.content.default-reply-visibility", selection: $userPreferences.appDefaultReplyVisibility) {
|
||||||
|
ForEach(Visibility.allCases, id: \.rawValue) { vis in
|
||||||
|
if UserPreferences.getIntOfVisibility(vis) <=
|
||||||
|
UserPreferences.getIntOfVisibility(userPreferences.postVisibility)
|
||||||
|
{
|
||||||
|
Text(vis.title).tag(vis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: userPreferences.postVisibility) {
|
||||||
|
userPreferences.conformReplyVisibilityConstraints()
|
||||||
|
}
|
||||||
|
|
||||||
Toggle(isOn: $userPreferences.appDefaultPostsSensitive) {
|
Toggle(isOn: $userPreferences.appDefaultPostsSensitive) {
|
||||||
Text("settings.content.default-sensitive")
|
Text("settings.content.default-sensitive")
|
||||||
}
|
}
|
||||||
.disabled(userPreferences.useInstanceContentSettings)
|
.disabled(userPreferences.useInstanceContentSettings)
|
||||||
}
|
|
||||||
|
|
||||||
|
Toggle(isOn: $userPreferences.appRequireAltText) {
|
||||||
|
Text("settings.content.require-alt-text")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Section("timeline.content-filter.title") {
|
||||||
|
Toggle(isOn: $contentFilter.showBoosts) {
|
||||||
|
Label("timeline.filter.show-boosts", image: "Rocket")
|
||||||
|
}
|
||||||
|
Toggle(isOn: $contentFilter.showReplies) {
|
||||||
|
Label("timeline.filter.show-replies", systemImage: "bubble.left.and.bubble.right")
|
||||||
|
}
|
||||||
|
Toggle(isOn: $contentFilter.showThreads) {
|
||||||
|
Label("timeline.filter.show-threads", systemImage: "bubble.left.and.text.bubble.right")
|
||||||
|
}
|
||||||
|
Toggle(isOn: $contentFilter.showQuotePosts) {
|
||||||
|
Label("timeline.filter.show-quote", systemImage: "quote.bubble")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.content.navigation-title")
|
.navigationTitle("settings.content.navigation-title")
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,89 +1,125 @@
|
||||||
|
import Combine
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Status
|
import Observation
|
||||||
|
import StatusKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@Observable class DisplaySettingsLocalValues {
|
||||||
|
var tintColor = Theme.shared.tintColor
|
||||||
|
var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
||||||
|
var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor
|
||||||
|
var labelColor = Theme.shared.labelColor
|
||||||
|
var lineSpacing = Theme.shared.lineSpacing
|
||||||
|
var fontSizeScale = Theme.shared.fontSizeScale
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct DisplaySettingsView: View {
|
struct DisplaySettingsView: View {
|
||||||
typealias FontState = Theme.FontState
|
typealias FontState = Theme.FontState
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var userPreferences: UserPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
|
||||||
|
@State private var localValues = DisplaySettingsLocalValues()
|
||||||
|
|
||||||
@State private var isFontSelectorPresented = false
|
@State private var isFontSelectorPresented = false
|
||||||
|
|
||||||
@State private var didChangeColors = false
|
private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"),
|
||||||
@State private var localTintColor = Theme.shared.tintColor
|
|
||||||
@State private var localPrimaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
|
||||||
@State private var localSecondaryBackgroundColor = Theme.shared.secondaryBackgroundColor
|
|
||||||
@State private var localLabelColor = Theme.shared.labelColor
|
|
||||||
|
|
||||||
private var previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"),
|
|
||||||
client: Client(server: ""),
|
client: Client(server: ""),
|
||||||
routerPath: RouterPath()) // translate from latin button
|
routerPath: RouterPath()) // translate from latin button
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ZStack(alignment: .top) {
|
||||||
Form {
|
Form {
|
||||||
exampleSection
|
#if !os(visionOS)
|
||||||
|
StatusRowExternalView(viewModel: previewStatusViewModel)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.opacity(0)
|
||||||
|
.hidden()
|
||||||
themeSection
|
themeSection
|
||||||
|
#endif
|
||||||
fontSection
|
fontSection
|
||||||
layoutSection
|
layoutSection
|
||||||
platformsSection
|
platformsSection
|
||||||
resetSection
|
resetSection
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.display.navigation-title")
|
.navigationTitle("settings.display.navigation-title")
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
.task(id: localValues.tintColor) {
|
||||||
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
|
theme.tintColor = localValues.tintColor
|
||||||
|
}
|
||||||
|
.task(id: localValues.primaryBackgroundColor) {
|
||||||
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
|
theme.primaryBackgroundColor = localValues.primaryBackgroundColor
|
||||||
|
}
|
||||||
|
.task(id: localValues.secondaryBackgroundColor) {
|
||||||
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
|
theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor
|
||||||
|
}
|
||||||
|
.task(id: localValues.labelColor) {
|
||||||
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
|
theme.labelColor = localValues.labelColor
|
||||||
|
}
|
||||||
|
.task(id: localValues.lineSpacing) {
|
||||||
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
|
theme.lineSpacing = localValues.lineSpacing
|
||||||
|
}
|
||||||
|
.task(id: localValues.fontSizeScale) {
|
||||||
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
|
theme.fontSizeScale = localValues.fontSizeScale
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
examplePost
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var exampleSection: some View {
|
private var examplePost: some View {
|
||||||
Section("settings.display.example-toot") {
|
VStack(spacing: 0) {
|
||||||
StatusRowView(viewModel: { previewStatusViewModel })
|
StatusRowExternalView(viewModel: previewStatusViewModel)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
|
.padding(.layoutPadding)
|
||||||
|
.background(theme.primaryBackgroundColor)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.padding(.horizontal, .layoutPadding)
|
||||||
|
.padding(.top, .layoutPadding)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
Rectangle()
|
||||||
|
.fill(theme.secondaryBackgroundColor)
|
||||||
|
.frame(height: 30)
|
||||||
|
.mask(LinearGradient(gradient: Gradient(colors: [theme.secondaryBackgroundColor, .clear]),
|
||||||
|
startPoint: .top, endPoint: .bottom))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var themeSection: some View {
|
private var themeSection: some View {
|
||||||
|
@Bindable var theme = theme
|
||||||
Section {
|
Section {
|
||||||
Toggle("settings.display.theme.systemColor", isOn: $theme.followSystemColorScheme)
|
Toggle("settings.display.theme.systemColor", isOn: $theme.followSystemColorScheme)
|
||||||
themeSelectorButton
|
themeSelectorButton
|
||||||
Group {
|
Group {
|
||||||
ColorPicker("settings.display.theme.tint", selection: $localTintColor)
|
ColorPicker("settings.display.theme.tint", selection: $localValues.tintColor)
|
||||||
ColorPicker("settings.display.theme.background", selection: $localPrimaryBackgroundColor)
|
ColorPicker("settings.display.theme.background", selection: $localValues.primaryBackgroundColor)
|
||||||
ColorPicker("settings.display.theme.secondary-background", selection: $localSecondaryBackgroundColor)
|
ColorPicker("settings.display.theme.secondary-background", selection: $localValues.secondaryBackgroundColor)
|
||||||
ColorPicker("settings.display.theme.text-color", selection: $localLabelColor)
|
ColorPicker("settings.display.theme.text-color", selection: $localValues.labelColor)
|
||||||
}
|
}
|
||||||
.disabled(theme.followSystemColorScheme)
|
.disabled(theme.followSystemColorScheme)
|
||||||
.opacity(theme.followSystemColorScheme ? 0.5 : 1.0)
|
.opacity(theme.followSystemColorScheme ? 0.5 : 1.0)
|
||||||
.onChange(of: localTintColor) { _ in
|
.onChange(of: theme.selectedSet) {
|
||||||
didChangeColors = true
|
localValues.tintColor = theme.tintColor
|
||||||
}
|
localValues.primaryBackgroundColor = theme.primaryBackgroundColor
|
||||||
.onChange(of: localSecondaryBackgroundColor) { _ in
|
localValues.secondaryBackgroundColor = theme.secondaryBackgroundColor
|
||||||
didChangeColors = true
|
localValues.labelColor = theme.labelColor
|
||||||
}
|
|
||||||
.onChange(of: localPrimaryBackgroundColor) { _ in
|
|
||||||
didChangeColors = true
|
|
||||||
}
|
|
||||||
.onChange(of: localLabelColor) { _ in
|
|
||||||
didChangeColors = true
|
|
||||||
}
|
|
||||||
.onChange(of: theme.selectedSet) { _ in
|
|
||||||
localTintColor = theme.tintColor
|
|
||||||
localPrimaryBackgroundColor = theme.primaryBackgroundColor
|
|
||||||
localSecondaryBackgroundColor = theme.secondaryBackgroundColor
|
|
||||||
localLabelColor = theme.labelColor
|
|
||||||
}
|
|
||||||
if didChangeColors {
|
|
||||||
Button {
|
|
||||||
didChangeColors = false
|
|
||||||
theme.tintColor = localTintColor
|
|
||||||
theme.primaryBackgroundColor = localPrimaryBackgroundColor
|
|
||||||
theme.secondaryBackgroundColor = localSecondaryBackgroundColor
|
|
||||||
theme.labelColor = localLabelColor
|
|
||||||
} label: {
|
|
||||||
Text("settings.display.colors.apply")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("settings.display.section.theme")
|
Text("settings.display.section.theme")
|
||||||
|
@ -92,7 +128,9 @@ struct DisplaySettingsView: View {
|
||||||
Text("settings.display.section.theme.footer")
|
Text("settings.display.section.theme.footer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fontSection: some View {
|
private var fontSection: some View {
|
||||||
|
@ -127,18 +165,32 @@ struct DisplaySettingsView: View {
|
||||||
.navigationDestination(isPresented: $isFontSelectorPresented, destination: { FontPicker() })
|
.navigationDestination(isPresented: $isFontSelectorPresented, destination: { FontPicker() })
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Slider(value: $theme.fontSizeScale, in: 0.5 ... 1.5, step: 0.1)
|
Slider(value: $localValues.fontSizeScale, in: 0.5 ... 1.5, step: 0.1)
|
||||||
Text("settings.display.font.scaling-\(String(format: "%.1f", theme.fontSizeScale))")
|
Text("settings.display.font.scaling-\(String(format: "%.1f", localValues.fontSizeScale))")
|
||||||
|
.font(.scaledBody)
|
||||||
|
}
|
||||||
|
.alignmentGuide(.listRowSeparatorLeading) { d in
|
||||||
|
d[.leading]
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Slider(value: $localValues.lineSpacing, in: 0.4 ... 10.0, step: 0.2)
|
||||||
|
Text("settings.display.font.line-spacing-\(String(format: "%.1f", localValues.lineSpacing))")
|
||||||
.font(.scaledBody)
|
.font(.scaledBody)
|
||||||
}
|
}
|
||||||
.alignmentGuide(.listRowSeparatorLeading) { d in
|
.alignmentGuide(.listRowSeparatorLeading) { d in
|
||||||
d[.leading]
|
d[.leading]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var layoutSection: some View {
|
private var layoutSection: some View {
|
||||||
|
@Bindable var theme = theme
|
||||||
|
@Bindable var userPreferences = userPreferences
|
||||||
Section("settings.display.section.display") {
|
Section("settings.display.section.display") {
|
||||||
Picker("settings.display.avatar.position", selection: $theme.avatarPosition) {
|
Picker("settings.display.avatar.position", selection: $theme.avatarPosition) {
|
||||||
ForEach(Theme.AvatarPosition.allCases, id: \.rawValue) { position in
|
ForEach(Theme.AvatarPosition.allCases, id: \.rawValue) { position in
|
||||||
|
@ -156,53 +208,68 @@ struct DisplaySettingsView: View {
|
||||||
Text(buttonStyle.description).tag(buttonStyle)
|
Text(buttonStyle.description).tag(buttonStyle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Picker("settings.display.status.action-secondary", selection: $theme.statusActionSecondary) {
|
||||||
|
ForEach(Theme.StatusActionSecondary.allCases, id: \.rawValue) { action in
|
||||||
|
Text(action.description).tag(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
Picker("settings.display.status.media-style", selection: $theme.statusDisplayStyle) {
|
Picker("settings.display.status.media-style", selection: $theme.statusDisplayStyle) {
|
||||||
ForEach(Theme.StatusDisplayStyle.allCases, id: \.rawValue) { buttonStyle in
|
ForEach(Theme.StatusDisplayStyle.allCases, id: \.rawValue) { buttonStyle in
|
||||||
Text(buttonStyle.description).tag(buttonStyle)
|
Text(buttonStyle.description).tag(buttonStyle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Toggle("settings.display.translate-button", isOn: $userPreferences.showTranslateButton)
|
Toggle("settings.display.translate-button", isOn: $userPreferences.showTranslateButton)
|
||||||
|
Toggle("settings.display.pending-at-bottom", isOn: $userPreferences.pendingShownAtBottom)
|
||||||
|
Toggle("settings.display.pending-left", isOn: $userPreferences.pendingShownLeft)
|
||||||
|
Toggle("settings.display.show-reply-indentation", isOn: $userPreferences.showReplyIndentation)
|
||||||
|
if userPreferences.showReplyIndentation {
|
||||||
|
VStack {
|
||||||
|
Slider(value: .init(get: {
|
||||||
|
Double(userPreferences.maxReplyIndentation)
|
||||||
|
}, set: { newVal in
|
||||||
|
userPreferences.maxReplyIndentation = UInt(newVal)
|
||||||
|
}), in: 1 ... 20, step: 1)
|
||||||
|
Text("settings.display.max-reply-indentation-\(String(userPreferences.maxReplyIndentation))")
|
||||||
|
.font(.scaledBody)
|
||||||
}
|
}
|
||||||
|
.alignmentGuide(.listRowSeparatorLeading) { d in
|
||||||
|
d[.leading]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Toggle("settings.display.show-account-popover", isOn: $userPreferences.showAccountPopover)
|
||||||
|
Toggle("Show Content Gradient", isOn: $theme.showContentGradient)
|
||||||
|
Toggle("Compact Layout", isOn: $theme.compactLayoutPadding)
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var platformsSection: some View {
|
private var platformsSection: some View {
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
@Bindable var userPreferences = userPreferences
|
||||||
Section("iPhone") {
|
|
||||||
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
|
|
||||||
}
|
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
Section("iPad") {
|
Section("settings.display.section.platform") {
|
||||||
Toggle("settings.display.show-ipad-column", isOn: $userPreferences.showiPadSecondaryColumn)
|
Toggle("settings.display.show-ipad-column", isOn: $userPreferences.showiPadSecondaryColumn)
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var resetSection: some View {
|
private var resetSection: some View {
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
theme.followSystemColorScheme = true
|
theme.restoreDefault()
|
||||||
theme.selectedSet = colorScheme == .dark ? .iceCubeDark : .iceCubeLight
|
|
||||||
theme.avatarShape = .rounded
|
|
||||||
theme.avatarPosition = .top
|
|
||||||
theme.statusActionsDisplay = .full
|
|
||||||
|
|
||||||
localTintColor = theme.tintColor
|
|
||||||
localPrimaryBackgroundColor = theme.primaryBackgroundColor
|
|
||||||
localSecondaryBackgroundColor = theme.secondaryBackgroundColor
|
|
||||||
localLabelColor = theme.labelColor
|
|
||||||
|
|
||||||
} label: {
|
} label: {
|
||||||
Text("settings.display.restore")
|
Text("settings.display.restore")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var themeSelectorButton: some View {
|
private var themeSelectorButton: some View {
|
||||||
|
|
|
@ -1,24 +1,30 @@
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Status
|
import StatusKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct HapticSettingsView: View {
|
struct HapticSettingsView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var userPreferences: UserPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@Bindable var userPreferences = userPreferences
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
Toggle("settings.haptic.timeline", isOn: $userPreferences.hapticTimelineEnabled)
|
Toggle("settings.haptic.timeline", isOn: $userPreferences.hapticTimelineEnabled)
|
||||||
Toggle("settings.haptic.tab-selection", isOn: $userPreferences.hapticTabSelectionEnabled)
|
Toggle("settings.haptic.tab-selection", isOn: $userPreferences.hapticTabSelectionEnabled)
|
||||||
Toggle("settings.haptic.buttons", isOn: $userPreferences.hapticButtonPressEnabled)
|
Toggle("settings.haptic.buttons", isOn: $userPreferences.hapticButtonPressEnabled)
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.haptic.navigation-title")
|
.navigationTitle("settings.haptic.navigation-title")
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct IconSelectorView: View {
|
struct IconSelectorView: View {
|
||||||
enum Icon: Int, CaseIterable, Identifiable {
|
enum Icon: Int, CaseIterable, Identifiable {
|
||||||
var id: String {
|
var id: String {
|
||||||
|
@ -8,7 +9,7 @@ struct IconSelectorView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
init(string: String) {
|
init(string: String) {
|
||||||
if string == Icon.primary.appIconName {
|
if string == "AppIcon" {
|
||||||
self = .primary
|
self = .primary
|
||||||
} else {
|
} else {
|
||||||
self = .init(rawValue: Int(String(string.replacing("AppIconAlternate", with: "")))!)!
|
self = .init(rawValue: Int(String(string.replacing("AppIconAlternate", with: "")))!)!
|
||||||
|
@ -16,27 +17,24 @@ struct IconSelectorView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
case primary = 0
|
case primary = 0
|
||||||
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8
|
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8, alt9, alt10, alt11, alt12, alt13, alt14, alt15
|
||||||
case alt9, alt10, alt11, alt12, alt13, alt14
|
case alt16, alt17, alt18, alt19, alt20, alt21
|
||||||
case alt15, alt16, alt17, alt18, alt19, alt20, alt21
|
case alt22, alt23, alt24, alt25, alt26
|
||||||
case alt22, alt23, alt24, alt25
|
case alt27, alt28, alt29
|
||||||
case alt26, alt27, alt28
|
case alt30, alt31, alt32, alt33, alt34, alt35, alt36
|
||||||
case alt29, alt30, alt31, alt32
|
case alt37
|
||||||
case alt33
|
case alt38
|
||||||
case alt34, alt35
|
case alt39, alt40, alt41, alt42, alt43
|
||||||
case alt36
|
case alt44, alt45
|
||||||
|
case alt46, alt47, alt48
|
||||||
|
case alt49
|
||||||
|
|
||||||
var appIconName: String {
|
var appIconName: String {
|
||||||
switch self {
|
|
||||||
case .primary:
|
|
||||||
return "AppIcon"
|
|
||||||
default:
|
|
||||||
return "AppIconAlternate\(rawValue)"
|
return "AppIconAlternate\(rawValue)"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var iconName: String {
|
var previewImageName: String {
|
||||||
"icon\(rawValue)"
|
return "AppIconAlternate\(rawValue)-image"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,18 +44,24 @@ struct IconSelectorView: View {
|
||||||
let icons: [Icon]
|
let icons: [Icon]
|
||||||
|
|
||||||
static let items = [
|
static let items = [
|
||||||
IconSelector(title: "settings.app.icon.official".localized, icons: [.primary, .alt1, .alt2, .alt3, .alt4, .alt5, .alt6, .alt7, .alt8,
|
IconSelector(title: "settings.app.icon.official".localized, icons: [
|
||||||
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14,
|
.primary, .alt46, .alt1, .alt2, .alt3, .alt4,
|
||||||
.alt15, .alt16, .alt17, .alt18, .alt19, .alt25]),
|
.alt5, .alt6, .alt7, .alt8,
|
||||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt20, .alt21, .alt22, .alt23, .alt24]),
|
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt15,
|
||||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt26, .alt27, .alt28]),
|
.alt16, .alt17, .alt18, .alt19, .alt20, .alt21]),
|
||||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Chanhwi Joo (GitHub @te6-in)", icons: [.alt29, .alt34, .alt31, .alt35, .alt30, .alt32]),
|
IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt22, .alt23, .alt24, .alt25, .alt26]),
|
||||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) W. Kovács Ágnes (@wildgica)", icons: [.alt33]),
|
IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt27, .alt28, .alt29]),
|
||||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Duncan Horne", icons: [.alt36]),
|
IconSelector(title: "\("settings.app.icon.designed-by".localized) Chanhwi Joo (GitHub @te6-in)", icons: [.alt30, .alt31, .alt32, .alt33, .alt34, .alt35, .alt36]),
|
||||||
|
IconSelector(title: "\("settings.app.icon.designed-by".localized) W. Kovács Ágnes (@wildgica)", icons: [.alt37]),
|
||||||
|
IconSelector(title: "\("settings.app.icon.designed-by".localized) Duncan Horne", icons: [.alt38]),
|
||||||
|
IconSelector(title: "\("settings.app.icon.designed-by".localized) BeAware@social.beaware.live", icons: [.alt39, .alt40, .alt41, .alt42, .alt43]),
|
||||||
|
IconSelector(title: "\("settings.app.icon.designed-by".localized) Simone Margio", icons: [.alt44, .alt45]),
|
||||||
|
IconSelector(title: "\("settings.app.icon.designed-by".localized) Peter Broqvist (@PKB)", icons: [.alt47, .alt48]),
|
||||||
|
IconSelector(title: "\("settings.app.icon.designed-by".localized) Oz Tsori (@oztsori)", icons: [.alt49]),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@State private var currentIcon = UIApplication.shared.alternateIconName ?? Icon.primary.appIconName
|
@State private var currentIcon = UIApplication.shared.alternateIconName ?? Icon.primary.appIconName
|
||||||
|
|
||||||
private let columns = [GridItem(.adaptive(minimum: 125, maximum: 1024))]
|
private let columns = [GridItem(.adaptive(minimum: 125, maximum: 1024))]
|
||||||
|
@ -77,7 +81,9 @@ struct IconSelectorView: View {
|
||||||
.padding(6)
|
.padding(6)
|
||||||
.navigationTitle("settings.app.icon.navigation-title")
|
.navigationTitle("settings.app.icon.navigation-title")
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.background(theme.primaryBackgroundColor)
|
.background(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeIconGridView(icons: [Icon]) -> some View {
|
private func makeIconGridView(icons: [Icon]) -> some View {
|
||||||
|
@ -88,11 +94,14 @@ struct IconSelectorView: View {
|
||||||
if icon.rawValue == Icon.primary.rawValue {
|
if icon.rawValue == Icon.primary.rawValue {
|
||||||
UIApplication.shared.setAlternateIconName(nil)
|
UIApplication.shared.setAlternateIconName(nil)
|
||||||
} else {
|
} else {
|
||||||
UIApplication.shared.setAlternateIconName(icon.appIconName)
|
UIApplication.shared.setAlternateIconName(icon.appIconName) { err in
|
||||||
|
guard let err else { return }
|
||||||
|
assertionFailure("\(err.localizedDescription) - Icon name: \(icon.appIconName)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
Image(uiImage: .init(named: icon.iconName) ?? .init())
|
Image(icon.previewImageName)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(minHeight: 125, maxHeight: 1024)
|
.frame(minHeight: 125, maxHeight: 1024)
|
||||||
|
@ -105,6 +114,7 @@ struct IconSelectorView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +122,6 @@ struct IconSelectorView: View {
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
var localized: String {
|
var localized: String {
|
||||||
return NSLocalizedString(self, comment: "")
|
NSLocalizedString(self, comment: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import NukeUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct InstanceInfoView: View {
|
struct InstanceInfoView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
let instance: Instance
|
let instance: Instance
|
||||||
|
|
||||||
|
@ -13,13 +13,15 @@ struct InstanceInfoView: View {
|
||||||
InstanceInfoSection(instance: instance)
|
InstanceInfoSection(instance: instance)
|
||||||
}
|
}
|
||||||
.navigationTitle("instance.info.navigation-title")
|
.navigationTitle("instance.info.navigation-title")
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct InstanceInfoSection: View {
|
public struct InstanceInfoSection: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
let instance: Instance
|
let instance: Instance
|
||||||
|
|
||||||
|
@ -35,7 +37,9 @@ public struct InstanceInfoSection: View {
|
||||||
LabeledContent("instance.info.posts", value: format(instance.stats.statusCount))
|
LabeledContent("instance.info.posts", value: format(instance.stats.statusCount))
|
||||||
LabeledContent("instance.info.domains", value: format(instance.stats.domainCount))
|
LabeledContent("instance.info.domains", value: format(instance.stats.domainCount))
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
if let rules = instance.rules {
|
if let rules = instance.rules {
|
||||||
Section("instance.info.section.rules") {
|
Section("instance.info.section.rules") {
|
||||||
|
@ -43,7 +47,9 @@ public struct InstanceInfoSection: View {
|
||||||
Text(rule.text.trimmingCharacters(in: .whitespacesAndNewlines))
|
Text(rule.text.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,13 @@ import NukeUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct PushNotificationsView: View {
|
struct PushNotificationsView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
@Environment(PushNotificationsService.self) private var pushNotifications
|
||||||
|
|
||||||
@StateObject public var subscription: PushNotificationSubscriptionSettings
|
@State public var subscription: PushNotificationSubscriptionSettings
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
@ -32,7 +33,9 @@ struct PushNotificationsView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("settings.push.main-toggle.description")
|
Text("settings.push.main-toggle.description")
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
if subscription.isEnabled {
|
if subscription.isEnabled {
|
||||||
Section {
|
Section {
|
||||||
|
@ -85,7 +88,9 @@ struct PushNotificationsView: View {
|
||||||
Label("settings.push.new-posts", systemImage: "bubble.right")
|
Label("settings.push.new-posts", systemImage: "bubble.right")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
@ -100,11 +105,15 @@ struct PushNotificationsView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("settings.push.duplicate.footer")
|
Text("settings.push.duplicate.footer")
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.push.navigation-title")
|
.navigationTitle("settings.push.navigation-title")
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
.task {
|
.task {
|
||||||
await subscription.fetchSubscription()
|
await subscription.fetchSubscription()
|
||||||
}
|
}
|
||||||
|
|
44
IceCubesApp/App/Tabs/Settings/RecenTagsSettingView.swift
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct RecenTagsSettingView: View {
|
||||||
|
@Environment(\.modelContext) private var context
|
||||||
|
|
||||||
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
|
@Query(sort: \RecentTag.lastUse, order: .reverse) var tags: [RecentTag]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
ForEach(tags) { tag in
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("#\(tag.title)")
|
||||||
|
.font(.scaledBody)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text(tag.formattedDate)
|
||||||
|
.font(.scaledFootnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}.onDelete { indexes in
|
||||||
|
if let index = indexes.first {
|
||||||
|
context.delete(tags[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.navigationTitle("settings.general.recent-tags")
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
#if !os(visionOS)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
.toolbar {
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct RemoteTimelinesSettingView: View {
|
||||||
|
@Environment(\.modelContext) private var context
|
||||||
|
|
||||||
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
|
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
ForEach(localTimelines) { timeline in
|
||||||
|
Text(timeline.instance)
|
||||||
|
}.onDelete { indexes in
|
||||||
|
if let index = indexes.first {
|
||||||
|
context.delete(localTimelines[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
Button {
|
||||||
|
routerPath.presentedSheet = .addRemoteLocalTimeline
|
||||||
|
} label: {
|
||||||
|
Label("settings.timeline.add", systemImage: "badge.plus.radiowaves.right")
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.navigationTitle("settings.general.remote-timelines")
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
#if !os(visionOS)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
.toolbar {
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,26 +6,31 @@ import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Nuke
|
import Nuke
|
||||||
|
import SwiftData
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Timeline
|
import Timeline
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct SettingsTabs: View {
|
struct SettingsTabs: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
@Environment(PushNotificationsService.self) private var pushNotifications
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@Environment(UserPreferences.self) private var preferences
|
||||||
@EnvironmentObject private var client: Client
|
@Environment(Client.self) private var client
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
@Environment(CurrentInstance.self) private var currentInstance
|
||||||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
@StateObject private var routerPath = RouterPath()
|
|
||||||
|
|
||||||
|
@State private var routerPath = RouterPath()
|
||||||
@State private var addAccountSheetPresented = false
|
@State private var addAccountSheetPresented = false
|
||||||
@State private var isEditingAccount = false
|
@State private var isEditingAccount = false
|
||||||
@State private var cachedRemoved = false
|
@State private var cachedRemoved = false
|
||||||
|
@State private var timelineCache = TimelineCache()
|
||||||
|
|
||||||
@Binding var popToRootTab: Tab
|
let isModal: Bool
|
||||||
|
|
||||||
|
@State private var startingPoint: SettingsStartingPoint? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routerPath.path) {
|
NavigationStack(path: $routerPath.path) {
|
||||||
|
@ -34,15 +39,19 @@ struct SettingsTabs: View {
|
||||||
accountsSection
|
accountsSection
|
||||||
generalSection
|
generalSection
|
||||||
otherSections
|
otherSections
|
||||||
|
postStreamingSection
|
||||||
|
AISection
|
||||||
cacheSection
|
cacheSection
|
||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
|
#if !os(visionOS)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
.navigationTitle(Text("settings.title"))
|
.navigationTitle(Text("settings.title"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
if isModal {
|
||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
Button {
|
Button {
|
||||||
dismiss()
|
dismiss()
|
||||||
|
@ -51,12 +60,38 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad && !preferences.showiPadSecondaryColumn {
|
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal {
|
||||||
SecondaryColumnToolbarItem()
|
SecondaryColumnToolbarItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.withAppRouter()
|
.withAppRouter()
|
||||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||||
|
.onAppear {
|
||||||
|
startingPoint = RouterPath.settingsStartingPoint
|
||||||
|
RouterPath.settingsStartingPoint = nil
|
||||||
|
}
|
||||||
|
.navigationDestination(item: $startingPoint) { targetView in
|
||||||
|
switch targetView {
|
||||||
|
case .display:
|
||||||
|
DisplaySettingsView()
|
||||||
|
case .haptic:
|
||||||
|
HapticSettingsView()
|
||||||
|
case .remoteTimelines:
|
||||||
|
RemoteTimelinesSettingView()
|
||||||
|
case .tagGroups:
|
||||||
|
TagsGroupSettingView()
|
||||||
|
case .recentTags:
|
||||||
|
RecenTagsSettingView()
|
||||||
|
case .content:
|
||||||
|
ContentSettingsView()
|
||||||
|
case .swipeActions:
|
||||||
|
SwipeActionsSettingsView()
|
||||||
|
case .tabAndSidebarEntries:
|
||||||
|
EmptyView()
|
||||||
|
case .translation:
|
||||||
|
TranslationSettingsView()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routerPath.client = client
|
routerPath.client = client
|
||||||
|
@ -67,12 +102,7 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.withSafariRouter()
|
.withSafariRouter()
|
||||||
.environmentObject(routerPath)
|
.environment(routerPath)
|
||||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
|
||||||
if popToRootTab == .notifications {
|
|
||||||
routerPath.path = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accountsSection: some View {
|
private var accountsSection: some View {
|
||||||
|
@ -90,7 +120,7 @@ struct SettingsTabs: View {
|
||||||
.tint(.red)
|
.tint(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppAccountView(viewModel: .init(appAccount: account))
|
AppAccountView(viewModel: .init(appAccount: account), isParentPresented: .constant(false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDelete { indexSet in
|
.onDelete { indexSet in
|
||||||
|
@ -101,12 +131,14 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
addAccountButton
|
||||||
if !appAccountsManager.availableAccounts.isEmpty {
|
if !appAccountsManager.availableAccounts.isEmpty {
|
||||||
editAccountButton
|
editAccountButton
|
||||||
}
|
}
|
||||||
addAccountButton
|
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func logoutAccount(account: AppAccount) async {
|
private func logoutAccount(account: AppAccount) async {
|
||||||
|
@ -114,9 +146,10 @@ struct SettingsTabs: View {
|
||||||
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token })
|
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token })
|
||||||
{
|
{
|
||||||
let client = Client(server: account.server, oauthToken: token)
|
let client = Client(server: account.server, oauthToken: token)
|
||||||
await TimelineCache.shared.clearCache(for: client.id)
|
await timelineCache.clearCache(for: client.id)
|
||||||
await sub.deleteSubscription()
|
await sub.deleteSubscription()
|
||||||
appAccountsManager.delete(account: account)
|
appAccountsManager.delete(account: account)
|
||||||
|
Telemetry.signal("account.removed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,26 +169,50 @@ struct SettingsTabs: View {
|
||||||
Label("settings.general.haptic", systemImage: "waveform.path")
|
Label("settings.general.haptic", systemImage: "waveform.path")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(destination: remoteLocalTimelinesView) {
|
NavigationLink(destination: RemoteTimelinesSettingView()) {
|
||||||
Label("settings.general.remote-timelines", systemImage: "dot.radiowaves.right")
|
Label("settings.general.remote-timelines", systemImage: "dot.radiowaves.right")
|
||||||
}
|
}
|
||||||
|
NavigationLink(destination: TagsGroupSettingView()) {
|
||||||
|
Label("timeline.filter.tag-groups", systemImage: "number")
|
||||||
|
}
|
||||||
|
NavigationLink(destination: RecenTagsSettingView()) {
|
||||||
|
Label("settings.general.recent-tags", systemImage: "clock")
|
||||||
|
}
|
||||||
NavigationLink(destination: ContentSettingsView()) {
|
NavigationLink(destination: ContentSettingsView()) {
|
||||||
Label("settings.general.content", systemImage: "rectangle.stack")
|
Label("settings.general.content", systemImage: "rectangle.stack")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: SwipeActionsSettingsView()) {
|
NavigationLink(destination: SwipeActionsSettingsView()) {
|
||||||
Label("settings.general.swipeactions", systemImage: "hand.draw")
|
Label("settings.general.swipeactions", systemImage: "hand.draw")
|
||||||
}
|
}
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact {
|
||||||
|
NavigationLink(destination: TabbarEntriesSettingsView()) {
|
||||||
|
Label("settings.general.tabbarEntries", systemImage: "platter.filled.bottom.iphone")
|
||||||
|
}
|
||||||
|
} else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
NavigationLink(destination: SidebarEntriesSettingsView()) {
|
||||||
|
Label("settings.general.sidebarEntries", systemImage: "sidebar.squares.leading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NavigationLink(destination: TranslationSettingsView()) {
|
||||||
|
Label("settings.general.translate", systemImage: "captions.bubble")
|
||||||
|
}
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
Link(destination: URL(string: UIApplication.openSettingsURLString)!) {
|
Link(destination: URL(string: UIApplication.openSettingsURLString)!) {
|
||||||
Label("settings.system", systemImage: "gear")
|
Label("settings.system", systemImage: "gear")
|
||||||
}
|
}
|
||||||
.tint(theme.labelColor)
|
.tint(theme.labelColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var otherSections: some View {
|
private var otherSections: some View {
|
||||||
Section("settings.section.other") {
|
@Bindable var preferences = preferences
|
||||||
if !ProcessInfo.processInfo.isiOSAppOnMac {
|
Section {
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
Picker(selection: $preferences.preferredBrowser) {
|
Picker(selection: $preferences.preferredBrowser) {
|
||||||
ForEach(PreferredBrowser.allCases, id: \.rawValue) { browser in
|
ForEach(PreferredBrowser.allCases, id: \.rawValue) { browser in
|
||||||
switch browser {
|
switch browser {
|
||||||
|
@ -172,39 +229,84 @@ struct SettingsTabs: View {
|
||||||
Label("settings.general.browser.in-app.readerview", systemImage: "doc.plaintext")
|
Label("settings.general.browser.in-app.readerview", systemImage: "doc.plaintext")
|
||||||
}
|
}
|
||||||
.disabled(preferences.preferredBrowser != PreferredBrowser.inAppSafari)
|
.disabled(preferences.preferredBrowser != PreferredBrowser.inAppSafari)
|
||||||
}
|
#endif
|
||||||
Toggle(isOn: $preferences.isOpenAIEnabled) {
|
|
||||||
Label("settings.other.hide-openai", systemImage: "faxmachine")
|
|
||||||
}
|
|
||||||
Toggle(isOn: $preferences.isSocialKeyboardEnabled) {
|
Toggle(isOn: $preferences.isSocialKeyboardEnabled) {
|
||||||
Label("settings.other.social-keyboard", systemImage: "keyboard")
|
Label("settings.other.social-keyboard", systemImage: "keyboard")
|
||||||
}
|
}
|
||||||
Toggle(isOn: $preferences.soundEffectEnabled) {
|
Toggle(isOn: $preferences.soundEffectEnabled) {
|
||||||
Label("settings.other.sound-effect", systemImage: "hifispeaker")
|
Label("settings.other.sound-effect", systemImage: "hifispeaker")
|
||||||
}
|
}
|
||||||
|
Toggle(isOn: $preferences.fastRefreshEnabled) {
|
||||||
|
Label("settings.other.fast-refresh", systemImage: "arrow.clockwise")
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
|
Text("settings.section.other")
|
||||||
|
} footer: {
|
||||||
|
Text("settings.section.other.footer")
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var postStreamingSection: some View {
|
||||||
|
@Bindable var preferences = preferences
|
||||||
|
Section {
|
||||||
|
Toggle(isOn: $preferences.isPostsStreamingEnabled) {
|
||||||
|
Label("Posts streaming", systemImage: "clock.badge")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Streaming")
|
||||||
|
} footer: {
|
||||||
|
Text("Enabling post streaming will automatically add new posts at the top of your home timeline. Disable if you get performance issues.")
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var AISection: some View {
|
||||||
|
@Bindable var preferences = preferences
|
||||||
|
Section {
|
||||||
|
Toggle(isOn: $preferences.isOpenAIEnabled) {
|
||||||
|
Label("settings.other.hide-openai", systemImage: "faxmachine")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("AI")
|
||||||
|
} footer: {
|
||||||
|
Text("Disable to hide AI assisted tool options such as copywritting and alt-image description generated using AI. Uses OpenAI API. See our Privacy Policy for more information.")
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var appSection: some View {
|
private var appSection: some View {
|
||||||
Section {
|
Section {
|
||||||
if !ProcessInfo.processInfo.isiOSAppOnMac {
|
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
||||||
NavigationLink(destination: IconSelectorView()) {
|
NavigationLink(destination: IconSelectorView()) {
|
||||||
Label {
|
Label {
|
||||||
Text("settings.app.icon")
|
Text("settings.app.icon")
|
||||||
} icon: {
|
} icon: {
|
||||||
let icon = IconSelectorView.Icon(string: UIApplication.shared.alternateIconName ?? "AppIcon")
|
let icon = IconSelectorView.Icon(string: UIApplication.shared.alternateIconName ?? "AppIcon")
|
||||||
Image(uiImage: .init(named: icon.iconName)!)
|
if let image: UIImage = .init(named: icon.previewImageName) {
|
||||||
|
Image(uiImage: image)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 25, height: 25)
|
.frame(width: 25, height: 25)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp")!) {
|
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp")!) {
|
||||||
Label("settings.app.source", systemImage: "link")
|
Label("settings.app.source", systemImage: "link")
|
||||||
}
|
}
|
||||||
|
.accessibilityRemoveTraits(.isButton)
|
||||||
.tint(theme.labelColor)
|
.tint(theme.labelColor)
|
||||||
|
|
||||||
NavigationLink(destination: SupportAppView()) {
|
NavigationLink(destination: SupportAppView()) {
|
||||||
|
@ -215,13 +317,22 @@ struct SettingsTabs: View {
|
||||||
Link(destination: reviewURL) {
|
Link(destination: reviewURL) {
|
||||||
Label("settings.rate", systemImage: "link")
|
Label("settings.rate", systemImage: "link")
|
||||||
}
|
}
|
||||||
|
.accessibilityRemoveTraits(.isButton)
|
||||||
.tint(theme.labelColor)
|
.tint(theme.labelColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink(destination: AboutView()) {
|
NavigationLink {
|
||||||
|
AboutView()
|
||||||
|
} label: {
|
||||||
Label("settings.app.about", systemImage: "info.circle")
|
Label("settings.app.about", systemImage: "info.circle")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
WishlistView()
|
||||||
|
} label: {
|
||||||
|
Label("Feature Requests", systemImage: "list.bullet.rectangle.portrait")
|
||||||
|
}
|
||||||
|
|
||||||
} header: {
|
} header: {
|
||||||
Text("settings.section.app")
|
Text("settings.section.app")
|
||||||
} footer: {
|
} footer: {
|
||||||
|
@ -229,14 +340,16 @@ struct SettingsTabs: View {
|
||||||
Text("settings.section.app.footer \(appVersion)").frame(maxWidth: .infinity, alignment: .center)
|
Text("settings.section.app.footer \(appVersion)").frame(maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addAccountButton: some View {
|
private var addAccountButton: some View {
|
||||||
Button {
|
Button {
|
||||||
addAccountSheetPresented.toggle()
|
addAccountSheetPresented.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
Text("settings.account.add")
|
Label("settings.account.add", systemImage: "person.badge.plus")
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $addAccountSheetPresented) {
|
.sheet(isPresented: $addAccountSheetPresented) {
|
||||||
AddAccountView()
|
AddAccountView()
|
||||||
|
@ -244,43 +357,23 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var editAccountButton: some View {
|
private var editAccountButton: some View {
|
||||||
Button(role: isEditingAccount ? .none : .destructive) {
|
Button(role: .destructive) {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
isEditingAccount.toggle()
|
isEditingAccount.toggle()
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
if isEditingAccount {
|
if isEditingAccount {
|
||||||
Text("action.done")
|
Label("action.done", systemImage: "person.badge.minus")
|
||||||
|
.foregroundStyle(.red)
|
||||||
} else {
|
} else {
|
||||||
Text("account.action.logout")
|
Label("account.action.logout", systemImage: "person.badge.minus")
|
||||||
|
.foregroundStyle(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var remoteLocalTimelinesView: some View {
|
|
||||||
Form {
|
|
||||||
ForEach(preferences.remoteLocalTimelines, id: \.self) { server in
|
|
||||||
Text(server)
|
|
||||||
}.onDelete { indexes in
|
|
||||||
if let index = indexes.first {
|
|
||||||
_ = preferences.remoteLocalTimelines.remove(at: index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
|
||||||
Button {
|
|
||||||
routerPath.presentedSheet = .addRemoteLocalTimeline
|
|
||||||
} label: {
|
|
||||||
Label("settings.timeline.add", systemImage: "badge.plus.radiowaves.right")
|
|
||||||
}
|
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
|
||||||
}
|
|
||||||
.navigationTitle("settings.general.remote-timelines")
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
.background(theme.secondaryBackgroundColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cacheSection: some View {
|
private var cacheSection: some View {
|
||||||
Section("settings.section.cache") {
|
Section {
|
||||||
if cachedRemoved {
|
if cachedRemoved {
|
||||||
Text("action.done")
|
Text("action.done")
|
||||||
.transition(.move(edge: .leading))
|
.transition(.move(edge: .leading))
|
||||||
|
@ -292,7 +385,13 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
|
Text("settings.section.cache")
|
||||||
|
} footer: {
|
||||||
|
Text("Remove all cached images and videos")
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct SidebarEntriesSettingsView: View {
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
|
||||||
|
@State private var sidebarTabs = SidebarTabs.shared
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
@Bindable var userPreferences = userPreferences
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
ForEach($sidebarTabs.tabs, id: \.tab) { $tab in
|
||||||
|
if tab.tab != .profile && tab.tab != .settings {
|
||||||
|
Toggle(isOn: $tab.enabled) {
|
||||||
|
tab.tab.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onMove(perform: move)
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.environment(\.editMode, .constant(.active))
|
||||||
|
.navigationTitle("settings.general.sidebarEntries")
|
||||||
|
#if !os(visionOS)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func move(from source: IndexSet, to destination: Int) {
|
||||||
|
sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import RevenueCat
|
import RevenueCat
|
||||||
import Shimmer
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct SupportAppView: View {
|
struct SupportAppView: View {
|
||||||
enum Tip: String, CaseIterable {
|
enum Tip: String, CaseIterable {
|
||||||
case one, two, three, four, supporter
|
case one, two, three, four, supporter
|
||||||
|
@ -19,35 +19,35 @@ struct SupportAppView: View {
|
||||||
var title: LocalizedStringKey {
|
var title: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .one:
|
case .one:
|
||||||
return "settings.support.one.title"
|
"settings.support.one.title"
|
||||||
case .two:
|
case .two:
|
||||||
return "settings.support.two.title"
|
"settings.support.two.title"
|
||||||
case .three:
|
case .three:
|
||||||
return "settings.support.three.title"
|
"settings.support.three.title"
|
||||||
case .four:
|
case .four:
|
||||||
return "settings.support.four.title"
|
"settings.support.four.title"
|
||||||
case .supporter:
|
case .supporter:
|
||||||
return "settings.support.supporter.title"
|
"settings.support.supporter.title"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitle: LocalizedStringKey {
|
var subtitle: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .one:
|
case .one:
|
||||||
return "settings.support.one.subtitle"
|
"settings.support.one.subtitle"
|
||||||
case .two:
|
case .two:
|
||||||
return "settings.support.two.subtitle"
|
"settings.support.two.subtitle"
|
||||||
case .three:
|
case .three:
|
||||||
return "settings.support.three.subtitle"
|
"settings.support.three.subtitle"
|
||||||
case .four:
|
case .four:
|
||||||
return "settings.support.four.subtitle"
|
"settings.support.four.subtitle"
|
||||||
case .supporter:
|
case .supporter:
|
||||||
return "settings.support.supporter.subtitle"
|
"settings.support.supporter.subtitle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
|
|
||||||
|
@ -68,8 +68,10 @@ struct SupportAppView: View {
|
||||||
linksSection
|
linksSection
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.support.navigation-title")
|
.navigationTitle("settings.support.navigation-title")
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
|
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
|
||||||
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
|
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
|
||||||
}, message: {
|
}, message: {
|
||||||
|
@ -103,9 +105,9 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchStoreProducts() {
|
private func fetchStoreProducts() {
|
||||||
Purchases.shared.getProducts(Tip.allCases.map { $0.productId }) { products in
|
Purchases.shared.getProducts(Tip.allCases.map(\.productId)) { products in
|
||||||
self.subscription = products.first(where: { $0.productIdentifier == Tip.supporter.productId })
|
subscription = products.first(where: { $0.productIdentifier == Tip.supporter.productId })
|
||||||
self.products = products.filter{ $0.productIdentifier != Tip.supporter.productId}.sorted(by: { $0.price < $1.price })
|
self.products = products.filter { $0.productIdentifier != Tip.supporter.productId }.sorted(by: { $0.price < $1.price })
|
||||||
withAnimation {
|
withAnimation {
|
||||||
loadingProducts = false
|
loadingProducts = false
|
||||||
}
|
}
|
||||||
|
@ -114,7 +116,7 @@ struct SupportAppView: View {
|
||||||
|
|
||||||
private func refreshUserInfo() {
|
private func refreshUserInfo() {
|
||||||
Purchases.shared.getCustomerInfo { info, _ in
|
Purchases.shared.getCustomerInfo { info, _ in
|
||||||
self.customerInfo = info
|
customerInfo = info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +152,9 @@ struct SupportAppView: View {
|
||||||
Text("settings.support.message-from-dev")
|
Text("settings.support.message-from-dev")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var subscriptionSection: some View {
|
private var subscriptionSection: some View {
|
||||||
|
@ -174,12 +178,11 @@ struct SupportAppView: View {
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
Text(Tip.supporter.subtitle)
|
Text(Tip.supporter.subtitle)
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
makePurchaseButton(product: subscription)
|
makePurchaseButton(product: subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
@ -188,7 +191,9 @@ struct SupportAppView: View {
|
||||||
Text("settings.support.supporter.subscription-info")
|
Text("settings.support.supporter.subscription-info")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var tipsSection: some View {
|
private var tipsSection: some View {
|
||||||
|
@ -204,7 +209,7 @@ struct SupportAppView: View {
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
Text(tip.subtitle)
|
Text(tip.subtitle)
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
makePurchaseButton(product: product)
|
makePurchaseButton(product: product)
|
||||||
|
@ -213,7 +218,9 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var restorePurchase: some View {
|
private var restorePurchase: some View {
|
||||||
|
@ -222,7 +229,7 @@ struct SupportAppView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
Purchases.shared.restorePurchases { info, _ in
|
Purchases.shared.restorePurchases { info, _ in
|
||||||
self.customerInfo = info
|
customerInfo = info
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text("settings.support.restore-purchase.button")
|
Text("settings.support.restore-purchase.button")
|
||||||
|
@ -232,7 +239,9 @@ struct SupportAppView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("settings.support.restore-purchase.explanation")
|
Text("settings.support.restore-purchase.explanation")
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.secondaryBackgroundColor)
|
.listRowBackground(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var linksSection: some View {
|
private var linksSection: some View {
|
||||||
|
@ -252,21 +261,23 @@ struct SupportAppView: View {
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.secondaryBackgroundColor)
|
.listRowBackground(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingPlaceholder: some View {
|
private var loadingPlaceholder: some View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("placeholder.loading.short.")
|
Text("placeholder.loading.short")
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
Text("settings.support.placeholder.loading-subtitle")
|
Text("settings.support.placeholder.loading-subtitle")
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
.shimmering()
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,13 @@ import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct SwipeActionsSettingsView: View {
|
struct SwipeActionsSettingsView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var userPreferences: UserPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@Bindable var userPreferences = userPreferences
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
Label("settings.swipeactions.status.leading", systemImage: "arrow.right")
|
Label("settings.swipeactions.status.leading", systemImage: "arrow.right")
|
||||||
|
@ -14,7 +16,7 @@ struct SwipeActionsSettingsView: View {
|
||||||
|
|
||||||
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusLeadingLeft,
|
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusLeadingLeft,
|
||||||
label: "settings.swipeactions.primary")
|
label: "settings.swipeactions.primary")
|
||||||
.onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { action in
|
.onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { _, action in
|
||||||
if action == .none {
|
if action == .none {
|
||||||
userPreferences.swipeActionsStatusLeadingRight = .none
|
userPreferences.swipeActionsStatusLeadingRight = .none
|
||||||
}
|
}
|
||||||
|
@ -29,7 +31,7 @@ struct SwipeActionsSettingsView: View {
|
||||||
|
|
||||||
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusTrailingRight,
|
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusTrailingRight,
|
||||||
label: "settings.swipeactions.primary")
|
label: "settings.swipeactions.primary")
|
||||||
.onChange(of: userPreferences.swipeActionsStatusTrailingRight) { action in
|
.onChange(of: userPreferences.swipeActionsStatusTrailingRight) { _, action in
|
||||||
if action == .none {
|
if action == .none {
|
||||||
userPreferences.swipeActionsStatusTrailingLeft = .none
|
userPreferences.swipeActionsStatusTrailingLeft = .none
|
||||||
}
|
}
|
||||||
|
@ -44,7 +46,9 @@ struct SwipeActionsSettingsView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("settings.swipeactions.status.explanation")
|
Text("settings.swipeactions.status.explanation")
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) {
|
Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) {
|
||||||
|
@ -60,15 +64,19 @@ struct SwipeActionsSettingsView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("settings.swipeactions.use-theme-colors-explanation")
|
Text("settings.swipeactions.use-theme-colors-explanation")
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.swipeactions.navigation-title")
|
.navigationTitle("settings.swipeactions.navigation-title")
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createStatusActionPicker(selection: Binding<StatusAction>, label: LocalizedStringKey) -> some View {
|
private func createStatusActionPicker(selection: Binding<StatusAction>, label: LocalizedStringKey) -> some View {
|
||||||
return Picker(selection: selection, label: Text(label)) {
|
Picker(selection: selection, label: Text(label)) {
|
||||||
Section {
|
Section {
|
||||||
Text(StatusAction.none.displayName()).tag(StatusAction.none)
|
Text(StatusAction.none.displayName()).tag(StatusAction.none)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct TabbarEntriesSettingsView: View {
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
|
||||||
|
@State private var tabs = iOSTabs.shared
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
@Bindable var userPreferences = userPreferences
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
Picker("settings.tabs.first-tab", selection: $tabs.firstTab) {
|
||||||
|
ForEach(AppTab.allCases) { tab in
|
||||||
|
if tab == tabs.firstTab || !tabs.tabs.contains(tab) {
|
||||||
|
tab.label.tag(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Picker("settings.tabs.second-tab", selection: $tabs.secondTab) {
|
||||||
|
ForEach(AppTab.allCases) { tab in
|
||||||
|
if tab == tabs.secondTab || !tabs.tabs.contains(tab) {
|
||||||
|
tab.label.tag(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Picker("settings.tabs.third-tab", selection: $tabs.thirdTab) {
|
||||||
|
ForEach(AppTab.allCases) { tab in
|
||||||
|
if tab == tabs.thirdTab || !tabs.tabs.contains(tab) {
|
||||||
|
tab.label.tag(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Picker("settings.tabs.fourth-tab", selection: $tabs.fourthTab) {
|
||||||
|
ForEach(AppTab.allCases) { tab in
|
||||||
|
if tab == tabs.fourthTab || !tabs.tabs.contains(tab) {
|
||||||
|
tab.label.tag(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Picker("settings.tabs.fifth-tab", selection: $tabs.fifthTab) {
|
||||||
|
ForEach(AppTab.allCases) { tab in
|
||||||
|
if tab == tabs.fifthTab || !tabs.tabs.contains(tab) {
|
||||||
|
tab.label.tag(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.navigationTitle("settings.general.tabbarEntries")
|
||||||
|
#if !os(visionOS)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
50
IceCubesApp/App/Tabs/Settings/TagsGroupSettingView.swift
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TagsGroupSettingView: View {
|
||||||
|
@Environment(\.modelContext) private var context
|
||||||
|
|
||||||
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
|
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
ForEach(tagGroups) { group in
|
||||||
|
Label(group.title, systemImage: group.symbolName)
|
||||||
|
.onTapGesture {
|
||||||
|
routerPath.presentedSheet = .editTagGroup(tagGroup: group, onSaved: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indexes in
|
||||||
|
if let index = indexes.first {
|
||||||
|
context.delete(tagGroups[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Button {
|
||||||
|
routerPath.presentedSheet = .addTagGroup
|
||||||
|
} label: {
|
||||||
|
Label("timeline.filter.add-tag-groups", systemImage: "plus")
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.navigationTitle("timeline.filter.tag-groups")
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
#if !os(visionOS)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
.toolbar {
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
153
IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct TranslationSettingsView: View {
|
||||||
|
@Environment(UserPreferences.self) private var preferences
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
|
@State private var apiKey: String = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
translationSelector
|
||||||
|
if preferences.preferredTranslationType == .useDeepl {
|
||||||
|
Section("settings.translation.user-api-key") {
|
||||||
|
deepLPicker
|
||||||
|
SecureField("settings.translation.user-api-key", text: $apiKey)
|
||||||
|
.textContentType(.password)
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if apiKey.isEmpty {
|
||||||
|
Section {
|
||||||
|
Link(destination: URL(string: "https://www.deepl.com/pro-api")!) {
|
||||||
|
Text("settings.translation.needed-message")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backgroundAPIKey
|
||||||
|
autoDetectSection
|
||||||
|
}
|
||||||
|
.navigationTitle("settings.translation.navigation-title")
|
||||||
|
#if !os(visionOS)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
.onChange(of: apiKey) {
|
||||||
|
writeNewValue()
|
||||||
|
}
|
||||||
|
.onAppear(perform: updatePrefs)
|
||||||
|
.onAppear(perform: readValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var translationSelector: some View {
|
||||||
|
@Bindable var preferences = preferences
|
||||||
|
Picker("Translation Service", selection: $preferences.preferredTranslationType) {
|
||||||
|
ForEach(allTTCases, id: \.self) { type in
|
||||||
|
Text(type.description).tag(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var allTTCases: [TranslationType] {
|
||||||
|
TranslationType.allCases.filter { type in
|
||||||
|
if type != .useApple {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
#if canImport(_Translation_SwiftUI)
|
||||||
|
if #available(iOS 17.4, *) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
return false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var deepLPicker: some View {
|
||||||
|
@Bindable var preferences = preferences
|
||||||
|
Picker("settings.translation.api-key-type", selection: $preferences.userDeeplAPIFree) {
|
||||||
|
Text("DeepL API Free").tag(true)
|
||||||
|
Text("DeepL API Pro").tag(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var autoDetectSection: some View {
|
||||||
|
@Bindable var preferences = preferences
|
||||||
|
Section {
|
||||||
|
Toggle(isOn: $preferences.autoDetectPostLanguage) {
|
||||||
|
Text("settings.translation.auto-detect-post-language")
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
Text("settings.translation.auto-detect-post-language-footer")
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var backgroundAPIKey: some View {
|
||||||
|
if preferences.preferredTranslationType != .useDeepl,
|
||||||
|
!apiKey.isEmpty
|
||||||
|
{
|
||||||
|
Section {
|
||||||
|
Text("The DeepL API Key is still stored!")
|
||||||
|
if preferences.preferredTranslationType == .useServerIfPossible {
|
||||||
|
Text("It can however still be used as a fallback for your instance's translation service.")
|
||||||
|
}
|
||||||
|
Button(role: .destructive) {
|
||||||
|
withAnimation {
|
||||||
|
writeNewValue(value: "")
|
||||||
|
readValue()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("action.delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeNewValue() {
|
||||||
|
writeNewValue(value: apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeNewValue(value: String) {
|
||||||
|
DeepLUserAPIHandler.write(value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func readValue() {
|
||||||
|
apiKey = DeepLUserAPIHandler.readKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePrefs() {
|
||||||
|
DeepLUserAPIHandler.deactivateToggleIfNoKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TranslationSettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TranslationSettingsView()
|
||||||
|
.environment(UserPreferences.shared)
|
||||||
|
}
|
||||||
|
}
|
8
IceCubesApp/App/Tabs/Settings/WishlistView.swift
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import SwiftUI
|
||||||
|
import WishKit
|
||||||
|
|
||||||
|
struct WishlistView: View {
|
||||||
|
var body: some View {
|
||||||
|
WishKit.view
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,54 +1,78 @@
|
||||||
import Account
|
import Account
|
||||||
|
import AppIntents
|
||||||
|
import DesignSystem
|
||||||
import Explore
|
import Explore
|
||||||
import Foundation
|
import Foundation
|
||||||
import Status
|
import StatusKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import DesignSystem
|
|
||||||
|
|
||||||
enum Tab: Int, Identifiable, Hashable {
|
@MainActor
|
||||||
|
enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||||
case timeline, notifications, mentions, explore, messages, settings, other
|
case timeline, notifications, mentions, explore, messages, settings, other
|
||||||
case trending, federated, local
|
case trending, federated, local
|
||||||
case profile
|
case profile
|
||||||
|
case bookmarks
|
||||||
|
case favorites
|
||||||
|
case post
|
||||||
|
case followedTags
|
||||||
|
case lists
|
||||||
|
case links
|
||||||
|
|
||||||
var id: Int {
|
nonisolated var id: Int {
|
||||||
rawValue
|
rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
static func loggedOutTab() -> [Tab] {
|
static func loggedOutTab() -> [AppTab] {
|
||||||
[.timeline, .settings]
|
[.timeline, .settings]
|
||||||
}
|
}
|
||||||
|
|
||||||
static func loggedInTabs() -> [Tab] {
|
static func visionOSTab() -> [AppTab] {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
[.profile, .timeline, .notifications, .mentions, .explore, .post, .settings]
|
||||||
return [.timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .settings]
|
|
||||||
} else {
|
|
||||||
return [.timeline, .notifications, .explore, .messages, .profile]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeContentView(popToRootTab: Binding<Tab>) -> some View {
|
func makeContentView(selectedTab: Binding<AppTab>) -> some View {
|
||||||
switch self {
|
switch self {
|
||||||
case .timeline:
|
case .timeline:
|
||||||
TimelineTab(popToRootTab: popToRootTab)
|
TimelineTab()
|
||||||
case .trending:
|
case .trending:
|
||||||
TimelineTab(popToRootTab: popToRootTab, timeline: .trending)
|
TimelineTab(timeline: .trending)
|
||||||
case .local:
|
case .local:
|
||||||
TimelineTab(popToRootTab: popToRootTab, timeline: .local)
|
TimelineTab(timeline: .local)
|
||||||
case .federated:
|
case .federated:
|
||||||
TimelineTab(popToRootTab: popToRootTab, timeline: .federated)
|
TimelineTab(timeline: .federated)
|
||||||
case .notifications:
|
case .notifications:
|
||||||
NotificationsTab(popToRootTab: popToRootTab, lockedType: nil)
|
NotificationsTab(selectedTab: selectedTab, lockedType: nil)
|
||||||
case .mentions:
|
case .mentions:
|
||||||
NotificationsTab(popToRootTab: popToRootTab, lockedType: .mention)
|
NotificationsTab(selectedTab: selectedTab, lockedType: .mention)
|
||||||
case .explore:
|
case .explore:
|
||||||
ExploreTab(popToRootTab: popToRootTab)
|
ExploreTab()
|
||||||
case .messages:
|
case .messages:
|
||||||
MessagesTab(popToRootTab: popToRootTab)
|
MessagesTab()
|
||||||
case .settings:
|
case .settings:
|
||||||
SettingsTabs(popToRootTab: popToRootTab)
|
SettingsTabs(isModal: false)
|
||||||
case .profile:
|
case .profile:
|
||||||
ProfileTab(popToRootTab: popToRootTab)
|
ProfileTab()
|
||||||
|
case .bookmarks:
|
||||||
|
NavigationTab {
|
||||||
|
AccountStatusesListView(mode: .bookmarks)
|
||||||
|
}
|
||||||
|
case .favorites:
|
||||||
|
NavigationTab {
|
||||||
|
AccountStatusesListView(mode: .favorites)
|
||||||
|
}
|
||||||
|
case .followedTags:
|
||||||
|
NavigationTab {
|
||||||
|
FollowedTagsListView()
|
||||||
|
}
|
||||||
|
case .lists:
|
||||||
|
NavigationTab {
|
||||||
|
ListsListView()
|
||||||
|
}
|
||||||
|
case .links:
|
||||||
|
NavigationTab { TrendingLinksListView(cards: []) }
|
||||||
|
case .post:
|
||||||
|
VStack {}
|
||||||
case .other:
|
case .other:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
@ -56,56 +80,194 @@ enum Tab: Int, Identifiable, Hashable {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var label: some View {
|
var label: some View {
|
||||||
|
if self != .other {
|
||||||
|
Label(title, systemImage: iconName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .timeline:
|
case .timeline:
|
||||||
Label("tab.timeline", systemImage: iconName)
|
"tab.timeline"
|
||||||
case .trending:
|
case .trending:
|
||||||
Label("tab.trending", systemImage: iconName)
|
"tab.trending"
|
||||||
case .local:
|
case .local:
|
||||||
Label("tab.local", systemImage: iconName)
|
"tab.local"
|
||||||
case .federated:
|
case .federated:
|
||||||
Label("tab.federated", systemImage: iconName)
|
"tab.federated"
|
||||||
case .notifications:
|
case .notifications:
|
||||||
Label("tab.notifications", systemImage: iconName)
|
"tab.notifications"
|
||||||
case .mentions:
|
case .mentions:
|
||||||
Label("tab.notifications", systemImage: iconName)
|
"tab.mentions"
|
||||||
case .explore:
|
case .explore:
|
||||||
Label("tab.explore", systemImage: iconName)
|
"tab.explore"
|
||||||
case .messages:
|
case .messages:
|
||||||
Label("tab.messages", systemImage: iconName)
|
"tab.messages"
|
||||||
case .settings:
|
case .settings:
|
||||||
Label("tab.settings", systemImage: iconName)
|
"tab.settings"
|
||||||
case .profile:
|
case .profile:
|
||||||
Label("tab.profile", systemImage: iconName)
|
"tab.profile"
|
||||||
|
case .bookmarks:
|
||||||
|
"accessibility.tabs.profile.picker.bookmarks"
|
||||||
|
case .favorites:
|
||||||
|
"accessibility.tabs.profile.picker.favorites"
|
||||||
|
case .post:
|
||||||
|
"menu.new-post"
|
||||||
|
case .followedTags:
|
||||||
|
"timeline.filter.tags"
|
||||||
|
case .lists:
|
||||||
|
"timeline.filter.lists"
|
||||||
|
case .links:
|
||||||
|
"explore.section.trending.links"
|
||||||
case .other:
|
case .other:
|
||||||
EmptyView()
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var iconName: String {
|
var iconName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .timeline:
|
case .timeline:
|
||||||
return "rectangle.stack"
|
"rectangle.stack"
|
||||||
case .trending:
|
case .trending:
|
||||||
return "chart.line.uptrend.xyaxis"
|
"chart.line.uptrend.xyaxis"
|
||||||
case .local:
|
case .local:
|
||||||
return "person.2"
|
"person.2"
|
||||||
case .federated:
|
case .federated:
|
||||||
return "globe.americas"
|
"globe.americas"
|
||||||
case .notifications:
|
case .notifications:
|
||||||
return "bell"
|
"bell"
|
||||||
case .mentions:
|
case .mentions:
|
||||||
return "at"
|
"at"
|
||||||
case .explore:
|
case .explore:
|
||||||
return "magnifyingglass"
|
"magnifyingglass"
|
||||||
case .messages:
|
case .messages:
|
||||||
return "tray"
|
"tray"
|
||||||
case .settings:
|
case .settings:
|
||||||
return "gear"
|
"gear"
|
||||||
case .profile:
|
case .profile:
|
||||||
return "person.crop.circle"
|
"person.crop.circle"
|
||||||
|
case .bookmarks:
|
||||||
|
"bookmark"
|
||||||
|
case .favorites:
|
||||||
|
"star"
|
||||||
|
case .post:
|
||||||
|
"square.and.pencil"
|
||||||
|
case .followedTags:
|
||||||
|
"tag"
|
||||||
|
case .lists:
|
||||||
|
"list.bullet"
|
||||||
|
case .links:
|
||||||
|
"newspaper"
|
||||||
case .other:
|
case .other:
|
||||||
return ""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
class SidebarTabs {
|
||||||
|
struct SidedebarTab: Hashable, Codable {
|
||||||
|
let tab: AppTab
|
||||||
|
var enabled: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
@AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [
|
||||||
|
.init(tab: .timeline, enabled: true),
|
||||||
|
.init(tab: .trending, enabled: true),
|
||||||
|
.init(tab: .federated, enabled: true),
|
||||||
|
.init(tab: .local, enabled: true),
|
||||||
|
.init(tab: .notifications, enabled: true),
|
||||||
|
.init(tab: .mentions, enabled: true),
|
||||||
|
.init(tab: .messages, enabled: true),
|
||||||
|
.init(tab: .explore, enabled: true),
|
||||||
|
.init(tab: .bookmarks, enabled: true),
|
||||||
|
.init(tab: .favorites, enabled: true),
|
||||||
|
.init(tab: .followedTags, enabled: true),
|
||||||
|
.init(tab: .lists, enabled: true),
|
||||||
|
.init(tab: .links, enabled: true),
|
||||||
|
|
||||||
|
.init(tab: .settings, enabled: true),
|
||||||
|
.init(tab: .profile, enabled: true),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private let storage = Storage()
|
||||||
|
public static let shared = SidebarTabs()
|
||||||
|
|
||||||
|
var tabs: [SidedebarTab] {
|
||||||
|
didSet {
|
||||||
|
storage.tabs = tabs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEnabled(_ tab: AppTab) -> Bool {
|
||||||
|
tabs.first(where: { $0.tab.id == tab.id })?.enabled == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
tabs = storage.tabs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
class iOSTabs {
|
||||||
|
enum TabEntries: String {
|
||||||
|
case first, second, third, fourth, fifth
|
||||||
|
}
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
@AppStorage(TabEntries.first.rawValue) var firstTab = AppTab.timeline
|
||||||
|
@AppStorage(TabEntries.second.rawValue) var secondTab = AppTab.notifications
|
||||||
|
@AppStorage(TabEntries.third.rawValue) var thirdTab = AppTab.explore
|
||||||
|
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = AppTab.links
|
||||||
|
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = AppTab.profile
|
||||||
|
}
|
||||||
|
|
||||||
|
private let storage = Storage()
|
||||||
|
public static let shared = iOSTabs()
|
||||||
|
|
||||||
|
var tabs: [AppTab] {
|
||||||
|
[firstTab, secondTab, thirdTab, fourthTab, fifthTab]
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstTab: AppTab {
|
||||||
|
didSet {
|
||||||
|
storage.firstTab = firstTab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondTab: AppTab {
|
||||||
|
didSet {
|
||||||
|
storage.secondTab = secondTab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var thirdTab: AppTab {
|
||||||
|
didSet {
|
||||||
|
storage.thirdTab = thirdTab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fourthTab: AppTab {
|
||||||
|
didSet {
|
||||||
|
storage.fourthTab = fourthTab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fifthTab: AppTab {
|
||||||
|
didSet {
|
||||||
|
storage.fifthTab = fifthTab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
firstTab = storage.firstTab
|
||||||
|
secondTab = storage.secondTab
|
||||||
|
thirdTab = storage.thirdTab
|
||||||
|
fourthTab = storage.fourthTab
|
||||||
|
fifthTab = storage.fifthTab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
448
IceCubesApp/App/Tabs/TagGroup/EditTagGroupView.swift
Normal file
|
@ -0,0 +1,448 @@
|
||||||
|
import Combine
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import NukeUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct EditTagGroupView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@Environment(\.modelContext) private var context
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
|
@State private var tagGroup: TagGroup
|
||||||
|
|
||||||
|
private let onSaved: ((TagGroup) -> Void)?
|
||||||
|
private let isNewGroup: Bool
|
||||||
|
|
||||||
|
@FocusState private var focusedField: Focus?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
TitleInputView(
|
||||||
|
title: $tagGroup.title,
|
||||||
|
titleValidationStatus: tagGroup.titleValidationStatus,
|
||||||
|
focusedField: $focusedField,
|
||||||
|
isNewGroup: isNewGroup
|
||||||
|
)
|
||||||
|
|
||||||
|
SymbolInputView(
|
||||||
|
selectedSymbol: $tagGroup.symbolName,
|
||||||
|
selectedSymbolValidationStatus: tagGroup.symbolNameValidationStatus,
|
||||||
|
focusedField: $focusedField
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Section("add-tag-groups.edit.tags") {
|
||||||
|
TagsInputView(
|
||||||
|
tags: $tagGroup.tags,
|
||||||
|
tagsValidationStatus: tagGroup.tagsValidationStatus,
|
||||||
|
focusedField: $focusedField
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
.navigationTitle(
|
||||||
|
isNewGroup
|
||||||
|
? "timeline.filter.add-tag-groups"
|
||||||
|
: "timeline.filter.edit-tag-groups"
|
||||||
|
)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
#if !os(visionOS)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
.scrollDismissesKeyboard(.interactively)
|
||||||
|
#endif
|
||||||
|
.toolbar {
|
||||||
|
CancelToolbarItem()
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("action.save", action: { save() })
|
||||||
|
.disabled(!tagGroup.isValid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
focusedField = .title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(tagGroup: TagGroup = .emptyGroup(), onSaved: ((TagGroup) -> Void)? = nil) {
|
||||||
|
_tagGroup = State(wrappedValue: tagGroup)
|
||||||
|
self.onSaved = onSaved
|
||||||
|
isNewGroup = tagGroup.title.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save() {
|
||||||
|
tagGroup.format()
|
||||||
|
context.insert(tagGroup)
|
||||||
|
onSaved?(tagGroup)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Focus {
|
||||||
|
case title
|
||||||
|
case symbol
|
||||||
|
case new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AddTagGroupView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let container = try? ModelContainer(for: TagGroup.self, configurations: ModelConfiguration())
|
||||||
|
|
||||||
|
// need to use `sheet` to show `symbolsSuggestionView` in preview
|
||||||
|
return Text(verbatim: "parent view for EditTagGroupView")
|
||||||
|
.sheet(isPresented: .constant(true)) {
|
||||||
|
EditTagGroupView()
|
||||||
|
.withEnvironments()
|
||||||
|
.modelContainer(container!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct TitleInputView: View {
|
||||||
|
@Binding var title: String
|
||||||
|
let titleValidationStatus: TagGroup.TitleValidationStatus
|
||||||
|
|
||||||
|
@FocusState.Binding var focusedField: EditTagGroupView.Focus?
|
||||||
|
|
||||||
|
@Query var tagGroups: [TagGroup]
|
||||||
|
|
||||||
|
let isNewGroup: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TextField("add-tag-groups.edit.title.field", text: $title, axis: .horizontal)
|
||||||
|
.focused($focusedField, equals: .title)
|
||||||
|
.onSubmit {
|
||||||
|
focusedField = .symbol
|
||||||
|
}
|
||||||
|
|
||||||
|
if focusedField == .title, warningText != "" {
|
||||||
|
Text(warningText).warningLabel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var warningText: LocalizedStringKey {
|
||||||
|
if case let .invalid(description) = titleValidationStatus {
|
||||||
|
return description
|
||||||
|
} else if
|
||||||
|
isNewGroup,
|
||||||
|
tagGroups.contains(where: { $0.title == title })
|
||||||
|
{
|
||||||
|
return "\(title) add-tag-groups.edit.title.field.warning.already-exists"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct SymbolInputView: View {
|
||||||
|
@State private var symbolQuery = ""
|
||||||
|
|
||||||
|
@Binding var selectedSymbol: String
|
||||||
|
let selectedSymbolValidationStatus: TagGroup.SymbolNameValidationStatus
|
||||||
|
|
||||||
|
@FocusState.Binding var focusedField: EditTagGroupView.Focus?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
TextField("add-tag-groups.edit.icon.field", text: $symbolQuery, axis: .horizontal)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
.focused($focusedField, equals: .symbol)
|
||||||
|
.onSubmit {
|
||||||
|
if TagGroup.allSymbols.contains(symbolQuery) {
|
||||||
|
selectedSymbol = symbolQuery
|
||||||
|
}
|
||||||
|
focusedField = .new
|
||||||
|
}
|
||||||
|
.onChange(of: focusedField) {
|
||||||
|
symbolQuery = selectedSymbol
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(systemName: selectedSymbol)
|
||||||
|
.frame(height: 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
if case let .invalid(description) = selectedSymbolValidationStatus,
|
||||||
|
focusedField == .symbol
|
||||||
|
{
|
||||||
|
Text(description).warningLabel()
|
||||||
|
}
|
||||||
|
|
||||||
|
if focusedField == .symbol {
|
||||||
|
SymbolSearchResultsView(
|
||||||
|
symbolQuery: $symbolQuery,
|
||||||
|
selectedSymbol: $selectedSymbol,
|
||||||
|
focusedField: $focusedField
|
||||||
|
)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.frame(height: 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct TagsInputView: View {
|
||||||
|
@State private var newTag: String = ""
|
||||||
|
@Binding var tags: [String]
|
||||||
|
let tagsValidationStatus: TagGroup.TagsValidationStatus
|
||||||
|
|
||||||
|
@FocusState.Binding var focusedField: EditTagGroupView.Focus?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ForEach(tags, id: \.self) { tag in
|
||||||
|
HStack {
|
||||||
|
Text(tag)
|
||||||
|
Spacer()
|
||||||
|
Button { deleteTag(tag) } label: {
|
||||||
|
Image(systemName: "trash")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indexes in
|
||||||
|
if let index = indexes.first {
|
||||||
|
let tag = tags[index]
|
||||||
|
deleteTag(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this `VStack` need to be here to overcome a SwiftUI bug
|
||||||
|
// "add new tag" `TextField` is not focused after adding the first tag
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
// this condition is using to overcome a SwiftUI bug
|
||||||
|
// "add new tag" `TextField` is not focused after adding the first tag
|
||||||
|
if tags.isEmpty {
|
||||||
|
addNewTagTextField()
|
||||||
|
} else {
|
||||||
|
addNewTagTextField()
|
||||||
|
.onAppear { focusedField = .new }
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if !newTag.isEmpty, !tags.contains(newTag) {
|
||||||
|
Button { addNewTag() } label: {
|
||||||
|
Image(systemName: "checkmark.circle.fill").tint(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if focusedField == .new, warningText != "" {
|
||||||
|
Text(warningText).warningLabel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var warningText: LocalizedStringKey {
|
||||||
|
if tags.contains(newTag) {
|
||||||
|
return "add-tag-groups.edit.tags.field.warning.duplicated-tag"
|
||||||
|
} else if case let .invalid(description) = tagsValidationStatus {
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addNewTagTextField() -> some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TextField("add-tag-groups.edit.tags.add", text: $newTag, axis: .horizontal)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
.onSubmit {
|
||||||
|
addNewTag()
|
||||||
|
}
|
||||||
|
.focused($focusedField, equals: .new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addNewTag() {
|
||||||
|
addTag(newTag.trimmingCharacters(in: .whitespaces).lowercased())
|
||||||
|
newTag = ""
|
||||||
|
focusedField = .new
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addTag(_ tag: String) {
|
||||||
|
guard !tag.isEmpty,
|
||||||
|
!tags.contains(tag)
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
tags.append(tag)
|
||||||
|
tags.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteTag(_ tag: String) {
|
||||||
|
tags.removeAll(where: { $0 == tag })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct SymbolSearchResultsView: View {
|
||||||
|
@Binding var symbolQuery: String
|
||||||
|
@Binding var selectedSymbol: String
|
||||||
|
@State private var results: [String] = []
|
||||||
|
|
||||||
|
@FocusState.Binding var focusedField: EditTagGroupView.Focus?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
switch validationStatus {
|
||||||
|
case .valid:
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack {
|
||||||
|
ForEach(results, id: \.self) { name in
|
||||||
|
Button {
|
||||||
|
results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol)
|
||||||
|
selectedSymbol = name
|
||||||
|
symbolQuery = name
|
||||||
|
focusedField = .new
|
||||||
|
} label: {
|
||||||
|
Image(systemName: name)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.spring(duration: 0.2), value: results)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol)
|
||||||
|
}
|
||||||
|
case let .invalid(description):
|
||||||
|
Text(description)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol)
|
||||||
|
}
|
||||||
|
.onChange(of: symbolQuery) {
|
||||||
|
results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: search results validation
|
||||||
|
|
||||||
|
enum ValidationStatus: Equatable {
|
||||||
|
case valid
|
||||||
|
case invalid(description: LocalizedStringKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationStatus: ValidationStatus {
|
||||||
|
if results.isEmpty {
|
||||||
|
if symbolQuery == selectedSymbol,
|
||||||
|
!symbolQuery.isEmpty,
|
||||||
|
results.count == 0
|
||||||
|
{
|
||||||
|
.invalid(description: "\(symbolQuery) add-tag-groups.edit.tags.field.warning.search-results.already-selected")
|
||||||
|
} else {
|
||||||
|
.invalid(description: "add-tag-groups.edit.tags.field.warning.search-results.no-symbol-found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
.valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TagGroup {
|
||||||
|
// MARK: title validation
|
||||||
|
|
||||||
|
enum TitleValidationStatus: Equatable {
|
||||||
|
case valid
|
||||||
|
case invalid(description: LocalizedStringKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleValidationStatus: TitleValidationStatus {
|
||||||
|
title.isEmpty
|
||||||
|
? .invalid(description: "add-tag-groups.edit.title.field.warning.empty-title")
|
||||||
|
: .valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: symbolName validation
|
||||||
|
|
||||||
|
enum SymbolNameValidationStatus: Equatable {
|
||||||
|
case valid
|
||||||
|
case invalid(description: LocalizedStringKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbolNameValidationStatus: SymbolNameValidationStatus {
|
||||||
|
if symbolName.isEmpty {
|
||||||
|
return .invalid(description: "add-tag-groups.edit.title.field.warning.no-symbol-selected")
|
||||||
|
} else if !Self.allSymbols.contains(symbolName) {
|
||||||
|
return .invalid(description: "\(symbolName) add-tag-groups.edit.title.field.warning.invalid-sfsymbol-name")
|
||||||
|
}
|
||||||
|
|
||||||
|
return .valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: tags validation
|
||||||
|
|
||||||
|
enum TagsValidationStatus: Equatable {
|
||||||
|
case valid
|
||||||
|
case invalid(description: LocalizedStringKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagsValidationStatus: TagsValidationStatus {
|
||||||
|
if tags.count < 2 {
|
||||||
|
return .invalid(description: "add-tag-groups.edit.tags.field.warning.number-of-tags")
|
||||||
|
}
|
||||||
|
return .valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: TagGroup validation
|
||||||
|
|
||||||
|
var isValid: Bool {
|
||||||
|
titleValidationStatus == .valid
|
||||||
|
&& symbolNameValidationStatus == .valid
|
||||||
|
&& tagsValidationStatus == .valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: format
|
||||||
|
|
||||||
|
func format() {
|
||||||
|
title = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
tags = tags.map { $0.lowercased() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: static members
|
||||||
|
|
||||||
|
static func emptyGroup() -> TagGroup {
|
||||||
|
TagGroup(title: "", symbolName: "", tags: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
static func searchSymbol(for query: String, exclude excludedSymbol: String) -> [String] {
|
||||||
|
guard !query.isEmpty else { return [] }
|
||||||
|
|
||||||
|
return allSymbols.filter {
|
||||||
|
$0.contains(query) &&
|
||||||
|
$0 != excludedSymbol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static let allSymbols: [String] = SFSymbol.allSymbols.map { symbol in
|
||||||
|
symbol.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Text {
|
||||||
|
func warningLabel() -> Text {
|
||||||
|
font(.caption)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,14 +4,15 @@ import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import Shimmer
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct AddRemoteTimelineView: View {
|
struct AddRemoteTimelineView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@Environment(\.modelContext) private var context
|
||||||
|
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@Environment(UserPreferences.self) private var preferences
|
||||||
@EnvironmentObject private var theme: Theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
@State private var instanceName: String = ""
|
@State private var instanceName: String = ""
|
||||||
@State private var instance: Instance?
|
@State private var instance: Instance?
|
||||||
|
@ -36,29 +37,35 @@ struct AddRemoteTimelineView: View {
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.green)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
if !instanceName.isEmpty && instance == nil {
|
||||||
|
Label("timeline.\(instanceName)-not-valid", systemImage: "xmark.seal.fill")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}
|
||||||
Button {
|
Button {
|
||||||
guard instance != nil else { return }
|
guard instance != nil else { return }
|
||||||
preferences.remoteLocalTimelines.append(instanceName)
|
context.insert(LocalTimeline(instance: instanceName))
|
||||||
dismiss()
|
dismiss()
|
||||||
} label: {
|
} label: {
|
||||||
Text("timeline.add.action.add")
|
Text("timeline.add.action.add")
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
.disabled(instance == nil)
|
||||||
|
|
||||||
instancesListView
|
instancesListView
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
.navigationTitle("timeline.add-remote.title")
|
.navigationTitle("timeline.add-remote.title")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
CancelToolbarItem()
|
||||||
Button("action.cancel", action: { dismiss() })
|
|
||||||
}
|
}
|
||||||
}
|
.onChange(of: instanceName) { _, newValue in
|
||||||
.onChange(of: instanceName) { newValue in
|
|
||||||
instanceNamePublisher.send(newValue)
|
instanceNamePublisher.send(newValue)
|
||||||
}
|
}
|
||||||
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
|
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
|
||||||
|
@ -70,8 +77,9 @@ struct AddRemoteTimelineView: View {
|
||||||
.onAppear {
|
.onAppear {
|
||||||
isInstanceURLFieldFocused = true
|
isInstanceURLFieldFocused = true
|
||||||
let client = InstanceSocialClient()
|
let client = InstanceSocialClient()
|
||||||
|
let instanceName = instanceName
|
||||||
Task {
|
Task {
|
||||||
self.instances = await client.fetchInstances()
|
instances = await client.fetchInstances(keyword: instanceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +93,7 @@ struct AddRemoteTimelineView: View {
|
||||||
} else {
|
} else {
|
||||||
ForEach(instanceName.isEmpty ? instances : instances.filter { $0.name.contains(instanceName.lowercased()) }) { instance in
|
ForEach(instanceName.isEmpty ? instances : instances.filter { $0.name.contains(instanceName.lowercased()) }) { instance in
|
||||||
Button {
|
Button {
|
||||||
self.instanceName = instance.name
|
instanceName = instance.name
|
||||||
} label: {
|
} label: {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(instance.name)
|
Text(instance.name)
|
||||||
|
@ -93,13 +101,13 @@ struct AddRemoteTimelineView: View {
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
Text(instance.info?.shortDescription ?? "")
|
Text(instance.info?.shortDescription ?? "")
|
||||||
.font(.scaledBody)
|
.font(.scaledBody)
|
||||||
.foregroundColor(.gray)
|
.foregroundStyle(Color.secondary)
|
||||||
|
|
||||||
(Text("instance.list.users-\(instance.users)")
|
(Text("instance.list.users-\(instance.users)")
|
||||||
+ Text(" ⸱ ")
|
+ Text(" ⸱ ")
|
||||||
+ Text("instance.list.posts-\(instance.statuses)"))
|
+ Text("instance.list.posts-\(instance.statuses)"))
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundStyle(Color.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
|
@ -4,153 +4,122 @@ import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftData
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Timeline
|
import Timeline
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct TimelineTab: View {
|
struct TimelineTab: View {
|
||||||
@EnvironmentObject private var appAccount: AppAccountsManager
|
@Environment(\.modelContext) private var context
|
||||||
@EnvironmentObject private var theme: Theme
|
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@Environment(AppAccountsManager.self) private var appAccount
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@Environment(Theme.self) private var theme
|
||||||
@EnvironmentObject private var client: Client
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@StateObject private var routerPath = RouterPath()
|
@Environment(UserPreferences.self) private var preferences
|
||||||
@Binding var popToRootTab: Tab
|
@Environment(Client.self) private var client
|
||||||
|
@State private var routerPath = RouterPath()
|
||||||
|
|
||||||
@State private var didAppear: Bool = false
|
@State private var didAppear: Bool = false
|
||||||
@State private var timeline: TimelineFilter
|
@State private var timeline: TimelineFilter = .home
|
||||||
@State private var scrollToTopSignal: Int = 0
|
@State private var selectedTagGroup: TagGroup?
|
||||||
|
|
||||||
@AppStorage("last_timeline_filter") public var lastTimelineFilter: TimelineFilter = .home
|
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
|
||||||
|
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
|
||||||
|
|
||||||
|
@AppStorage("last_timeline_filter") var lastTimelineFilter: TimelineFilter = .home
|
||||||
|
@AppStorage("timeline_pinned_filters") private var pinnedFilters: [TimelineFilter] = []
|
||||||
|
|
||||||
private let canFilterTimeline: Bool
|
private let canFilterTimeline: Bool
|
||||||
|
|
||||||
init(popToRootTab: Binding<Tab>, timeline: TimelineFilter? = nil) {
|
init(timeline: TimelineFilter? = nil) {
|
||||||
canFilterTimeline = timeline == nil
|
canFilterTimeline = timeline == nil
|
||||||
self.timeline = timeline ?? .home
|
_timeline = .init(initialValue: timeline ?? .home)
|
||||||
_popToRootTab = popToRootTab
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routerPath.path) {
|
NavigationStack(path: $routerPath.path) {
|
||||||
TimelineView(timeline: $timeline, scrollToTopSignal: $scrollToTopSignal)
|
TimelineView(timeline: $timeline,
|
||||||
|
pinnedFilters: $pinnedFilters,
|
||||||
|
selectedTagGroup: $selectedTagGroup,
|
||||||
|
canFilterTimeline: canFilterTimeline)
|
||||||
.withAppRouter()
|
.withAppRouter()
|
||||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
toolbarView
|
toolbarView
|
||||||
}
|
}
|
||||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||||
.id(client.id)
|
.id(client.id)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routerPath.client = client
|
routerPath.client = client
|
||||||
if !didAppear && canFilterTimeline {
|
if !didAppear, canFilterTimeline {
|
||||||
didAppear = true
|
didAppear = true
|
||||||
if client.isAuth {
|
if client.isAuth {
|
||||||
timeline = lastTimelineFilter
|
timeline = lastTimelineFilter
|
||||||
} else {
|
} else {
|
||||||
timeline = .federated
|
timeline = .trending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task {
|
|
||||||
await currentAccount.fetchLists()
|
|
||||||
}
|
|
||||||
if !client.isAuth {
|
if !client.isAuth {
|
||||||
routerPath.presentedSheet = .addAccount
|
routerPath.presentedSheet = .addAccount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: client.isAuth, perform: { _ in
|
.task {
|
||||||
if client.isAuth {
|
await currentAccount.fetchLists()
|
||||||
timeline = lastTimelineFilter
|
|
||||||
} else {
|
|
||||||
timeline = .federated
|
|
||||||
}
|
}
|
||||||
})
|
.onChange(of: client.isAuth) {
|
||||||
.onChange(of: currentAccount.account?.id, perform: { _ in
|
resetTimelineFilter()
|
||||||
|
}
|
||||||
|
.onChange(of: currentAccount.account?.id) {
|
||||||
|
resetTimelineFilter()
|
||||||
|
}
|
||||||
|
.onChange(of: client.id) {
|
||||||
|
routerPath.path = []
|
||||||
|
}
|
||||||
|
.onChange(of: timeline) { _, newValue in
|
||||||
if client.isAuth, canFilterTimeline {
|
if client.isAuth, canFilterTimeline {
|
||||||
timeline = lastTimelineFilter
|
lastTimelineFilter = newValue
|
||||||
} else {
|
}
|
||||||
|
switch newValue {
|
||||||
|
case let .tagGroup(title, _, _):
|
||||||
|
if let group = tagGroups.first(where: { $0.title == title }) {
|
||||||
|
selectedTagGroup = group
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
selectedTagGroup = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .refreshTimeline)) { _ in
|
||||||
|
timeline = .latest
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .trendingTimeline)) { _ in
|
||||||
|
timeline = .trending
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .localTimeline)) { _ in
|
||||||
|
timeline = .local
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .federatedTimeline)) { _ in
|
||||||
timeline = .federated
|
timeline = .federated
|
||||||
}
|
}
|
||||||
})
|
.onReceive(NotificationCenter.default.publisher(for: .homeTimeline)) { _ in
|
||||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
timeline = .home
|
||||||
if popToRootTab == .timeline {
|
|
||||||
if routerPath.path.isEmpty {
|
|
||||||
scrollToTopSignal += 1
|
|
||||||
} else {
|
|
||||||
routerPath.path = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: client.id) { _ in
|
|
||||||
routerPath.path = []
|
|
||||||
}
|
|
||||||
.onChange(of: timeline) { timeline in
|
|
||||||
if timeline == .home || timeline == .federated || timeline == .local {
|
|
||||||
lastTimelineFilter = timeline
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.withSafariRouter()
|
.withSafariRouter()
|
||||||
.environmentObject(routerPath)
|
.environment(routerPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var timelineFilterButton: some View {
|
private var timelineFilterButton: some View {
|
||||||
if timeline.supportNewestPagination {
|
headerGroup
|
||||||
Button {
|
timelineFiltersButtons
|
||||||
self.timeline = .latest
|
if client.isAuth {
|
||||||
} label: {
|
listsFiltersButons
|
||||||
Label(TimelineFilter.latest.localizedTitle(), systemImage: TimelineFilter.latest.iconName() ?? "")
|
tagsFiltersButtons
|
||||||
}
|
}
|
||||||
.keyboardShortcut("r", modifiers: .command)
|
localTimelinesFiltersButtons
|
||||||
|
tagGroupsFiltersButtons
|
||||||
Divider()
|
Divider()
|
||||||
}
|
contentFilterButton
|
||||||
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
|
|
||||||
Button {
|
|
||||||
self.timeline = timeline
|
|
||||||
} label: {
|
|
||||||
Label(timeline.localizedTitle(), systemImage: timeline.iconName() ?? "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !currentAccount.lists.isEmpty {
|
|
||||||
Menu("timeline.filter.lists") {
|
|
||||||
ForEach(currentAccount.sortedLists) { list in
|
|
||||||
Button {
|
|
||||||
timeline = .list(list: list)
|
|
||||||
} label: {
|
|
||||||
Label(list.title, systemImage: "list.bullet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !currentAccount.tags.isEmpty {
|
|
||||||
Menu("timeline.filter.tags") {
|
|
||||||
ForEach(currentAccount.sortedTags) { tag in
|
|
||||||
Button {
|
|
||||||
timeline = .hashtag(tag: tag.name, accountId: nil)
|
|
||||||
} label: {
|
|
||||||
Label("#\(tag.name)", systemImage: "number")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu("timeline.filter.local") {
|
|
||||||
ForEach(preferences.remoteLocalTimelines, id: \.self) { server in
|
|
||||||
Button {
|
|
||||||
timeline = .remoteLocal(server: server, filter: .local)
|
|
||||||
} label: {
|
|
||||||
VStack {
|
|
||||||
Label(server, systemImage: "dot.radiowaves.right")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
routerPath.presentedSheet = .addRemoteLocalTimeline
|
|
||||||
} label: {
|
|
||||||
Label("timeline.filter.add-local", systemImage: "badge.plus.radiowaves.right")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addAccountButton: some View {
|
private var addAccountButton: some View {
|
||||||
|
@ -170,17 +139,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if client.isAuth {
|
if client.isAuth {
|
||||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
ToolbarTab(routerPath: $routerPath)
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
AppAccountsSelectorView(routerPath: routerPath)
|
|
||||||
.id(currentAccount.account?.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statusEditorToolbarItem(routerPath: routerPath,
|
|
||||||
visibility: preferences.postVisibility)
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad && !preferences.showiPadSecondaryColumn {
|
|
||||||
SecondaryColumnToolbarItem()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
addAccountButton
|
addAccountButton
|
||||||
|
@ -215,4 +174,149 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var headerGroup: some View {
|
||||||
|
ControlGroup {
|
||||||
|
if timeline.supportNewestPagination {
|
||||||
|
Button {
|
||||||
|
timeline = .latest
|
||||||
|
} label: {
|
||||||
|
Label(TimelineFilter.latest.localizedTitle(), systemImage: TimelineFilter.latest.iconName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if timeline == .home {
|
||||||
|
Button {
|
||||||
|
timeline = .resume
|
||||||
|
} label: {
|
||||||
|
VStack {
|
||||||
|
Label(TimelineFilter.resume.localizedTitle(),
|
||||||
|
systemImage: TimelineFilter.resume.iconName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pinButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var pinButton: some View {
|
||||||
|
let index = pinnedFilters.firstIndex(where: { $0.id == timeline.id })
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
if let index {
|
||||||
|
let timeline = pinnedFilters.remove(at: index)
|
||||||
|
Telemetry.signal("timeline.pin.removed", parameters: ["timeline" : timeline.rawValue])
|
||||||
|
} else {
|
||||||
|
pinnedFilters.append(timeline)
|
||||||
|
Telemetry.signal("timeline.pin.added", parameters: ["timeline" : timeline.rawValue])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if index != nil {
|
||||||
|
Label("status.action.unpin", systemImage: "pin.slash")
|
||||||
|
} else {
|
||||||
|
Label("status.action.pin", systemImage: "pin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var timelineFiltersButtons: some View {
|
||||||
|
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
|
||||||
|
Button {
|
||||||
|
self.timeline = timeline
|
||||||
|
} label: {
|
||||||
|
Label(timeline.localizedTitle(), systemImage: timeline.iconName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var listsFiltersButons: some View {
|
||||||
|
Menu("timeline.filter.lists") {
|
||||||
|
Button {
|
||||||
|
routerPath.presentedSheet = .listCreate
|
||||||
|
} label: {
|
||||||
|
Label("account.list.create", systemImage: "plus")
|
||||||
|
}
|
||||||
|
ForEach(currentAccount.sortedLists) { list in
|
||||||
|
Button {
|
||||||
|
timeline = .list(list: list)
|
||||||
|
} label: {
|
||||||
|
Label(list.title, systemImage: "list.bullet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var tagsFiltersButtons: some View {
|
||||||
|
if !currentAccount.tags.isEmpty {
|
||||||
|
Menu("timeline.filter.tags") {
|
||||||
|
ForEach(currentAccount.sortedTags) { tag in
|
||||||
|
Button {
|
||||||
|
timeline = .hashtag(tag: tag.name, accountId: nil)
|
||||||
|
} label: {
|
||||||
|
Label("#\(tag.name)", systemImage: "number")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var localTimelinesFiltersButtons: some View {
|
||||||
|
Menu("timeline.filter.local") {
|
||||||
|
ForEach(localTimelines) { remoteLocal in
|
||||||
|
Button {
|
||||||
|
timeline = .remoteLocal(server: remoteLocal.instance, filter: .local)
|
||||||
|
} label: {
|
||||||
|
VStack {
|
||||||
|
Label(remoteLocal.instance, systemImage: "dot.radiowaves.right")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
routerPath.presentedSheet = .addRemoteLocalTimeline
|
||||||
|
} label: {
|
||||||
|
Label("timeline.filter.add-local", systemImage: "badge.plus.radiowaves.right")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var tagGroupsFiltersButtons: some View {
|
||||||
|
Menu("timeline.filter.tag-groups") {
|
||||||
|
ForEach(tagGroups) { group in
|
||||||
|
Button {
|
||||||
|
timeline = .tagGroup(title: group.title, tags: group.tags, symbolName: group.symbolName)
|
||||||
|
} label: {
|
||||||
|
VStack {
|
||||||
|
let icon = group.symbolName.isEmpty ? "number" : group.symbolName
|
||||||
|
Label(group.title, systemImage: icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
routerPath.presentedSheet = .addTagGroup
|
||||||
|
} label: {
|
||||||
|
Label("timeline.filter.add-tag-groups", systemImage: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contentFilterButton: some View {
|
||||||
|
Button(action: {
|
||||||
|
routerPath.presentedSheet = .timelineContentFilter
|
||||||
|
}, label: {
|
||||||
|
Label("timeline.content-filter.title", systemSymbol: .line3HorizontalDecrease)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetTimelineFilter() {
|
||||||
|
if client.isAuth, canFilterTimeline {
|
||||||
|
timeline = lastTimelineFilter
|
||||||
|
} else if !client.isAuth {
|
||||||
|
timeline = .trending
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
51
IceCubesApp/App/Tabs/ToolbarTab.swift
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import AppAccount
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct ToolbarTab: ToolbarContent {
|
||||||
|
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
|
@Binding var routerPath: RouterPath
|
||||||
|
|
||||||
|
var body: some ToolbarContent {
|
||||||
|
if !isSecondaryColumn {
|
||||||
|
if horizontalSizeClass == .regular {
|
||||||
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
userPreferences.isSidebarExpanded.toggle()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if userPreferences.isSidebarExpanded {
|
||||||
|
Image(systemName: "sidebar.squares.left")
|
||||||
|
} else {
|
||||||
|
Image(systemName: "sidebar.left")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statusEditorToolbarItem(routerPath: routerPath,
|
||||||
|
visibility: userPreferences.postVisibility)
|
||||||
|
if UIDevice.current.userInterfaceIdiom != .pad ||
|
||||||
|
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
|
||||||
|
{
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
AppAccountsSelectorView(routerPath: routerPath, avatarConfig: theme.avatarShape == .circle ? .badge : .badgeRounded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .regular {
|
||||||
|
if (!isSecondaryColumn && !userPreferences.showiPadSecondaryColumn) || isSecondaryColumn {
|
||||||
|
SecondaryColumnToolbarItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/128.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/16.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/256 1.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/256.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/32 1.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/32.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/512 1.png
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/512.png
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/64.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/AppIcon-fs8.png
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/Content.png
Normal file
After Width: | Height: | Size: 501 KiB |
36
IceCubesApp/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file → Executable file
|
@ -1,57 +1,91 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "icon.png",
|
"filename" : "AppIcon-fs8.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "dark.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "tinted.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "16.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "16x16"
|
"size" : "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "32.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "16x16"
|
"size" : "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "32 1.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "32x32"
|
"size" : "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "64.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "32x32"
|
"size" : "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "128.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "256 1.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "256.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "512 1.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "512.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "512x512"
|
"size" : "512x512"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "Content.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "512x512"
|
"size" : "512x512"
|
||||||
|
|
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/dark.png
Normal file
After Width: | Height: | Size: 370 KiB |
Before Width: | Height: | Size: 295 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/tinted.png
Normal file
After Width: | Height: | Size: 545 KiB |
After Width: | Height: | Size: 764 KiB |
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Background.png",
|
||||||
|
"idiom" : "vision",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"filename" : "Front.solidimagestacklayer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Mid.solidimagestacklayer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Back.solidimagestacklayer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Layer 1.png",
|
||||||
|
"idiom" : "vision",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 549 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "vision",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/AppIcon-fs8.png
vendored
Normal file
After Width: | Height: | Size: 79 KiB |
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "icon14.heic",
|
"filename" : "AppIcon-fs8.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
After Width: | Height: | Size: 156 KiB |
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-fs8.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate1-image.imageset/1024.png
vendored
Normal file
After Width: | Height: | Size: 85 KiB |
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "icon.heic",
|
"filename" : "1024.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
After Width: | Height: | Size: 846 KiB |
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "icon.png",
|
"filename" : "1024.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|
Before Width: | Height: | Size: 615 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/AppIconAlternate10.png
vendored
Normal file
After Width: | Height: | Size: 151 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AppIconAlternate10.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 565 KiB |
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_4b25c3a5-7ae2-401b-9b2f-17c9832e175a.png",
|
"filename" : "AppIconAlternate10.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|
21
IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon15.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/icon15.png
vendored
Normal file
After Width: | Height: | Size: 120 KiB |
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_7ca60288-3e96-43a3-a88b-f4c882ac2183.png",
|
"filename" : "icon15.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 406 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon16.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/icon16.png
vendored
Normal file
After Width: | Height: | Size: 108 KiB |
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_2338f943-14f9-4639-aabf-70dc18cf4d52.png",
|
"filename" : "icon16.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|
Before Width: | Height: | Size: 401 KiB After Width: | Height: | Size: 401 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon17.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/icon17.png
vendored
Normal file
After Width: | Height: | Size: 72 KiB |