Compare commits
423 commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
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"
|
|
@ -9,13 +9,22 @@
|
|||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "buttonkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Dean151/ButtonKit",
|
||||
"state" : {
|
||||
"revision" : "377f5bab4ed047704316d531e0826d4de5ebf6a4",
|
||||
"version" : "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "emojitext",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/divadretlaw/EmojiText",
|
||||
"state" : {
|
||||
"revision" : "d0664390e3236ff6241ea0586d80f4e92702973b",
|
||||
"version" : "3.2.1"
|
||||
"revision" : "c54000aa9ccc048619054a5a2da2ce0576ffea18",
|
||||
"version" : "4.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -23,8 +32,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Giphy/giphy-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "95c32b862185e76107841b49bfff8ff7230f3b68",
|
||||
"version" : "2.2.7"
|
||||
"revision" : "9c58a350a3381f1641f5a31cdcd162a406274892",
|
||||
"version" : "2.2.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -59,17 +68,17 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke",
|
||||
"state" : {
|
||||
"revision" : "1694798e876113d44f6ec6ead965d7286695981d",
|
||||
"version" : "12.2.0"
|
||||
"revision" : "8ecbfc886da39bccb01c34abef5f2ff4073ad633",
|
||||
"version" : "12.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "purchases-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/RevenueCat/purchases-ios.git",
|
||||
"location" : "https://github.com/RevenueCat/purchases-ios",
|
||||
"state" : {
|
||||
"revision" : "b8d20ba1c8e13cc73d72e37cf98607d01fd357b6",
|
||||
"version" : "4.31.2"
|
||||
"revision" : "a9763ca482d52ea3d59aa2dfd2fc23427b02dada",
|
||||
"version" : "4.40.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -90,6 +99,24 @@
|
|||
"version" : "0.14.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-cmark",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-cmark.git",
|
||||
"state" : {
|
||||
"revision" : "f218e5d7691f78b55bfa39b367763f4612486c35",
|
||||
"version" : "0.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-markdown",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-markdown",
|
||||
"state" : {
|
||||
"revision" : "e4f95e2dc23097a1a9a1dfdfe3fe3ee44de77378",
|
||||
"version" : "0.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftsoup",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -107,15 +134,6 @@
|
|||
"revision" : "9e1cc02a65b22e09a8251261cccbccce02731fc5",
|
||||
"version" : "1.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-shimmer",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/markiv/SwiftUI-Shimmer",
|
||||
"state" : {
|
||||
"revision" : "965a7cbcbf094cbcf22b9251a2323bdc3432e171",
|
||||
"version" : "1.1.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1530"
|
||||
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>
|
|
@ -8,7 +8,8 @@ import LinkPresentation
|
|||
import Lists
|
||||
import MediaUI
|
||||
import Models
|
||||
import Status
|
||||
import Notifications
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
|
||||
|
@ -33,11 +34,13 @@ extension View {
|
|||
ConversationDetailView(conversation: conversation)
|
||||
case let .hashTag(tag, accountId):
|
||||
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
scrollToTopSignal: .constant(0),
|
||||
canFilterTimeline: false)
|
||||
case let .list(list):
|
||||
TimelineView(timeline: .constant(.list(list: list)),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
scrollToTopSignal: .constant(0),
|
||||
canFilterTimeline: false)
|
||||
|
@ -53,67 +56,114 @@ extension View {
|
|||
AccountsListView(mode: .accountsList(accounts: accounts))
|
||||
case .trendingTimeline:
|
||||
TimelineView(timeline: .constant(.trending),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
scrollToTopSignal: .constant(0),
|
||||
canFilterTimeline: false)
|
||||
case let .trendingLinks(cards):
|
||||
CardsListView(cards: cards)
|
||||
TrendingLinksListView(cards: cards)
|
||||
case let .tagsList(tags):
|
||||
TagsListView(tags: tags)
|
||||
case .notificationsRequests:
|
||||
NotificationsRequestsListView()
|
||||
case let .notificationForAccount(accountId):
|
||||
NotificationsListView(lockedType: nil,
|
||||
lockedAccountId: accountId,
|
||||
scrollToTopSignal: .constant(0))
|
||||
case .blockedAccounts:
|
||||
AccountsListView(mode: .blocked)
|
||||
case .mutedAccounts:
|
||||
AccountsListView(mode: .muted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withSheetDestinations(sheetDestinations: Binding<SheetDestination?>) -> some View {
|
||||
sheet(item: sheetDestinations) { destination in
|
||||
Group {
|
||||
switch destination {
|
||||
case let .replyToStatusEditor(status):
|
||||
StatusEditorView(mode: .replyTo(status: status))
|
||||
case let .newStatusEditor(visibility):
|
||||
StatusEditorView(mode: .new(visibility: visibility))
|
||||
case let .editStatusEditor(status):
|
||||
StatusEditorView(mode: .edit(status: status))
|
||||
case let .quoteStatusEditor(status):
|
||||
StatusEditorView(mode: .quote(status: status))
|
||||
case let .mentionStatusEditor(account, visibility):
|
||||
StatusEditorView(mode: .mention(account: account, visibility: visibility))
|
||||
case .listCreate:
|
||||
ListCreateView()
|
||||
case let .listEdit(list):
|
||||
ListEditView(list: list)
|
||||
case let .listAddAccount(account):
|
||||
ListAddAccountView(account: account)
|
||||
case .addAccount:
|
||||
AddAccountView()
|
||||
case .addRemoteLocalTimeline:
|
||||
AddRemoteTimelineView()
|
||||
case .addTagGroup:
|
||||
EditTagGroupView()
|
||||
case let .statusEditHistory(status):
|
||||
StatusEditHistoryView(statusId: status)
|
||||
case .settings:
|
||||
SettingsTabs(popToRootTab: .constant(.settings), isModal: true)
|
||||
.preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light)
|
||||
case .accountPushNotficationsSettings:
|
||||
if let subscription = PushNotificationsService.shared.subscriptions.first(where: { $0.account.token == AppAccountsManager.shared.currentAccount.oauthToken }) {
|
||||
NavigationSheet { PushNotificationsView(subscription: subscription) }
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
case .about:
|
||||
NavigationSheet { AboutView() }
|
||||
case .support:
|
||||
NavigationSheet { SupportAppView() }
|
||||
case let .report(status):
|
||||
ReportView(status: status)
|
||||
case let .shareImage(image, status):
|
||||
ActivityView(image: image, status: status)
|
||||
case let .editTagGroup(tagGroup, onSaved):
|
||||
EditTagGroupView(tagGroup: tagGroup, onSaved: onSaved)
|
||||
switch destination {
|
||||
case let .replyToStatusEditor(status):
|
||||
StatusEditor.MainView(mode: .replyTo(status: status))
|
||||
.withEnvironments()
|
||||
case let .newStatusEditor(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()
|
||||
case let .editStatusEditor(status):
|
||||
StatusEditor.MainView(mode: .edit(status: status))
|
||||
.withEnvironments()
|
||||
case let .quoteStatusEditor(status):
|
||||
StatusEditor.MainView(mode: .quote(status: status))
|
||||
.withEnvironments()
|
||||
case let .quoteLinkStatusEditor(link):
|
||||
StatusEditor.MainView(mode: .quoteLink(link: link))
|
||||
.withEnvironments()
|
||||
case let .mentionStatusEditor(account, visibility):
|
||||
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case .listCreate:
|
||||
ListCreateView()
|
||||
.withEnvironments()
|
||||
case let .listEdit(list):
|
||||
ListEditView(list: list)
|
||||
.withEnvironments()
|
||||
case let .listAddAccount(account):
|
||||
ListAddAccountView(account: account)
|
||||
.withEnvironments()
|
||||
case .addAccount:
|
||||
AddAccountView()
|
||||
.withEnvironments()
|
||||
case .addRemoteLocalTimeline:
|
||||
AddRemoteTimelineView()
|
||||
.withEnvironments()
|
||||
case .addTagGroup:
|
||||
EditTagGroupView()
|
||||
.withEnvironments()
|
||||
case let .statusEditHistory(status):
|
||||
StatusEditHistoryView(statusId: status)
|
||||
.withEnvironments()
|
||||
case .settings:
|
||||
SettingsTabs(popToRootTab: .constant(.settings), isModal: true)
|
||||
.withEnvironments()
|
||||
.preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light)
|
||||
case .accountPushNotficationsSettings:
|
||||
if let subscription = PushNotificationsService.shared.subscriptions.first(where: { $0.account.token == AppAccountsManager.shared.currentAccount.oauthToken }) {
|
||||
NavigationSheet { PushNotificationsView(subscription: subscription) }
|
||||
.withEnvironments()
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
case .about:
|
||||
NavigationSheet { AboutView() }
|
||||
.withEnvironments()
|
||||
case .support:
|
||||
NavigationSheet { SupportAppView() }
|
||||
.withEnvironments()
|
||||
case let .report(status):
|
||||
ReportView(status: status)
|
||||
.withEnvironments()
|
||||
case let .shareImage(image, 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()
|
||||
}
|
||||
.withEnvironments()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,6 +183,7 @@ extension View {
|
|||
Draft.self,
|
||||
LocalTimeline.self,
|
||||
TagGroup.self,
|
||||
RecentTag.self,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.$(BUNDLE_ID_PREFIX).IceCubesApp</string>
|
||||
<string>iCloud.icecubesapp</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
|
|
150
IceCubesApp/App/Main/AppView.swift
Normal file
|
@ -0,0 +1,150 @@
|
|||
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: Tab
|
||||
@Binding var appRouterPath: RouterPath
|
||||
|
||||
@State var popToRootTab: Tab = .other
|
||||
@State var iosTabs = iOSTabs.shared
|
||||
@State var sidebarTabs = SidebarTabs.shared
|
||||
|
||||
var body: some View {
|
||||
#if os(visionOS)
|
||||
tabBarView
|
||||
#else
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
sidebarView
|
||||
} else {
|
||||
tabBarView
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var availableTabs: [Tab] {
|
||||
guard appAccountsManager.currentClient.isAuth else {
|
||||
return Tab.loggedOutTab()
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact {
|
||||
return iosTabs.tabs
|
||||
} else if UIDevice.current.userInterfaceIdiom == .vision {
|
||||
return Tab.visionOSTab()
|
||||
}
|
||||
return sidebarTabs.tabs.map { $0.tab }
|
||||
}
|
||||
|
||||
var tabBarView: some View {
|
||||
TabView(selection: .init(get: {
|
||||
selectedTab
|
||||
}, set: { newTab in
|
||||
if newTab == .post {
|
||||
#if os(visionOS)
|
||||
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||
#else
|
||||
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
#endif
|
||||
return
|
||||
}
|
||||
if newTab == selectedTab {
|
||||
/// Stupid hack to trigger onChange binding in tab views.
|
||||
popToRootTab = .other
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
popToRootTab = selectedTab
|
||||
}
|
||||
}
|
||||
|
||||
HapticManager.shared.fireHaptic(.tabSelection)
|
||||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
|
||||
selectedTab = newTab
|
||||
})) {
|
||||
ForEach(availableTabs) { tab in
|
||||
tab.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
|
||||
.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)
|
||||
}
|
||||
|
||||
private func badgeFor(tab: Tab) -> 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: $selectedTab,
|
||||
popToRootTab: $popToRootTab,
|
||||
tabs: availableTabs)
|
||||
{
|
||||
HStack(spacing: 0) {
|
||||
TabView(selection: $selectedTab) {
|
||||
ForEach(availableTabs) { tab in
|
||||
tab
|
||||
.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
|
||||
.tabItem {
|
||||
tab.label
|
||||
}
|
||||
.tag(tab)
|
||||
}
|
||||
}
|
||||
.introspect(.tabView, on: .iOS(.v17)) { (tabview: UITabBarController) in
|
||||
tabview.tabBar.isHidden = horizontalSizeClass == .regular
|
||||
tabview.customizableViewControllers = []
|
||||
tabview.moreNavigationController.isNavigationBarHidden = true
|
||||
}
|
||||
if horizontalSizeClass == .regular,
|
||||
appAccountsManager.currentClient.isAuth,
|
||||
userPreferences.showiPadSecondaryColumn
|
||||
{
|
||||
Divider().edgesIgnoringSafeArea(.all)
|
||||
notificationsSecondaryColumn
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(appRouterPath)
|
||||
}
|
||||
#endif
|
||||
|
||||
var notificationsSecondaryColumn: some View {
|
||||
NotificationsTab(selectedTab: .constant(.notifications),
|
||||
popToRootTab: $popToRootTab, lockedType: nil)
|
||||
.environment(\.isSecondaryColumn, true)
|
||||
.frame(maxWidth: .secondaryColumnWidth)
|
||||
.id(appAccountsManager.currentAccount.id)
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ extension IceCubesApp {
|
|||
#if targetEnvironment(macCatalyst)
|
||||
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||
#else
|
||||
sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
#endif
|
||||
}
|
||||
.keyboardShortcut("n", modifiers: .command)
|
||||
|
@ -54,5 +54,11 @@ extension IceCubesApp {
|
|||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import AppIntents
|
||||
import Env
|
||||
import MediaUI
|
||||
import Status
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
extension IceCubesApp {
|
||||
var appScene: some Scene {
|
||||
WindowGroup(id: "MainWindow") {
|
||||
appView
|
||||
AppView(selectedTab: $selectedTab, appRouterPath: $appRouterPath)
|
||||
.applyTheme(theme)
|
||||
.onAppear {
|
||||
setNewClientsInEnv(client: appAccountsManager.currentClient)
|
||||
|
@ -22,6 +23,7 @@ extension IceCubesApp {
|
|||
.environment(theme)
|
||||
.environment(watcher)
|
||||
.environment(pushNotificationsService)
|
||||
.environment(appIntentService)
|
||||
.environment(\.isSupporter, isSupporter)
|
||||
.sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in
|
||||
MediaUIView(selectedAttachment: selectedMediaAttachment,
|
||||
|
@ -47,8 +49,19 @@ extension IceCubesApp {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: appIntentService.handledIntent) { _, _ in
|
||||
if let intent = appIntentService.handledIntent?.intent {
|
||||
handleIntent(intent)
|
||||
appIntentService.handledIntent = nil
|
||||
}
|
||||
}
|
||||
.withModelContainer()
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
.defaultSize(width: userPreferences.showiPadSecondaryColumn ? 1100 : 800, height: 1400)
|
||||
#elseif os(visionOS)
|
||||
.defaultSize(width: 800, height: 1200)
|
||||
#endif
|
||||
.commands {
|
||||
appMenu
|
||||
}
|
||||
|
@ -63,33 +76,31 @@ extension IceCubesApp {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var appView: some View {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
sidebarView
|
||||
} else {
|
||||
tabBarView
|
||||
}
|
||||
}
|
||||
|
||||
@SceneBuilder
|
||||
var otherScenes: some Scene {
|
||||
WindowGroup(for: WindowDestinationEditor.self) { destination in
|
||||
Group {
|
||||
switch destination.wrappedValue {
|
||||
case let .newStatusEditor(visibility):
|
||||
StatusEditorView(mode: .new(visibility: 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):
|
||||
StatusEditorView(mode: .edit(status: status))
|
||||
StatusEditor.MainView(mode: .edit(status: status))
|
||||
case let .quoteStatusEditor(status):
|
||||
StatusEditorView(mode: .quote(status: status))
|
||||
StatusEditor.MainView(mode: .quote(status: status))
|
||||
case let .replyToStatusEditor(status):
|
||||
StatusEditorView(mode: .replyTo(status: 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(RouterPath())
|
||||
.withModelContainer()
|
||||
.applyTheme(theme)
|
||||
.frame(minWidth: 300, minHeight: 400)
|
||||
|
@ -115,4 +126,23 @@ extension IceCubesApp {
|
|||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import Env
|
||||
import SwiftUI
|
||||
|
||||
extension IceCubesApp {
|
||||
var sidebarView: some View {
|
||||
SideBarView(selectedTab: $selectedTab,
|
||||
popToRootTab: $popToRootTab,
|
||||
tabs: availableTabs)
|
||||
{
|
||||
HStack(spacing: 0) {
|
||||
ZStack {
|
||||
if selectedTab == .profile {
|
||||
ProfileTab(popToRootTab: $popToRootTab)
|
||||
}
|
||||
ForEach(availableTabs) { tab in
|
||||
if tab == selectedTab || sideBarLoadedTabs.contains(tab) {
|
||||
tab
|
||||
.makeContentView(selectedTab: $selectedTab, 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) {
|
||||
sideBarLoadedTabs.removeAll()
|
||||
}
|
||||
.environment(sidebarRouterPath)
|
||||
}
|
||||
|
||||
var notificationsSecondaryColumn: some View {
|
||||
NotificationsTab(selectedTab: .constant(.notifications),
|
||||
popToRootTab: $popToRootTab, lockedType: nil)
|
||||
.environment(\.isSecondaryColumn, true)
|
||||
.frame(maxWidth: .secondaryColumnWidth)
|
||||
.id(appAccountsManager.currentAccount.id)
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import Env
|
||||
import SwiftUI
|
||||
|
||||
extension IceCubesApp {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
HapticManager.shared.fireHaptic(.tabSelection)
|
||||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
|
||||
selectedTab = newTab
|
||||
})) {
|
||||
ForEach(availableTabs) { tab in
|
||||
tab.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
|
||||
.tabItem {
|
||||
if userPreferences.showiPhoneTabLabel {
|
||||
tab.label
|
||||
} else {
|
||||
Image(systemName: tab.iconName)
|
||||
}
|
||||
}
|
||||
.tag(tab)
|
||||
.badge(badgeFor(tab: tab))
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .tabBar)
|
||||
}
|
||||
}
|
||||
.id(appAccountsManager.currentClient.id)
|
||||
}
|
||||
|
||||
private func badgeFor(tab: Tab) -> Int {
|
||||
if tab == .notifications, selectedTab != tab,
|
||||
let token = appAccountsManager.currentAccount.oauthToken
|
||||
{
|
||||
return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import KeychainSwift
|
|||
import MediaUI
|
||||
import Network
|
||||
import RevenueCat
|
||||
import Status
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
|
||||
|
@ -23,23 +23,15 @@ struct IceCubesApp: App {
|
|||
@State var currentAccount = CurrentAccount.shared
|
||||
@State var userPreferences = UserPreferences.shared
|
||||
@State var pushNotificationsService = PushNotificationsService.shared
|
||||
@State var watcher = StreamWatcher()
|
||||
@State var appIntentService = AppIntentService.shared
|
||||
@State var watcher = StreamWatcher.shared
|
||||
@State var quickLook = QuickLook.shared
|
||||
@State var theme = Theme.shared
|
||||
@State var sidebarRouterPath = RouterPath()
|
||||
|
||||
@State var selectedTab: Tab = .timeline
|
||||
@State var popToRootTab: Tab = .other
|
||||
@State var iosTabs = iOSTabs.shared
|
||||
@State var sideBarLoadedTabs: Set<Tab> = Set()
|
||||
@State var isSupporter: Bool = false
|
||||
@State var appRouterPath = RouterPath()
|
||||
|
||||
var availableTabs: [Tab] {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
return appAccountsManager.currentClient.isAuth ? iosTabs.tabs : Tab.loggedOutTab()
|
||||
}
|
||||
return appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab()
|
||||
}
|
||||
@State var isSupporter: Bool = false
|
||||
|
||||
var body: some Scene {
|
||||
appScene
|
||||
|
@ -88,11 +80,12 @@ struct IceCubesApp: App {
|
|||
}
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(_: UIApplication,
|
||||
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
|
||||
{
|
||||
try? AVAudioSession.sharedInstance().setCategory(.ambient)
|
||||
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
return true
|
||||
}
|
||||
|
@ -121,4 +114,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
}
|
||||
return configuration
|
||||
}
|
||||
|
||||
override func buildMenu(with builder: UIMenuBuilder) {
|
||||
super.buildMenu(with: builder)
|
||||
builder.remove(menu: .document)
|
||||
builder.remove(menu: .toolbar)
|
||||
builder.remove(menu: .sidebar)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import DesignSystem
|
|||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import Status
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
public struct ReportView: View {
|
||||
|
@ -36,43 +36,37 @@ public struct ReportView: View {
|
|||
.navigationTitle("report.title")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
isSendingReport = true
|
||||
Task {
|
||||
do {
|
||||
let _: ReportSent =
|
||||
try await client.post(endpoint: Statuses.report(accountId: status.account.id,
|
||||
statusId: status.id,
|
||||
comment: commentText))
|
||||
dismiss()
|
||||
isSendingReport = false
|
||||
} catch {
|
||||
isSendingReport = false
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
isSendingReport = true
|
||||
Task {
|
||||
do {
|
||||
let _: ReportSent =
|
||||
try await client.post(endpoint: Statuses.report(accountId: status.account.id,
|
||||
statusId: status.id,
|
||||
comment: commentText))
|
||||
dismiss()
|
||||
isSendingReport = false
|
||||
} catch {
|
||||
isSendingReport = false
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if isSendingReport {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("report.action.send")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if isSendingReport {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("report.action.send")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("action.cancel")
|
||||
}
|
||||
CancelToolbarItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,25 +13,28 @@ extension View {
|
|||
|
||||
@MainActor
|
||||
private struct SafariRouter: ViewModifier {
|
||||
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(UserPreferences.self) private var preferences
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
#if !os(visionOS)
|
||||
@State private var safariManager = InAppSafariManager()
|
||||
@State private var safariManager = InAppSafariManager()
|
||||
#endif
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
// Open internal URL.
|
||||
routerPath.handle(url: url)
|
||||
guard !isSecondaryColumn else { return .discarded }
|
||||
return routerPath.handle(url: url)
|
||||
})
|
||||
.onOpenURL { url in
|
||||
// Open external URL (from icecubesapp://)
|
||||
guard !isSecondaryColumn else { return }
|
||||
let urlString = url.absoluteString.replacingOccurrences(of: AppInfo.scheme, with: "https://")
|
||||
guard let url = URL(string: urlString), url.host != nil else { return }
|
||||
_ = routerPath.handle(url: url)
|
||||
_ = routerPath.handleDeepLink(url: url)
|
||||
}
|
||||
.onAppear {
|
||||
routerPath.urlHandler = { url in
|
||||
|
@ -47,81 +50,83 @@ private struct SafariRouter: ViewModifier {
|
|||
}
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
|
||||
#endif
|
||||
// SFSafariViewController only supports initial URLs with http:// or https:// schemes.
|
||||
guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else {
|
||||
return .systemAction
|
||||
}
|
||||
#if os(visionOS)
|
||||
return .systemAction
|
||||
// SFSafariViewController only supports initial URLs with http:// or https:// schemes.
|
||||
guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else {
|
||||
return .systemAction
|
||||
}
|
||||
#if os(visionOS)
|
||||
return .systemAction
|
||||
#else
|
||||
return safariManager.open(url)
|
||||
#endif
|
||||
#else
|
||||
return safariManager.open(url)
|
||||
return .systemAction
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
#if !os(visionOS)
|
||||
.background {
|
||||
WindowReader { window in
|
||||
safariManager.windowScene = window.windowScene
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
@MainActor
|
||||
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
|
||||
var windowScene: UIWindowScene?
|
||||
let viewController: UIViewController = .init()
|
||||
var window: UIWindow?
|
||||
|
||||
@MainActor
|
||||
func open(_ url: URL) -> OpenURLAction.Result {
|
||||
guard let windowScene else { return .systemAction }
|
||||
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
|
||||
var windowScene: UIWindowScene?
|
||||
let viewController: UIViewController = .init()
|
||||
var window: UIWindow?
|
||||
|
||||
window = setupWindow(windowScene: windowScene)
|
||||
@MainActor
|
||||
func open(_ url: URL) -> OpenURLAction.Result {
|
||||
guard let windowScene else { return .systemAction }
|
||||
|
||||
let configuration = SFSafariViewController.Configuration()
|
||||
configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView
|
||||
window = setupWindow(windowScene: windowScene)
|
||||
|
||||
let safari = SFSafariViewController(url: url, configuration: configuration)
|
||||
safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor)
|
||||
safari.preferredControlTintColor = UIColor(Theme.shared.tintColor)
|
||||
safari.delegate = self
|
||||
let configuration = SFSafariViewController.Configuration()
|
||||
configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.viewController.present(safari, animated: true)
|
||||
let safari = SFSafariViewController(url: url, configuration: configuration)
|
||||
safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor)
|
||||
safari.preferredControlTintColor = UIColor(Theme.shared.tintColor)
|
||||
safari.delegate = self
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.viewController.present(safari, animated: true)
|
||||
}
|
||||
|
||||
return .handled
|
||||
}
|
||||
|
||||
return .handled
|
||||
}
|
||||
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
|
||||
let window = window ?? UIWindow(windowScene: windowScene)
|
||||
|
||||
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
|
||||
let window = window ?? UIWindow(windowScene: windowScene)
|
||||
window.rootViewController = viewController
|
||||
window.makeKeyAndVisible()
|
||||
|
||||
window.rootViewController = viewController
|
||||
window.makeKeyAndVisible()
|
||||
switch Theme.shared.selectedScheme {
|
||||
case .dark:
|
||||
window.overrideUserInterfaceStyle = .dark
|
||||
case .light:
|
||||
window.overrideUserInterfaceStyle = .light
|
||||
}
|
||||
|
||||
switch Theme.shared.selectedScheme {
|
||||
case .dark:
|
||||
window.overrideUserInterfaceStyle = .dark
|
||||
case .light:
|
||||
window.overrideUserInterfaceStyle = .light
|
||||
self.window = window
|
||||
return window
|
||||
}
|
||||
|
||||
self.window = window
|
||||
return window
|
||||
}
|
||||
|
||||
nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) {
|
||||
Task { @MainActor in
|
||||
window?.resignKey()
|
||||
window?.isHidden = false
|
||||
window = nil
|
||||
nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) {
|
||||
Task { @MainActor in
|
||||
window?.resignKey()
|
||||
window?.isHidden = false
|
||||
window = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private struct WindowReader: UIViewRepresentable {
|
||||
|
|
|
@ -4,10 +4,12 @@ import DesignSystem
|
|||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
|
||||
@MainActor
|
||||
struct SideBarView<Content: View>: View {
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@Environment(AppAccountsManager.self) private var appAccounts
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
|
@ -21,6 +23,8 @@ struct SideBarView<Content: View>: View {
|
|||
var tabs: [Tab]
|
||||
@ViewBuilder var content: () -> Content
|
||||
|
||||
@State private var sidebarTabs = SidebarTabs.shared
|
||||
|
||||
private func badgeFor(tab: Tab) -> Int {
|
||||
if tab == .notifications, selectedTab != tab,
|
||||
let token = appAccounts.currentAccount.oauthToken
|
||||
|
@ -31,16 +35,25 @@ struct SideBarView<Content: View>: View {
|
|||
}
|
||||
|
||||
private func makeIconForTab(tab: Tab) -> some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
SideBarIcon(systemIconName: tab.iconName,
|
||||
isSelected: tab == selectedTab)
|
||||
let badge = badgeFor(tab: tab)
|
||||
if badge > 0 {
|
||||
makeBadgeView(count: badge)
|
||||
HStack {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
SideBarIcon(systemIconName: tab.iconName,
|
||||
isSelected: tab == selectedTab)
|
||||
let badge = badgeFor(tab: tab)
|
||||
if badge > 0 {
|
||||
makeBadgeView(count: badge)
|
||||
}
|
||||
}
|
||||
if userPreferences.isSidebarExpanded {
|
||||
Text(tab.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: .sidebarWidth, height: 50)
|
||||
.frame(width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24, height: 50)
|
||||
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear,
|
||||
in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
private func makeBadgeView(count: Int) -> some View {
|
||||
|
@ -57,7 +70,7 @@ struct SideBarView<Content: View>: View {
|
|||
|
||||
private var postButton: some View {
|
||||
Button {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
#if targetEnvironment(macCatalyst) || os(visionOS)
|
||||
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||
#else
|
||||
routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
|
@ -67,8 +80,10 @@ struct SideBarView<Content: View>: View {
|
|||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 30)
|
||||
.offset(x: 2, y: -2)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.help(Tab.post.title)
|
||||
}
|
||||
|
||||
private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View {
|
||||
|
@ -85,8 +100,19 @@ struct SideBarView<Content: View>: View {
|
|||
}
|
||||
} label: {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
AppAccountView(viewModel: .init(appAccount: account, isCompact: true))
|
||||
if showBadge,
|
||||
if userPreferences.isSidebarExpanded {
|
||||
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 notificationsCount = userPreferences.notificationsCount[token],
|
||||
notificationsCount > 0
|
||||
|
@ -94,69 +120,95 @@ struct SideBarView<Content: View>: View {
|
|||
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)
|
||||
.background(selectedTab == .profile && account.id == appAccounts.currentAccount.id ?
|
||||
theme.secondaryBackgroundColor : .clear)
|
||||
}
|
||||
|
||||
private func accountButtonTitle(accountName: String?) -> LocalizedStringKey {
|
||||
if let accountName {
|
||||
"tab.profile-account-\(accountName)"
|
||||
} else {
|
||||
Tab.profile.title
|
||||
}
|
||||
}
|
||||
|
||||
private var tabsView: some View {
|
||||
ForEach(tabs) { tab in
|
||||
Button {
|
||||
// ensure keyboard is always dismissed when selecting a tab
|
||||
hideKeyboard()
|
||||
if tab != .profile && sidebarTabs.isEnabled(tab) {
|
||||
Button {
|
||||
// ensure keyboard is always dismissed when selecting a tab
|
||||
hideKeyboard()
|
||||
|
||||
if tab == selectedTab {
|
||||
popToRootTab = .other
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
popToRootTab = tab
|
||||
if tab == selectedTab {
|
||||
popToRootTab = .other
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
popToRootTab = tab
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedTab = tab
|
||||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
if tab == .notifications {
|
||||
if let token = appAccounts.currentAccount.oauthToken {
|
||||
userPreferences.notificationsCount[token] = 0
|
||||
selectedTab = tab
|
||||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
if tab == .notifications {
|
||||
if let token = appAccounts.currentAccount.oauthToken {
|
||||
userPreferences.notificationsCount[token] = 0
|
||||
}
|
||||
watcher.unreadNotificationsCount = 0
|
||||
}
|
||||
watcher.unreadNotificationsCount = 0
|
||||
} label: {
|
||||
makeIconForTab(tab: tab)
|
||||
}
|
||||
} label: {
|
||||
makeIconForTab(tab: tab)
|
||||
.help(tab.title)
|
||||
}
|
||||
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@Bindable var routerPath = routerPath
|
||||
HStack(spacing: 0) {
|
||||
ScrollView {
|
||||
VStack(alignment: .center) {
|
||||
if appAccounts.availableAccounts.isEmpty {
|
||||
tabsView
|
||||
} else {
|
||||
ForEach(appAccounts.availableAccounts) { account in
|
||||
makeAccountButton(account: account,
|
||||
showBadge: account.id != appAccounts.currentAccount.id)
|
||||
if account.id == appAccounts.currentAccount.id {
|
||||
tabsView
|
||||
if horizontalSizeClass == .regular {
|
||||
ScrollView {
|
||||
VStack(alignment: .center) {
|
||||
if appAccounts.availableAccounts.isEmpty {
|
||||
tabsView
|
||||
} else {
|
||||
ForEach(appAccounts.availableAccounts) { account in
|
||||
makeAccountButton(account: account,
|
||||
showBadge: account.id != appAccounts.currentAccount.id)
|
||||
if account.id == appAccounts.currentAccount.id {
|
||||
tabsView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
postButton
|
||||
.padding(.top, 12)
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(.thinMaterial)
|
||||
.safeAreaInset(edge: .bottom, content: {
|
||||
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)
|
||||
}
|
||||
.frame(width: .sidebarWidth)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(.thinMaterial)
|
||||
Divider()
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
content()
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
}
|
||||
}
|
||||
|
@ -174,12 +226,14 @@ private struct SideBarIcon: View {
|
|||
.font(.title2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(isSelected ? theme.tintColor : theme.labelColor)
|
||||
.symbolVariant(isSelected ? .fill : .none)
|
||||
.scaleEffect(isHovered ? 0.8 : 1.0)
|
||||
.onHover { isHovered in
|
||||
withAnimation(.interpolatingSpring(stiffness: 300, damping: 15)) {
|
||||
self.isHovered = isHovered
|
||||
}
|
||||
}
|
||||
.frame(width: 50, height: 40)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import Env
|
|||
import Explore
|
||||
import Models
|
||||
import Network
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -22,18 +21,9 @@ struct ExploreTab: View {
|
|||
ExploreView(scrollToTopSignal: $scrollToTopSignal)
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.toolbar {
|
||||
statusEditorToolbarItem(routerPath: routerPath,
|
||||
visibility: preferences.postVisibility)
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routerPath: routerPath)
|
||||
}
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn {
|
||||
SecondaryColumnToolbarItem()
|
||||
}
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
}
|
||||
.withSafariRouter()
|
||||
|
|
|
@ -5,7 +5,6 @@ import DesignSystem
|
|||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -25,13 +24,9 @@ struct MessagesTab: View {
|
|||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbar {
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routerPath: routerPath)
|
||||
}
|
||||
}
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
}
|
||||
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
|
||||
|
|
|
@ -1,29 +1,23 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
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 {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle")
|
||||
}
|
||||
}
|
||||
CloseToolbarItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,45 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
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 {
|
||||
if !isSecondaryColumn {
|
||||
statusEditorToolbarItem(routerPath: routerPath,
|
||||
visibility: userPreferences.postVisibility)
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routerPath: routerPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
if (!isSecondaryColumn && !userPreferences.showiPadSecondaryColumn) || isSecondaryColumn {
|
||||
SecondaryColumnToolbarItem()
|
||||
}
|
||||
}
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.onChange(of: client.id) {
|
||||
routerPath.path = []
|
||||
}
|
||||
.onAppear {
|
||||
routerPath.client = client
|
||||
}
|
||||
.withSafariRouter()
|
||||
}
|
||||
.environment(routerPath)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ struct NotificationsTab: View {
|
|||
@Environment(PushNotificationsService.self) private var pushNotificationsService
|
||||
@State private var routerPath = RouterPath()
|
||||
@State private var scrollToTopSignal: Int = 0
|
||||
|
||||
|
||||
@Binding var selectedTab: Tab
|
||||
@Binding var popToRootTab: Tab
|
||||
|
||||
|
@ -37,25 +37,12 @@ struct NotificationsTab: View {
|
|||
Button {
|
||||
routerPath.presentedSheet = .accountPushNotficationsSettings
|
||||
} label: {
|
||||
Image(systemName: "bell.badge")
|
||||
}
|
||||
}
|
||||
if !isSecondaryColumn {
|
||||
statusEditorToolbarItem(routerPath: routerPath,
|
||||
visibility: userPreferences.postVisibility)
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routerPath: routerPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
if (!isSecondaryColumn && !userPreferences.showiPadSecondaryColumn) || isSecondaryColumn {
|
||||
SecondaryColumnToolbarItem()
|
||||
Image(systemName: "bell")
|
||||
}
|
||||
}
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
}
|
||||
.onAppear {
|
||||
|
@ -73,9 +60,9 @@ struct NotificationsTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedTab, { _, newValue in
|
||||
.onChange(of: selectedTab) { _, _ in
|
||||
clearNotifications()
|
||||
})
|
||||
}
|
||||
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
|
||||
if let newValue, let type = newValue.notification.supportedType {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
|
|
|
@ -5,7 +5,6 @@ import DesignSystem
|
|||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -24,7 +23,7 @@ struct ProfileTab: View {
|
|||
AccountDetailView(account: account, scrollToTopSignal: $scrollToTopSignal)
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(account.id)
|
||||
} else {
|
||||
AccountDetailView(account: .placeholder(), scrollToTopSignal: $scrollToTopSignal)
|
||||
|
|
|
@ -27,26 +27,28 @@ struct AboutView: View {
|
|||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(uiImage: .init(named: "AppIconAlternate0")!)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Image(uiImage: .init(named: "AppIconAlternate4")!)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Image(uiImage: .init(named: "AppIconAlternate17")!)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Image(uiImage: .init(named: "AppIconAlternate23")!)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Spacer()
|
||||
}
|
||||
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(uiImage: .init(named: "AppIconAlternate0")!)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Image(uiImage: .init(named: "AppIconAlternate4")!)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Image(uiImage: .init(named: "AppIconAlternate17")!)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Image(uiImage: .init(named: "AppIconAlternate23")!)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Spacer()
|
||||
}
|
||||
#endif
|
||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
|
||||
Label("settings.support.privacy-policy", systemImage: "lock")
|
||||
}
|
||||
|
@ -55,7 +57,7 @@ struct AboutView: View {
|
|||
Label("settings.support.terms-of-use", systemImage: "checkmark.shield")
|
||||
}
|
||||
} footer: {
|
||||
Text("\(versionNumber)©2023 Thomas Ricouard")
|
||||
Text("\(versionNumber)© 2024 Thomas Ricouard")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
@ -105,14 +107,14 @@ struct AboutView: View {
|
|||
}
|
||||
.listStyle(.insetGrouped)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.navigationTitle(Text("settings.about.title"))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
.navigationTitle(Text("settings.about.title"))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
@ -17,9 +17,8 @@ struct AccountSettingsView: View {
|
|||
@Environment(Theme.self) private var theme
|
||||
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||
@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 timelineCache = TimelineCache()
|
||||
|
||||
|
@ -30,7 +29,7 @@ struct AccountSettingsView: View {
|
|||
Form {
|
||||
Section {
|
||||
Button {
|
||||
isEditingAccount = true
|
||||
routerPath.presentedSheet = .accountEditInfo
|
||||
} label: {
|
||||
Label("account.action.edit-info", systemImage: "pencil")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
@ -40,7 +39,7 @@ struct AccountSettingsView: View {
|
|||
|
||||
if currentInstance.isFiltersSupported {
|
||||
Button {
|
||||
isEditingFilters = true
|
||||
routerPath.presentedSheet = .accountFiltersList
|
||||
} label: {
|
||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
@ -96,12 +95,6 @@ struct AccountSettingsView: View {
|
|||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
.sheet(isPresented: $isEditingAccount, content: {
|
||||
EditAccountView()
|
||||
})
|
||||
.sheet(isPresented: $isEditingFilters, content: {
|
||||
FiltersListView()
|
||||
})
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
|
@ -116,8 +109,8 @@ struct AccountSettingsView: View {
|
|||
}
|
||||
.navigationTitle(account.safeDisplayName)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import Models
|
|||
import Network
|
||||
import NukeUI
|
||||
import SafariServices
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -83,77 +82,75 @@ struct AddAccountView: View {
|
|||
.navigationTitle("account.add.navigation-title")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
#endif
|
||||
.toolbar {
|
||||
if !appAccountsManager.availableAccounts.isEmpty {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("action.cancel", action: { dismiss() })
|
||||
.toolbar {
|
||||
if !appAccountsManager.availableAccounts.isEmpty {
|
||||
CancelToolbarItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isInstanceURLFieldFocused = true
|
||||
Task {
|
||||
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||
withAnimation {
|
||||
self.instances = instances
|
||||
}
|
||||
}
|
||||
isSigninIn = false
|
||||
}
|
||||
.onChange(of: instanceName) {
|
||||
searchingTask.cancel()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
getInstanceDetailTask.cancel()
|
||||
getInstanceDetailTask = Task {
|
||||
try? await Task.sleep(for: .seconds(0.1))
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
do {
|
||||
// bare bones preflight for domain validity
|
||||
let instanceDetailClient = Client(server: sanitizedName)
|
||||
if
|
||||
instanceDetailClient.server.contains("."),
|
||||
instanceDetailClient.server.last != "."
|
||||
{
|
||||
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
|
||||
withAnimation {
|
||||
self.instance = instance
|
||||
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
|
||||
}
|
||||
instanceFetchError = nil
|
||||
} else {
|
||||
instance = nil
|
||||
instanceFetchError = nil
|
||||
.onAppear {
|
||||
isInstanceURLFieldFocused = true
|
||||
Task {
|
||||
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||
withAnimation {
|
||||
self.instances = instances
|
||||
}
|
||||
}
|
||||
isSigninIn = false
|
||||
}
|
||||
.onChange(of: instanceName) {
|
||||
searchingTask.cancel()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
getInstanceDetailTask.cancel()
|
||||
getInstanceDetailTask = Task {
|
||||
try? await Task.sleep(for: .seconds(0.1))
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
do {
|
||||
// bare bones preflight for domain validity
|
||||
let instanceDetailClient = Client(server: sanitizedName)
|
||||
if
|
||||
instanceDetailClient.server.contains("."),
|
||||
instanceDetailClient.server.last != "."
|
||||
{
|
||||
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
|
||||
withAnimation {
|
||||
self.instance = instance
|
||||
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
|
||||
}
|
||||
instanceFetchError = nil
|
||||
} else {
|
||||
instance = nil
|
||||
instanceFetchError = nil
|
||||
}
|
||||
} catch _ as DecodingError {
|
||||
instance = nil
|
||||
instanceFetchError = "account.add.error.instance-not-supported"
|
||||
} catch {
|
||||
instance = nil
|
||||
}
|
||||
} catch _ as DecodingError {
|
||||
instance = nil
|
||||
instanceFetchError = "account.add.error.instance-not-supported"
|
||||
} catch {
|
||||
instance = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { _, newValue in
|
||||
switch newValue {
|
||||
case .active:
|
||||
isSigninIn = false
|
||||
default:
|
||||
break
|
||||
.onChange(of: scenePhase) { _, newValue in
|
||||
switch newValue {
|
||||
case .active:
|
||||
isSigninIn = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -217,9 +214,9 @@ struct AddAccountView: View {
|
|||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
(Text("instance.list.users-\(formatAsNumber(instance.users))")
|
||||
+ Text(" ⸱ ")
|
||||
+ Text("instance.list.posts-\(formatAsNumber(instance.statuses))"))
|
||||
.foregroundStyle(theme.tintColor)
|
||||
+ Text(" ⸱ ")
|
||||
+ Text("instance.list.posts-\(formatAsNumber(instance.statuses))"))
|
||||
.foregroundStyle(theme.tintColor)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
|
||||
|
@ -265,9 +262,8 @@ struct AddAccountView: View {
|
|||
}
|
||||
.redacted(reason: .placeholder)
|
||||
.allowsHitTesting(false)
|
||||
.shimmering()
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -306,13 +302,3 @@ struct AddAccountView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SafariView: UIViewControllerRepresentable {
|
||||
let url: URL
|
||||
|
||||
func makeUIViewController(context _: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
|
||||
SFSafariViewController(url: url)
|
||||
}
|
||||
|
||||
func updateUIViewController(_: SFSafariViewController, context _: UIViewControllerRepresentableContext<SafariView>) {}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import Models
|
|||
import Network
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import UserNotifications
|
||||
|
||||
@MainActor
|
||||
|
@ -12,22 +13,18 @@ struct ContentSettingsView: View {
|
|||
@Environment(UserPreferences.self) private var userPreferences
|
||||
@Environment(Theme.self) private var theme
|
||||
|
||||
@State private var contentFilter = TimelineContentFilter.shared
|
||||
|
||||
var body: some View {
|
||||
@Bindable var userPreferences = userPreferences
|
||||
Form {
|
||||
Section("settings.content.boosts") {
|
||||
Toggle(isOn: $userPreferences.suppressDupeReblogs) {
|
||||
Text("settings.content.hide-repeated-boosts")
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section("settings.content.media") {
|
||||
Toggle(isOn: $userPreferences.autoPlayVideo) {
|
||||
Text("settings.other.autoplay-video")
|
||||
}
|
||||
Toggle(isOn: $userPreferences.muteVideo) {
|
||||
Text("settings.other.mute-video")
|
||||
}
|
||||
Toggle(isOn: $userPreferences.showAltTextForMedia) {
|
||||
Text("settings.content.media.show.alt")
|
||||
}
|
||||
|
@ -44,7 +41,7 @@ struct ContentSettingsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
|
@ -62,6 +59,7 @@ struct ContentSettingsView: View {
|
|||
userPreferences.appAutoExpandMedia = userPreferences.autoExpandMedia
|
||||
userPreferences.appDefaultPostsSensitive = userPreferences.postIsSensitive
|
||||
userPreferences.appDefaultPostVisibility = userPreferences.postVisibility
|
||||
userPreferences.appRequireAltText = userPreferences.appRequireAltText
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +84,7 @@ struct ContentSettingsView: View {
|
|||
} footer: {
|
||||
Text("settings.content.collapse-long-posts-hint")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
|
@ -115,6 +113,28 @@ struct ContentSettingsView: View {
|
|||
Text("settings.content.default-sensitive")
|
||||
}
|
||||
.disabled(userPreferences.useInstanceContentSettings)
|
||||
|
||||
Toggle(isOn: $userPreferences.appRequireAltText) {
|
||||
Text("settings.content.require-alt-text")
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.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)
|
||||
|
@ -122,8 +142,8 @@ struct ContentSettingsView: View {
|
|||
}
|
||||
.navigationTitle("settings.content.navigation-title")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@ import Env
|
|||
import Models
|
||||
import Network
|
||||
import Observation
|
||||
import Status
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable class DisplaySettingsLocalValues {
|
||||
var tintColor = Theme.shared.tintColor
|
||||
var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
||||
|
@ -35,11 +36,13 @@ struct DisplaySettingsView: View {
|
|||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
Form {
|
||||
StatusRowView(viewModel: previewStatusViewModel)
|
||||
.allowsHitTesting(false)
|
||||
.opacity(0)
|
||||
.hidden()
|
||||
themeSection
|
||||
#if !os(visionOS)
|
||||
StatusRowView(viewModel: previewStatusViewModel)
|
||||
.allowsHitTesting(false)
|
||||
.opacity(0)
|
||||
.hidden()
|
||||
themeSection
|
||||
#endif
|
||||
fontSection
|
||||
layoutSection
|
||||
platformsSection
|
||||
|
@ -47,34 +50,36 @@ struct DisplaySettingsView: View {
|
|||
}
|
||||
.navigationTitle("settings.display.navigation-title")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.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
|
||||
.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
|
||||
}
|
||||
examplePost
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,17 +246,9 @@ struct DisplaySettingsView: View {
|
|||
@ViewBuilder
|
||||
private var platformsSection: some View {
|
||||
@Bindable var userPreferences = userPreferences
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
Section("iPhone") {
|
||||
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
Section("iPad") {
|
||||
Section("settings.display.section.platform") {
|
||||
Toggle("settings.display.show-ipad-column", isOn: $userPreferences.showiPadSecondaryColumn)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
|
@ -263,21 +260,7 @@ struct DisplaySettingsView: View {
|
|||
private var resetSection: some View {
|
||||
Section {
|
||||
Button {
|
||||
theme.followSystemColorScheme = true
|
||||
theme.applySet(set: colorScheme == .dark ? .iceCubeDark : .iceCubeLight)
|
||||
theme.avatarShape = .circle
|
||||
theme.avatarPosition = .leading
|
||||
theme.statusActionsDisplay = .full
|
||||
theme.displayFullUsername = false
|
||||
theme.statusDisplayStyle = .large
|
||||
theme.lineSpacing = 1.2
|
||||
theme.fontSizeScale = 1.0
|
||||
|
||||
localValues.tintColor = theme.tintColor
|
||||
localValues.primaryBackgroundColor = theme.primaryBackgroundColor
|
||||
localValues.secondaryBackgroundColor = theme.secondaryBackgroundColor
|
||||
localValues.labelColor = theme.labelColor
|
||||
|
||||
theme.restoreDefault()
|
||||
} label: {
|
||||
Text("settings.display.restore")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Status
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -23,8 +23,8 @@ struct HapticSettingsView: View {
|
|||
}
|
||||
.navigationTitle("settings.haptic.navigation-title")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,19 +17,16 @@ struct IconSelectorView: View {
|
|||
}
|
||||
|
||||
case primary = 0
|
||||
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8
|
||||
case alt9, alt10, alt11, alt12, alt13, alt14
|
||||
case alt15, alt16, alt17, alt18, alt19, alt20, alt21
|
||||
case alt22, alt23, alt24, alt25
|
||||
case alt26, alt27, alt28
|
||||
case alt29, alt30, alt31, alt32
|
||||
case alt33
|
||||
case alt34, alt35
|
||||
case alt36
|
||||
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8, alt9, alt10, alt11, alt12, alt13, alt14, alt15
|
||||
case alt16, alt17, alt18, alt19, alt20, alt21
|
||||
case alt22, alt23, alt24, alt25, alt26
|
||||
case alt27, alt28, alt29
|
||||
case alt30, alt31, alt32, alt33, alt34, alt35, alt36
|
||||
case alt37
|
||||
case alt38, alt39
|
||||
case alt40, alt41, alt42
|
||||
case alt43, alt44, alt45, alt46, alt47, alt48, alt49
|
||||
case alt38
|
||||
case alt39, alt40, alt41, alt42, alt43
|
||||
case alt44, alt45
|
||||
case alt46
|
||||
|
||||
var appIconName: String {
|
||||
return "AppIconAlternate\(rawValue)"
|
||||
|
@ -42,17 +39,18 @@ struct IconSelectorView: View {
|
|||
let icons: [Icon]
|
||||
|
||||
static let items = [
|
||||
IconSelector(title: "settings.app.icon.official".localized, icons: [.primary, .alt1, .alt2, .alt3, .alt4, .alt5, .alt6, .alt7, .alt8,
|
||||
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14,
|
||||
.alt15, .alt16, .alt17, .alt18, .alt19, .alt25,
|
||||
.alt43, .alt44, .alt45, .alt46, .alt47]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt20, .alt21, .alt22, .alt23, .alt24]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt26, .alt27, .alt28]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Chanhwi Joo (GitHub @te6-in)", icons: [.alt29, .alt34, .alt31, .alt35, .alt30, .alt32, .alt40]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) W. Kovács Ágnes (@wildgica)", icons: [.alt33]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Duncan Horne", icons: [.alt36]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) BeAware@social.beaware.live", icons: [.alt37, .alt41, .alt42, .alt48, .alt49]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Simone Margio", icons: [.alt38, .alt39]),
|
||||
IconSelector(title: "settings.app.icon.official".localized, icons: [
|
||||
.primary, .alt46, .alt1, .alt2, .alt3, .alt4,
|
||||
.alt5, .alt6, .alt7, .alt8,
|
||||
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt15,
|
||||
.alt16, .alt17, .alt18, .alt19, .alt20, .alt21]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt22, .alt23, .alt24, .alt25, .alt26]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt27, .alt28, .alt29]),
|
||||
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]),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ struct InstanceInfoView: View {
|
|||
}
|
||||
.navigationTitle("instance.info.navigation-title")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,12 +111,12 @@ struct PushNotificationsView: View {
|
|||
}
|
||||
.navigationTitle("settings.push.navigation-title")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.task {
|
||||
await subscription.fetchSubscription()
|
||||
}
|
||||
.task {
|
||||
await subscription.fetchSubscription()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSubscription() {
|
||||
|
|
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import Timeline
|
|||
@MainActor
|
||||
struct SettingsTabs: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var context
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@Environment(PushNotificationsService.self) private var pushNotifications
|
||||
@Environment(UserPreferences.self) private var preferences
|
||||
|
@ -31,10 +31,9 @@ struct SettingsTabs: View {
|
|||
@Binding var popToRootTab: Tab
|
||||
|
||||
let isModal: Bool
|
||||
|
||||
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
|
||||
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
|
||||
|
||||
|
||||
@State private var startingPoint: SettingsStartingPoint? = nil
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
Form {
|
||||
|
@ -46,27 +45,53 @@ struct SettingsTabs: View {
|
|||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
#if !os(visionOS)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.navigationTitle(Text("settings.title"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
||||
.toolbar {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone || isModal {
|
||||
ToolbarItem {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("action.done").bold()
|
||||
.navigationTitle(Text("settings.title"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.toolbar {
|
||||
if isModal {
|
||||
ToolbarItem {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("action.done").bold()
|
||||
}
|
||||
}
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal {
|
||||
SecondaryColumnToolbarItem()
|
||||
}
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn {
|
||||
SecondaryColumnToolbarItem()
|
||||
.withAppRouter()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
}
|
||||
.onAppear {
|
||||
routerPath.client = client
|
||||
|
@ -100,7 +125,7 @@ struct SettingsTabs: View {
|
|||
.tint(.red)
|
||||
}
|
||||
}
|
||||
AppAccountView(viewModel: .init(appAccount: account))
|
||||
AppAccountView(viewModel: .init(appAccount: account), isParentPresented: .constant(false))
|
||||
}
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
|
@ -148,22 +173,29 @@ struct SettingsTabs: View {
|
|||
Label("settings.general.haptic", systemImage: "waveform.path")
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: remoteLocalTimelinesView) {
|
||||
NavigationLink(destination: RemoteTimelinesSettingView()) {
|
||||
Label("settings.general.remote-timelines", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
NavigationLink(destination: tagGroupsView) {
|
||||
NavigationLink(destination: TagsGroupSettingView()) {
|
||||
Label("timeline.filter.tag-groups", systemImage: "number")
|
||||
}
|
||||
NavigationLink(destination: RecenTagsSettingView()) {
|
||||
Label("settings.general.recent-tags", systemImage: "clock")
|
||||
}
|
||||
NavigationLink(destination: ContentSettingsView()) {
|
||||
Label("settings.general.content", systemImage: "rectangle.stack")
|
||||
}
|
||||
NavigationLink(destination: SwipeActionsSettingsView()) {
|
||||
Label("settings.general.swipeactions", systemImage: "hand.draw")
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
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")
|
||||
|
@ -299,73 +331,6 @@ struct SettingsTabs: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var tagGroupsView: 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()
|
||||
}
|
||||
}
|
||||
|
||||
private var remoteLocalTimelinesView: 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()
|
||||
}
|
||||
}
|
||||
|
||||
private var cacheSection: some View {
|
||||
Section("settings.section.cache") {
|
||||
if cachedRemoved {
|
||||
|
|
|
@ -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,7 +1,6 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import RevenueCat
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -70,24 +69,24 @@ struct SupportAppView: View {
|
|||
}
|
||||
.navigationTitle("settings.support.navigation-title")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
|
||||
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
|
||||
}, message: {
|
||||
Text("settings.support.alert.message")
|
||||
})
|
||||
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
|
||||
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
|
||||
}, message: {
|
||||
Text("settings.support.alert.error.message")
|
||||
})
|
||||
.onAppear {
|
||||
loadingProducts = true
|
||||
fetchStoreProducts()
|
||||
refreshUserInfo()
|
||||
}
|
||||
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
|
||||
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
|
||||
}, message: {
|
||||
Text("settings.support.alert.message")
|
||||
})
|
||||
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
|
||||
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
|
||||
}, message: {
|
||||
Text("settings.support.alert.error.message")
|
||||
})
|
||||
.onAppear {
|
||||
loadingProducts = true
|
||||
fetchStoreProducts()
|
||||
refreshUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private func purchase(product: StoreProduct) async {
|
||||
|
@ -280,6 +279,5 @@ struct SupportAppView: View {
|
|||
}
|
||||
.redacted(reason: .placeholder)
|
||||
.allowsHitTesting(false)
|
||||
.shimmering()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ struct SwipeActionsSettingsView: View {
|
|||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
|
||||
Section {
|
||||
Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) {
|
||||
ForEach(UserPreferences.SwipeActionsIconStyle.allCases, id: \.rawValue) { style in
|
||||
|
@ -70,8 +70,8 @@ struct SwipeActionsSettingsView: View {
|
|||
}
|
||||
.navigationTitle("settings.swipeactions.navigation-title")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,11 @@ import SwiftUI
|
|||
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) {
|
||||
|
@ -41,11 +42,18 @@ struct TabbarEntriesSettingsView: View {
|
|||
#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)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,20 +11,17 @@ struct TranslationSettingsView: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
deepLToggle
|
||||
if preferences.alwaysUseDeepl {
|
||||
translationSelector
|
||||
if preferences.preferredTranslationType == .useDeepl {
|
||||
Section("settings.translation.user-api-key") {
|
||||
deepLPicker
|
||||
SecureField("settings.translation.user-api-key", text: $apiKey)
|
||||
.textContentType(.password)
|
||||
}
|
||||
.onAppear {
|
||||
readValue()
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
|
||||
if apiKey.isEmpty {
|
||||
Section {
|
||||
Link(destination: URL(string: "https://www.deepl.com/pro-api")!) {
|
||||
|
@ -37,30 +34,51 @@ struct TranslationSettingsView: View {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
backgroundAPIKey
|
||||
autoDetectSection
|
||||
}
|
||||
.navigationTitle("settings.translation.navigation-title")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.onChange(of: apiKey) {
|
||||
writeNewValue()
|
||||
}
|
||||
.onAppear(perform: updatePrefs)
|
||||
.onChange(of: apiKey) {
|
||||
writeNewValue()
|
||||
}
|
||||
.onAppear(perform: updatePrefs)
|
||||
.onAppear(perform: readValue)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var deepLToggle: some View {
|
||||
private var translationSelector: some View {
|
||||
@Bindable var preferences = preferences
|
||||
Toggle(isOn: $preferences.alwaysUseDeepl) {
|
||||
Text("settings.translation.always-deepl")
|
||||
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
|
||||
|
@ -80,6 +98,34 @@ struct TranslationSettingsView: View {
|
|||
} 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() {
|
||||
|
@ -91,11 +137,7 @@ struct TranslationSettingsView: View {
|
|||
}
|
||||
|
||||
private func readValue() {
|
||||
if let apiKey = DeepLUserAPIHandler.readIfAllowed() {
|
||||
self.apiKey = apiKey
|
||||
} else {
|
||||
apiKey = ""
|
||||
}
|
||||
apiKey = DeepLUserAPIHandler.readKey()
|
||||
}
|
||||
|
||||
private func updatePrefs() {
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import Account
|
||||
import AppIntents
|
||||
import DesignSystem
|
||||
import Explore
|
||||
import Foundation
|
||||
import Status
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
enum Tab: Int, Identifiable, Hashable, CaseIterable {
|
||||
enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||
case timeline, notifications, mentions, explore, messages, settings, other
|
||||
case trending, federated, local
|
||||
case profile
|
||||
case bookmarks
|
||||
case favorites
|
||||
case post
|
||||
case followedTags
|
||||
case lists
|
||||
case links
|
||||
|
||||
nonisolated var id: Int {
|
||||
rawValue
|
||||
|
@ -21,15 +26,8 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable {
|
|||
[.timeline, .settings]
|
||||
}
|
||||
|
||||
static func loggedInTabs() -> [Tab] {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad ||
|
||||
UIDevice.current.userInterfaceIdiom == .mac {
|
||||
[.timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .bookmarks, .favorites, .settings]
|
||||
} else if UIDevice.current.userInterfaceIdiom == .vision {
|
||||
[.profile, .timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .settings]
|
||||
} else {
|
||||
[.timeline, .notifications, .explore, .messages, .profile]
|
||||
}
|
||||
static func visionOSTab() -> [Tab] {
|
||||
[.profile, .timeline, .notifications, .mentions, .explore, .post, .settings]
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -63,6 +61,18 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable {
|
|||
NavigationTab {
|
||||
AccountStatusesListView(mode: .favorites)
|
||||
}
|
||||
case .followedTags:
|
||||
NavigationTab {
|
||||
FollowedTagsListView()
|
||||
}
|
||||
case .lists:
|
||||
NavigationTab {
|
||||
ListsListView()
|
||||
}
|
||||
case .links:
|
||||
NavigationTab { TrendingLinksListView(cards: []) }
|
||||
case .post:
|
||||
VStack {}
|
||||
case .other:
|
||||
EmptyView()
|
||||
}
|
||||
|
@ -70,33 +80,47 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable {
|
|||
|
||||
@ViewBuilder
|
||||
var label: some View {
|
||||
if self != .other {
|
||||
Label(title, systemImage: iconName)
|
||||
}
|
||||
}
|
||||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .timeline:
|
||||
Label("tab.timeline", systemImage: iconName)
|
||||
"tab.timeline"
|
||||
case .trending:
|
||||
Label("tab.trending", systemImage: iconName)
|
||||
"tab.trending"
|
||||
case .local:
|
||||
Label("tab.local", systemImage: iconName)
|
||||
"tab.local"
|
||||
case .federated:
|
||||
Label("tab.federated", systemImage: iconName)
|
||||
"tab.federated"
|
||||
case .notifications:
|
||||
Label("tab.notifications", systemImage: iconName)
|
||||
"tab.notifications"
|
||||
case .mentions:
|
||||
Label("tab.mentions", systemImage: iconName)
|
||||
"tab.mentions"
|
||||
case .explore:
|
||||
Label("tab.explore", systemImage: iconName)
|
||||
"tab.explore"
|
||||
case .messages:
|
||||
Label("tab.messages", systemImage: iconName)
|
||||
"tab.messages"
|
||||
case .settings:
|
||||
Label("tab.settings", systemImage: iconName)
|
||||
"tab.settings"
|
||||
case .profile:
|
||||
Label("tab.profile", systemImage: iconName)
|
||||
"tab.profile"
|
||||
case .bookmarks:
|
||||
Label("accessibility.tabs.profile.picker.bookmarks", systemImage: iconName)
|
||||
"accessibility.tabs.profile.picker.bookmarks"
|
||||
case .favorites:
|
||||
Label("accessibility.tabs.profile.picker.favorites", systemImage: iconName)
|
||||
"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:
|
||||
EmptyView()
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,18 +150,73 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable {
|
|||
"bookmark"
|
||||
case .favorites:
|
||||
"star"
|
||||
case .post:
|
||||
"square.and.pencil"
|
||||
case .followedTags:
|
||||
"tag"
|
||||
case .lists:
|
||||
"list.bullet"
|
||||
case .links:
|
||||
"newspaper"
|
||||
case .other:
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
class SidebarTabs {
|
||||
struct SidedebarTab: Hashable, Codable {
|
||||
let tab: Tab
|
||||
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: .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: Tab) -> 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 = Tab.timeline
|
||||
@AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications
|
||||
|
@ -145,44 +224,44 @@ class iOSTabs {
|
|||
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.messages
|
||||
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile
|
||||
}
|
||||
|
||||
|
||||
private let storage = Storage()
|
||||
public static let shared = iOSTabs()
|
||||
|
||||
|
||||
var tabs: [Tab] {
|
||||
[firstTab, secondTab, thirdTab, fourthTab, fifthTab]
|
||||
}
|
||||
|
||||
|
||||
var firstTab: Tab {
|
||||
didSet {
|
||||
storage.firstTab = firstTab
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var secondTab: Tab {
|
||||
didSet {
|
||||
storage.secondTab = secondTab
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var thirdTab: Tab {
|
||||
didSet {
|
||||
storage.thirdTab = thirdTab
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var fourthTab: Tab {
|
||||
didSet {
|
||||
storage.fourthTab = fourthTab
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var fifthTab: Tab {
|
||||
didSet {
|
||||
storage.fifthTab = fifthTab
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private init() {
|
||||
firstTab = storage.firstTab
|
||||
secondTab = storage.secondTab
|
||||
|
|
|
@ -5,7 +5,6 @@ import Models
|
|||
import Network
|
||||
import NukeUI
|
||||
import SFSafeSymbols
|
||||
import Shimmer
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
|
@ -39,7 +38,9 @@ struct EditTagGroupView: View {
|
|||
focusedField: $focusedField
|
||||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section("add-tag-groups.edit.tags") {
|
||||
TagsInputView(
|
||||
|
@ -48,7 +49,9 @@ struct EditTagGroupView: View {
|
|||
focusedField: $focusedField
|
||||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(
|
||||
|
@ -58,22 +61,20 @@ struct EditTagGroupView: View {
|
|||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("action.cancel", action: { dismiss() })
|
||||
.toolbar {
|
||||
CancelToolbarItem()
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("action.save", action: { save() })
|
||||
.disabled(!tagGroup.isValid)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("action.save", action: { save() })
|
||||
.disabled(!tagGroup.isValid)
|
||||
.onAppear {
|
||||
focusedField = .title
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
focusedField = .title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,7 +104,7 @@ struct AddTagGroupView_Previews: PreviewProvider {
|
|||
let container = try? ModelContainer(for: TagGroup.self, configurations: ModelConfiguration())
|
||||
|
||||
// need to use `sheet` to show `symbolsSuggestionView` in preview
|
||||
return Text("parent view for EditTagGroupView")
|
||||
return Text(verbatim: "parent view for EditTagGroupView")
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
EditTagGroupView()
|
||||
.withEnvironments()
|
||||
|
|
|
@ -4,7 +4,6 @@ import Env
|
|||
import Models
|
||||
import Network
|
||||
import NukeUI
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -53,31 +52,29 @@ struct AddRemoteTimelineView: View {
|
|||
.navigationTitle("timeline.add-remote.title")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("action.cancel", action: { dismiss() })
|
||||
.toolbar {
|
||||
CancelToolbarItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: instanceName) { _, newValue in
|
||||
instanceNamePublisher.send(newValue)
|
||||
}
|
||||
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
|
||||
Task {
|
||||
let client = Client(server: newValue)
|
||||
instance = try? await client.get(endpoint: Instances.instance)
|
||||
.onChange(of: instanceName) { _, newValue in
|
||||
instanceNamePublisher.send(newValue)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isInstanceURLFieldFocused = true
|
||||
let client = InstanceSocialClient()
|
||||
Task {
|
||||
instances = await client.fetchInstances(keyword: instanceName)
|
||||
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
|
||||
Task {
|
||||
let client = Client(server: newValue)
|
||||
instance = try? await client.get(endpoint: Instances.instance)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isInstanceURLFieldFocused = true
|
||||
let client = InstanceSocialClient()
|
||||
Task {
|
||||
instances = await client.fetchInstances(keyword: instanceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ struct TimelineTab: View {
|
|||
@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
|
||||
|
||||
|
@ -41,6 +42,7 @@ struct TimelineTab: View {
|
|||
var body: some View {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
TimelineView(timeline: $timeline,
|
||||
pinnedFilters: $pinnedFilters,
|
||||
selectedTagGroup: $selectedTagGroup,
|
||||
scrollToTopSignal: $scrollToTopSignal,
|
||||
canFilterTimeline: canFilterTimeline)
|
||||
|
@ -49,7 +51,7 @@ struct TimelineTab: View {
|
|||
.toolbar {
|
||||
toolbarView
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
}
|
||||
.onAppear {
|
||||
|
@ -92,8 +94,10 @@ struct TimelineTab: View {
|
|||
lastTimelineFilter = newValue
|
||||
}
|
||||
switch newValue {
|
||||
case .tagGroup:
|
||||
break
|
||||
case let .tagGroup(title, _, _):
|
||||
if let group = tagGroups.first(where: { $0.title == title }) {
|
||||
selectedTagGroup = group
|
||||
}
|
||||
default:
|
||||
selectedTagGroup = nil
|
||||
}
|
||||
|
@ -119,96 +123,14 @@ struct TimelineTab: View {
|
|||
|
||||
@ViewBuilder
|
||||
private var timelineFilterButton: some View {
|
||||
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() ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
routerPath.presentedSheet = .listCreate
|
||||
} label: {
|
||||
Label("account.list.create", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(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")
|
||||
}
|
||||
}
|
||||
|
||||
Menu("timeline.filter.tag-groups") {
|
||||
ForEach(tagGroups) { group in
|
||||
Button {
|
||||
selectedTagGroup = group
|
||||
timeline = .tagGroup(title: group.title, tags: group.tags)
|
||||
} 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")
|
||||
}
|
||||
}
|
||||
headerGroup
|
||||
timelineFiltersButtons
|
||||
listsFiltersButons
|
||||
tagsFiltersButtons
|
||||
localTimelinesFiltersButtons
|
||||
tagGroupsFiltersButtons
|
||||
Divider()
|
||||
contentFilterButton
|
||||
}
|
||||
|
||||
private var addAccountButton: some View {
|
||||
|
@ -228,17 +150,7 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
if client.isAuth {
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routerPath: routerPath)
|
||||
.id(currentAccount.account?.id)
|
||||
}
|
||||
}
|
||||
statusEditorToolbarItem(routerPath: routerPath,
|
||||
visibility: preferences.postVisibility)
|
||||
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn {
|
||||
SecondaryColumnToolbarItem()
|
||||
}
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
} else {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
addAccountButton
|
||||
|
@ -274,6 +186,141 @@ 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 {
|
||||
pinnedFilters.remove(at: index)
|
||||
} else {
|
||||
pinnedFilters.append(timeline)
|
||||
}
|
||||
}
|
||||
} 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
|
||||
|
|
48
IceCubesApp/App/Tabs/ToolbarTab.swift
Normal file
|
@ -0,0 +1,48 @@
|
|||
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
|
||||
|
||||
@Binding var routerPath: RouterPath
|
||||
|
||||
var body: some ToolbarContent {
|
||||
if !isSecondaryColumn {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .regular {
|
||||
if (!isSecondaryColumn && !userPreferences.showiPadSecondaryColumn) || isSecondaryColumn {
|
||||
SecondaryColumnToolbarItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 16 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/1024.png
Executable file → Normal file
Before Width: | Height: | Size: 846 KiB After Width: | Height: | Size: 991 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 6 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -1,211 +1,67 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "57.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "114.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "50.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "50x50"
|
||||
},
|
||||
{
|
||||
"filename" : "100.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "50x50"
|
||||
},
|
||||
{
|
||||
"filename" : "72.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "72x72"
|
||||
},
|
||||
{
|
||||
"filename" : "144.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "72x72"
|
||||
},
|
||||
{
|
||||
"filename" : "76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-16.png",
|
||||
"filename" : "macos16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-32.png",
|
||||
"filename" : "macos32 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-32 1.png",
|
||||
"filename" : "macos32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-64.png",
|
||||
"filename" : "macos64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-128.png",
|
||||
"filename" : "macos128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-256 1.png",
|
||||
"filename" : "mac256 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-256.png",
|
||||
"filename" : "mac256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-512 1.png",
|
||||
"filename" : "macOS512 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-512.png",
|
||||
"filename" : "macOS512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-1024.png",
|
||||
"filename" : "macOS1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
|
|
Before Width: | Height: | Size: 749 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 653 B |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 201 KiB |
Before Width: | Height: | Size: 201 KiB |
Before Width: | Height: | Size: 5.2 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/mac256 1.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/mac256.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/macOS1024.png
Normal file
After Width: | Height: | Size: 973 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/macOS512 1.png
Normal file
After Width: | Height: | Size: 262 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/macOS512.png
Normal file
After Width: | Height: | Size: 262 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/macos128.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/macos16.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/macos32 1.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/macos32.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/macos64.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 764 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"filename" : "Background.png",
|
||||
"idiom" : "vision",
|
||||
"scale" : "2x"
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
{
|
||||
"filename" : "Front.solidimagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Mid.solidimagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Back.solidimagestacklayer"
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 846 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"filename" : "Layer 1.png",
|
||||
"idiom" : "vision",
|
||||
"scale" : "2x"
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 549 KiB |
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "vision",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 846 KiB After Width: | Height: | Size: 991 KiB |
Before Width: | Height: | Size: 846 KiB After Width: | Height: | Size: 846 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon.png",
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
|
Before Width: | Height: | Size: 615 KiB |
After Width: | Height: | Size: 565 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_4b25c3a5-7ae2-401b-9b2f-17c9832e175a.png",
|
||||
"filename" : "AppIconAlternate10.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Dimillian_Ice_Cubes_in_a_glass_neon_style_glyph_retro_7ca60288-3e96-43a3-a88b-f4c882ac2183.png",
|
||||
"filename" : "icon15.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
|