mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-05-17 03:52:42 +00:00
Compare commits
179 commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
35
.github/workflows/validate_translations.yml
vendored
Normal file
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"
|
|
@ -26,8 +26,6 @@
|
|||
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F24EEB729360C330042359D /* Preview Assets.xcassets */; };
|
||||
9F295540292B6C3400E0E81B /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F29553F292B6C3400E0E81B /* Timeline */; };
|
||||
9F2A540729699698009B2D7C /* SupportAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A540629699698009B2D7C /* SupportAppView.swift */; };
|
||||
9F2A540A29699705009B2D7C /* ReceiptParser in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A540929699705009B2D7C /* ReceiptParser */; };
|
||||
9F2A540C29699705009B2D7C /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9F2A540B29699705009B2D7C /* RevenueCat */; };
|
||||
9F2A540E2969A0B0009B2D7C /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F2A540D2969A0B0009B2D7C /* StoreKit.framework */; };
|
||||
9F2A5411296A1429009B2D7C /* PushNotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A5410296A1429009B2D7C /* PushNotificationsView.swift */; };
|
||||
9F2A5419296AB631009B2D7C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A5418296AB631009B2D7C /* NotificationService.swift */; };
|
||||
|
@ -44,6 +42,11 @@
|
|||
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; };
|
||||
9F35DB4A29506FA100B3281A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB4929506FA100B3281A /* Notifications */; };
|
||||
9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4B2952005C00B3281A /* MessagesTab.swift */; };
|
||||
9F37BDDB2BE36E22007F28AD /* PostIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */; };
|
||||
9F37BDDD2BE37193007F28AD /* AppIntentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDC2BE37193007F28AD /* AppIntentService.swift */; };
|
||||
9F37BDDF2BE37C35007F28AD /* TabIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDE2BE37C35007F28AD /* TabIntent.swift */; };
|
||||
9F37BDE12BE38646007F28AD /* PostImageIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDE02BE38646007F28AD /* PostImageIntent.swift */; };
|
||||
9F37BDE32BE393A7007F28AD /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */; };
|
||||
9F38A7332ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; };
|
||||
9F38A7342ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; };
|
||||
9F38A7352ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; };
|
||||
|
@ -54,6 +57,9 @@
|
|||
9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4A48182976B21900A1A038 /* ProfileTab.swift */; };
|
||||
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F55C68C2955968700F94077 /* ExploreTab.swift */; };
|
||||
9F55C6902955993C00F94077 /* Explore in Frameworks */ = {isa = PBXBuildFile; productRef = 9F55C68F2955993C00F94077 /* Explore */; };
|
||||
9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6252BF48FE10074387E /* ListEntity.swift */; };
|
||||
9F5BE6282BF492D10074387E /* ListsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */; };
|
||||
9F5BE6292BF492D40074387E /* ListsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6212BF48FBA0074387E /* ListsWidget.swift */; };
|
||||
9F5E581929545BE700A53960 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F5E581829545BE700A53960 /* Env */; };
|
||||
9F6028562B3F36AE00476078 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6028552B3F36AE00476078 /* AppView.swift */; };
|
||||
9F6028582B3F3B7600476078 /* ToolbarTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6028572B3F3B7600476078 /* ToolbarTab.swift */; };
|
||||
|
@ -63,10 +69,27 @@
|
|||
9F7335EF29674F7100AFF0BA /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7335EE29674F7100AFF0BA /* QuickLook.framework */; };
|
||||
9F7335F22967608F00AFF0BA /* AddRemoteTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */; };
|
||||
9F7335F92968576500AFF0BA /* DisplaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */; };
|
||||
9F7788C02BE63935004E6BEF /* InlinePostIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788BF2BE63935004E6BEF /* InlinePostIntent.swift */; };
|
||||
9F7788C72BE652B1004E6BEF /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7788C62BE652B1004E6BEF /* WidgetKit.framework */; };
|
||||
9F7788C92BE652B1004E6BEF /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7788C82BE652B1004E6BEF /* SwiftUI.framework */; };
|
||||
9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */; };
|
||||
9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */; };
|
||||
9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */; };
|
||||
9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F7788D12BE652B2004E6BEF /* Assets.xcassets */; };
|
||||
9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
9F7788DE2BE6543D004E6BEF /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DD2BE6543D004E6BEF /* Account */; };
|
||||
9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DF2BE6543D004E6BEF /* AppAccount */; };
|
||||
9F7788E22BE6543D004E6BEF /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E12BE6543D004E6BEF /* Env */; };
|
||||
9F7788E42BE6543D004E6BEF /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E32BE6543D004E6BEF /* Models */; };
|
||||
9F7788E62BE6543D004E6BEF /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E52BE6543D004E6BEF /* Network */; };
|
||||
9F7788E82BE65533004E6BEF /* AppAccountEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */; };
|
||||
9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */; };
|
||||
9F7788ED2BE78D75004E6BEF /* TimelineFilterEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */; };
|
||||
9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */; };
|
||||
9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788EF2BE78E77004E6BEF /* Timeline */; };
|
||||
9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; };
|
||||
9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */; };
|
||||
9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */; };
|
||||
9FAD85832971BF7200496AB1 /* Secret.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9FAD85822971BF7200496AB1 /* Secret.plist */; };
|
||||
9FAD858B29743F7400496AB1 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD858A29743F7400496AB1 /* ShareViewController.swift */; };
|
||||
9FAD858E29743F7400496AB1 /* (null) in Resources */ = {isa = PBXBuildFile; };
|
||||
9FAD859229743F7400496AB1 /* IceCubesShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -98,6 +121,13 @@
|
|||
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE3DB56296FEFCA00628CB0 /* AppAccount */; };
|
||||
9FE4CCAB2B4C848A00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = 9FE4CCAA2B4C848A00DA5F13 /* GiphyUISDK */; };
|
||||
9FE4CCAD2B4C849F00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE4CCAC2B4C849F00DA5F13 /* GiphyUISDK */; };
|
||||
9FE6A42E2BD043A90055D388 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE6A42D2BD043A90055D388 /* RevenueCat */; };
|
||||
9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */; };
|
||||
9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */; };
|
||||
9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */; };
|
||||
9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB682BE7F842001560CE /* SharedUtils.swift */; };
|
||||
9FF2FB702BE8AE9D001560CE /* MentionWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */; };
|
||||
9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */; };
|
||||
9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; };
|
||||
9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; };
|
||||
9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; };
|
||||
|
@ -123,6 +153,13 @@
|
|||
remoteGlobalIDString = 9F2A5415296AB631009B2D7C;
|
||||
remoteInfo = IceCubesNotifications;
|
||||
};
|
||||
9F7788D42BE652B2004E6BEF /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 9FBFE631292A715500C250E9 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 9F7788C42BE652B1004E6BEF;
|
||||
remoteInfo = IceCubesAppWidgetsExtensionExtension;
|
||||
};
|
||||
9FAD859029743F7400496AB1 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 9FBFE631292A715500C250E9 /* Project object */;
|
||||
|
@ -149,6 +186,7 @@
|
|||
E9DF420729830FEC0003AAD2 /* IceCubesActionExtension.appex in Embed Foundation Extensions */,
|
||||
9F2A541D296AB631009B2D7C /* IceCubesNotifications.appex in Embed Foundation Extensions */,
|
||||
9FAD859229743F7400496AB1 /* IceCubesShareExtension.appex in Embed Foundation Extensions */,
|
||||
9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -197,6 +235,11 @@
|
|||
9F35DB4629506F6600B3281A /* NotificationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTab.swift; sourceTree = "<group>"; };
|
||||
9F35DB4829506F7F00B3281A /* Notifications */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Notifications; path = Packages/Notifications; sourceTree = "<group>"; };
|
||||
9F35DB4B2952005C00B3281A /* MessagesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTab.swift; sourceTree = "<group>"; };
|
||||
9F37BDDA2BE36E22007F28AD /* PostIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostIntent.swift; sourceTree = "<group>"; };
|
||||
9F37BDDC2BE37193007F28AD /* AppIntentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentService.swift; sourceTree = "<group>"; };
|
||||
9F37BDDE2BE37C35007F28AD /* TabIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabIntent.swift; sourceTree = "<group>"; };
|
||||
9F37BDE02BE38646007F28AD /* PostImageIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImageIntent.swift; sourceTree = "<group>"; };
|
||||
9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = "<group>"; };
|
||||
9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = "<group>"; };
|
||||
9F398AA52935FE8A00A889F2 /* AppRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRegistry.swift; sourceTree = "<group>"; };
|
||||
|
@ -205,6 +248,9 @@
|
|||
9F4A48182976B21900A1A038 /* ProfileTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTab.swift; sourceTree = "<group>"; };
|
||||
9F55C68C2955968700F94077 /* ExploreTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreTab.swift; sourceTree = "<group>"; };
|
||||
9F55C68E295598F900F94077 /* Explore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Explore; path = Packages/Explore; sourceTree = "<group>"; };
|
||||
9F5BE6212BF48FBA0074387E /* ListsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsWidget.swift; sourceTree = "<group>"; };
|
||||
9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsWidgetConfiguration.swift; sourceTree = "<group>"; };
|
||||
9F5BE6252BF48FE10074387E /* ListEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListEntity.swift; sourceTree = "<group>"; };
|
||||
9F5E581729545B5500A53960 /* Env */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Env; path = Packages/Env; sourceTree = "<group>"; };
|
||||
9F6028552B3F36AE00476078 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
|
||||
9F6028572B3F3B7600476078 /* ToolbarTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarTab.swift; sourceTree = "<group>"; };
|
||||
|
@ -215,10 +261,21 @@
|
|||
9F7335EE29674F7100AFF0BA /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.2.sdk/System/Library/Frameworks/QuickLook.framework; sourceTree = DEVELOPER_DIR; };
|
||||
9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRemoteTimelineView.swift; sourceTree = "<group>"; };
|
||||
9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaySettingsView.swift; sourceTree = "<group>"; };
|
||||
9F7788BF2BE63935004E6BEF /* InlinePostIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinePostIntent.swift; sourceTree = "<group>"; };
|
||||
9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesAppWidgetsExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9F7788C62BE652B1004E6BEF /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
9F7788C82BE652B1004E6BEF /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesAppWidgetsExtensionBundle.swift; sourceTree = "<group>"; };
|
||||
9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidget.swift; sourceTree = "<group>"; };
|
||||
9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidgetConfiguration.swift; sourceTree = "<group>"; };
|
||||
9F7788D12BE652B2004E6BEF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
9F7788D32BE652B2004E6BEF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesAppWidgetsExtensionExtension.entitlements; sourceTree = "<group>"; };
|
||||
9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountEntity.swift; sourceTree = "<group>"; };
|
||||
9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilterEntity.swift; sourceTree = "<group>"; };
|
||||
9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "IceCubesApp-release.xcconfig"; sourceTree = "<group>"; };
|
||||
9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingView.swift; sourceTree = "<group>"; };
|
||||
9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
|
||||
9FAD85822971BF7200496AB1 /* Secret.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secret.plist; sourceTree = "<group>"; };
|
||||
9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9FAD858A29743F7400496AB1 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
9FAD858F29743F7400496AB1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -243,6 +300,12 @@
|
|||
9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
|
||||
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
|
||||
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
|
||||
9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidgetConfiguration.swift; sourceTree = "<group>"; };
|
||||
9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidget.swift; sourceTree = "<group>"; };
|
||||
9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsWidgetView.swift; sourceTree = "<group>"; };
|
||||
9FF2FB682BE7F842001560CE /* SharedUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUtils.swift; sourceTree = "<group>"; };
|
||||
9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionWidget.swift; sourceTree = "<group>"; };
|
||||
9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionWidgetConfiguration.swift; sourceTree = "<group>"; };
|
||||
B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
C4CBB90B298A0DA3007E1707 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
C4FBCF6F298FD88A0015DF22 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
|
@ -278,6 +341,21 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9F7788C22BE652B1004E6BEF /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9F7788E42BE6543D004E6BEF /* Models in Frameworks */,
|
||||
9F7788E62BE6543D004E6BEF /* Network in Frameworks */,
|
||||
9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */,
|
||||
9F7788DE2BE6543D004E6BEF /* Account in Frameworks */,
|
||||
9F7788E22BE6543D004E6BEF /* Env in Frameworks */,
|
||||
9F7788C92BE652B1004E6BEF /* SwiftUI.framework in Frameworks */,
|
||||
9F7788C72BE652B1004E6BEF /* WidgetKit.framework in Frameworks */,
|
||||
9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9FAD858529743F7400496AB1 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -300,7 +378,6 @@
|
|||
9F7335EF29674F7100AFF0BA /* QuickLook.framework in Frameworks */,
|
||||
9FE4CCAB2B4C848A00DA5F13 /* GiphyUISDK in Frameworks */,
|
||||
9F7335ED2967463400AFF0BA /* AVKit.framework in Frameworks */,
|
||||
9F2A540C29699705009B2D7C /* RevenueCat in Frameworks */,
|
||||
9F2A540E2969A0B0009B2D7C /* StoreKit.framework in Frameworks */,
|
||||
9F55C6902955993C00F94077 /* Explore in Frameworks */,
|
||||
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */,
|
||||
|
@ -311,8 +388,8 @@
|
|||
9FD542E72962D2FF0045321A /* Lists in Frameworks */,
|
||||
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
|
||||
9F5E581929545BE700A53960 /* Env in Frameworks */,
|
||||
9F2A540A29699705009B2D7C /* ReceiptParser in Frameworks */,
|
||||
DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */,
|
||||
9FE6A42E2BD043A90055D388 /* RevenueCat in Frameworks */,
|
||||
9F295540292B6C3400E0E81B /* Timeline in Frameworks */,
|
||||
9F35DB4A29506FA100B3281A /* Notifications in Frameworks */,
|
||||
9FC2A38B2B49D19A00DFD1C1 /* StatusKit in Frameworks */,
|
||||
|
@ -351,6 +428,22 @@
|
|||
path = IceCubesNotifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9F37BDD92BE36E08007F28AD /* IceCubesAppIntents */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */,
|
||||
9F37BDDC2BE37193007F28AD /* AppIntentService.swift */,
|
||||
9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */,
|
||||
9F7788BF2BE63935004E6BEF /* InlinePostIntent.swift */,
|
||||
9F37BDE02BE38646007F28AD /* PostImageIntent.swift */,
|
||||
9F37BDDA2BE36E22007F28AD /* PostIntent.swift */,
|
||||
9F37BDDE2BE37C35007F28AD /* TabIntent.swift */,
|
||||
9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */,
|
||||
9F5BE6252BF48FE10074387E /* ListEntity.swift */,
|
||||
);
|
||||
path = IceCubesAppIntents;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9F398AB429360A5800A889F2 /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -375,6 +468,15 @@
|
|||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9F5BE6202BF48FB20074387E /* ListsWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F5BE6212BF48FBA0074387E /* ListsWidget.swift */,
|
||||
9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */,
|
||||
);
|
||||
path = ListsWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9F654BF0299AC46200D27FA5 /* Report */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -392,6 +494,22 @@
|
|||
path = Timeline;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F5BE6202BF48FB20074387E /* ListsWidget */,
|
||||
9FF2FB6B2BE8AE78001560CE /* MentionWidget */,
|
||||
9FF2FB642BE7F7FA001560CE /* Shared */,
|
||||
9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */,
|
||||
9FF2FB5C2BE7F549001560CE /* LatestPostsWidget */,
|
||||
9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */,
|
||||
9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */,
|
||||
9F7788D12BE652B2004E6BEF /* Assets.xcassets */,
|
||||
9F7788D32BE652B2004E6BEF /* Info.plist */,
|
||||
);
|
||||
path = IceCubesAppWidgetsExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9FA0D2AC29921C1F008A143B /* Embeds */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -456,9 +574,11 @@
|
|||
DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */,
|
||||
9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */,
|
||||
9FBFE63B292A715500C250E9 /* IceCubesApp */,
|
||||
9F37BDD92BE36E08007F28AD /* IceCubesAppIntents */,
|
||||
E9DF41FD29830FEC0003AAD2 /* IceCubesActionExtension */,
|
||||
9F2A5417296AB631009B2D7C /* IceCubesNotifications */,
|
||||
9FAD858929743F7400496AB1 /* IceCubesShareExtension */,
|
||||
9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */,
|
||||
9FBFE63A292A715500C250E9 /* Products */,
|
||||
9FBFE64C292A72BD00C250E9 /* Frameworks */,
|
||||
9FE3DB55296FEF5800628CB0 /* AppAccount */,
|
||||
|
@ -484,6 +604,7 @@
|
|||
9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */,
|
||||
9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */,
|
||||
E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */,
|
||||
9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
@ -496,7 +617,6 @@
|
|||
9FAE4AC8293774FF00772766 /* Info.plist */,
|
||||
9F398AB429360A5800A889F2 /* App */,
|
||||
9F398AB529360A6100A889F2 /* Resources */,
|
||||
9FAD85822971BF7200496AB1 /* Secret.plist */,
|
||||
);
|
||||
path = IceCubesApp;
|
||||
sourceTree = "<group>";
|
||||
|
@ -509,6 +629,8 @@
|
|||
9F7335EE29674F7100AFF0BA /* QuickLook.framework */,
|
||||
9F7335EB2967461B00AFF0BA /* AVKit.framework */,
|
||||
E9DF41FB29830FEC0003AAD2 /* UniformTypeIdentifiers.framework */,
|
||||
9F7788C62BE652B1004E6BEF /* WidgetKit.framework */,
|
||||
9F7788C82BE652B1004E6BEF /* SwiftUI.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -538,6 +660,42 @@
|
|||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9FF2FB5C2BE7F549001560CE /* LatestPostsWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */,
|
||||
9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */,
|
||||
);
|
||||
path = LatestPostsWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */,
|
||||
9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */,
|
||||
);
|
||||
path = HashtagPostsWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9FF2FB642BE7F7FA001560CE /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */,
|
||||
9FF2FB682BE7F842001560CE /* SharedUtils.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9FF2FB6B2BE8AE78001560CE /* MentionWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */,
|
||||
9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */,
|
||||
);
|
||||
path = MentionWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E9B576C029743F2A00BCE646 /* Localization */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -589,6 +747,31 @@
|
|||
productReference = 9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 9F7788D82BE652B2004E6BEF /* Build configuration list for PBXNativeTarget "IceCubesAppWidgetsExtensionExtension" */;
|
||||
buildPhases = (
|
||||
9F7788C12BE652B1004E6BEF /* Sources */,
|
||||
9F7788C22BE652B1004E6BEF /* Frameworks */,
|
||||
9F7788C32BE652B1004E6BEF /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = IceCubesAppWidgetsExtensionExtension;
|
||||
packageProductDependencies = (
|
||||
9F7788DD2BE6543D004E6BEF /* Account */,
|
||||
9F7788DF2BE6543D004E6BEF /* AppAccount */,
|
||||
9F7788E12BE6543D004E6BEF /* Env */,
|
||||
9F7788E32BE6543D004E6BEF /* Models */,
|
||||
9F7788E52BE6543D004E6BEF /* Network */,
|
||||
9F7788EF2BE78E77004E6BEF /* Timeline */,
|
||||
);
|
||||
productName = IceCubesAppWidgetsExtensionExtension;
|
||||
productReference = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
9FAD858729743F7400496AB1 /* IceCubesShareExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 9FAD859329743F7400496AB1 /* Build configuration list for PBXNativeTarget "IceCubesShareExtension" */;
|
||||
|
@ -631,6 +814,7 @@
|
|||
9F2A541C296AB631009B2D7C /* PBXTargetDependency */,
|
||||
9FAD859129743F7400496AB1 /* PBXTargetDependency */,
|
||||
E9DF420629830FEC0003AAD2 /* PBXTargetDependency */,
|
||||
9F7788D52BE652B2004E6BEF /* PBXTargetDependency */,
|
||||
);
|
||||
name = IceCubesApp;
|
||||
packageProductDependencies = (
|
||||
|
@ -644,12 +828,11 @@
|
|||
9F55C68F2955993C00F94077 /* Explore */,
|
||||
9FD542E62962D2FF0045321A /* Lists */,
|
||||
9F7335E92966B3F800AFF0BA /* Conversations */,
|
||||
9F2A540929699705009B2D7C /* ReceiptParser */,
|
||||
9F2A540B29699705009B2D7C /* RevenueCat */,
|
||||
9FE3DB56296FEFCA00628CB0 /* AppAccount */,
|
||||
DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */,
|
||||
9FC2A38A2B49D19A00DFD1C1 /* StatusKit */,
|
||||
9FE4CCAA2B4C848A00DA5F13 /* GiphyUISDK */,
|
||||
9FE6A42D2BD043A90055D388 /* RevenueCat */,
|
||||
);
|
||||
productName = IceCubesApp;
|
||||
productReference = 9FBFE639292A715500C250E9 /* Ice Cubes.app */;
|
||||
|
@ -683,12 +866,15 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1420;
|
||||
LastSwiftUpdateCheck = 1530;
|
||||
LastUpgradeCheck = 1500;
|
||||
TargetAttributes = {
|
||||
9F2A5415296AB631009B2D7C = {
|
||||
CreatedOnToolsVersion = 14.2;
|
||||
};
|
||||
9F7788C42BE652B1004E6BEF = {
|
||||
CreatedOnToolsVersion = 15.3;
|
||||
};
|
||||
9FAD858729743F7400496AB1 = {
|
||||
CreatedOnToolsVersion = 14.2;
|
||||
};
|
||||
|
@ -724,13 +910,14 @@
|
|||
be,
|
||||
uk,
|
||||
"zh-Hant",
|
||||
Base,
|
||||
);
|
||||
mainGroup = 9FBFE630292A715500C250E9;
|
||||
packageReferences = (
|
||||
9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */,
|
||||
9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */,
|
||||
DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||
9FE4CCA92B4C848A00DA5F13 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */,
|
||||
9FE6A42C2BD043A80055D388 /* XCRemoteSwiftPackageReference "purchases-ios" */,
|
||||
);
|
||||
productRefGroup = 9FBFE63A292A715500C250E9 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -740,6 +927,7 @@
|
|||
E9DF41F929830FEC0003AAD2 /* IceCubesActionExtension */,
|
||||
9F2A5415296AB631009B2D7C /* IceCubesNotifications */,
|
||||
9FAD858729743F7400496AB1 /* IceCubesShareExtension */,
|
||||
9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
@ -755,6 +943,14 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9F7788C32BE652B1004E6BEF /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9FAD858629743F7400496AB1 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -778,7 +974,6 @@
|
|||
9F18801829AE477F00D85459 /* favorite.wav in Resources */,
|
||||
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */,
|
||||
069709A8298C87B5006E4CB5 /* OpenDyslexic-Regular.otf in Resources */,
|
||||
9FAD85832971BF7200496AB1 /* Secret.plist in Resources */,
|
||||
9F18801229AE477F00D85459 /* tabSelection.wav in Resources */,
|
||||
9F18801429AE477F00D85459 /* bookmark.wav in Resources */,
|
||||
9F18801629AE477F00D85459 /* refresh.wav in Resources */,
|
||||
|
@ -809,6 +1004,27 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9F7788C12BE652B1004E6BEF /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */,
|
||||
9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */,
|
||||
9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */,
|
||||
9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */,
|
||||
9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */,
|
||||
9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */,
|
||||
9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */,
|
||||
9F5BE6292BF492D40074387E /* ListsWidget.swift in Sources */,
|
||||
9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */,
|
||||
9FF2FB702BE8AE9D001560CE /* MentionWidgetConfiguration.swift in Sources */,
|
||||
9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */,
|
||||
9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */,
|
||||
9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */,
|
||||
9F5BE6282BF492D10074387E /* ListsWidgetConfiguration.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9FAD858429743F7400496AB1 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -830,6 +1046,11 @@
|
|||
9F15D6042B3DC2180008C220 /* NavigationSheet.swift in Sources */,
|
||||
9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */,
|
||||
9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */,
|
||||
9F37BDDB2BE36E22007F28AD /* PostIntent.swift in Sources */,
|
||||
9F37BDDD2BE37193007F28AD /* AppIntentService.swift in Sources */,
|
||||
9F37BDE12BE38646007F28AD /* PostImageIntent.swift in Sources */,
|
||||
9F37BDDF2BE37C35007F28AD /* TabIntent.swift in Sources */,
|
||||
9F7788C02BE63935004E6BEF /* InlinePostIntent.swift in Sources */,
|
||||
9FAD85CF2975B68900496AB1 /* SideBarView.swift in Sources */,
|
||||
9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */,
|
||||
9FC14EF42B494D940006CEE1 /* RemoteTimelinesSettingView.swift in Sources */,
|
||||
|
@ -848,9 +1069,12 @@
|
|||
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */,
|
||||
639CDF9C296AC82F00C35E58 /* SafariRouter.swift in Sources */,
|
||||
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */,
|
||||
9F7788ED2BE78D75004E6BEF /* TimelineFilterEntity.swift in Sources */,
|
||||
9F37BDE32BE393A7007F28AD /* AppShortcuts.swift in Sources */,
|
||||
9F654BEF299AC45B00D27FA5 /* ReportView.swift in Sources */,
|
||||
D08A9C3529956CFA00204A4A /* SwipeActionsSettingsView.swift in Sources */,
|
||||
9F7335F22967608F00AFF0BA /* AddRemoteTimelineView.swift in Sources */,
|
||||
9F7788E82BE65533004E6BEF /* AppAccountEntity.swift in Sources */,
|
||||
9FC14EF62B494DFF0006CEE1 /* RecenTagsSettingView.swift in Sources */,
|
||||
9F6028562B3F36AE00476078 /* AppView.swift in Sources */,
|
||||
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,
|
||||
|
@ -877,6 +1101,15 @@
|
|||
target = 9F2A5415296AB631009B2D7C /* IceCubesNotifications */;
|
||||
targetProxy = 9F2A541B296AB631009B2D7C /* PBXContainerItemProxy */;
|
||||
};
|
||||
9F7788D52BE652B2004E6BEF /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
platformFilters = (
|
||||
ios,
|
||||
maccatalyst,
|
||||
);
|
||||
target = 9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */;
|
||||
targetProxy = 9F7788D42BE652B2004E6BEF /* PBXContainerItemProxy */;
|
||||
};
|
||||
9FAD859129743F7400496AB1 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
platformFilters = (
|
||||
|
@ -940,7 +1173,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.25;
|
||||
MARKETING_VERSION = 1.10.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -975,7 +1208,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.25;
|
||||
MARKETING_VERSION = 1.10.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -991,6 +1224,78 @@
|
|||
};
|
||||
name = Release;
|
||||
};
|
||||
9F7788D92BE652B2004E6BEF /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
9F7788DA2BE652B2004E6BEF /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
9FAD859429743F7400496AB1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
|
@ -1011,7 +1316,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.25;
|
||||
MARKETING_VERSION = 1.10.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1045,7 +1350,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.25;
|
||||
MARKETING_VERSION = 1.10.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1193,6 +1498,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = IceCubesApp/App/IceCubesApp.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
|
@ -1208,7 +1514,8 @@
|
|||
INFOPLIST_FILE = IceCubesApp/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Upload photos & videos to Mastodon";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Upload photos & videos to attach to your Mastodon posts.";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2024 Thomas Ricouard";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Upload photos & videos to Mastodon";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
|
@ -1225,7 +1532,7 @@
|
|||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.10.25;
|
||||
MARKETING_VERSION = 1.10.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||
PRODUCT_NAME = "Ice Cubes";
|
||||
SDKROOT = auto;
|
||||
|
@ -1247,6 +1554,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = "IceCubesApp/App/IceCubesApp-release.entitlements";
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
|
@ -1262,7 +1570,8 @@
|
|||
INFOPLIST_FILE = IceCubesApp/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Upload photos & videos to Mastodon";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Upload photos & videos to attach to your Mastodon posts.";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2024 Thomas Ricouard";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Upload photos & videos to Mastodon";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
|
@ -1279,7 +1588,7 @@
|
|||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.10.25;
|
||||
MARKETING_VERSION = 1.10.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||
PRODUCT_NAME = "Ice Cubes";
|
||||
SDKROOT = auto;
|
||||
|
@ -1314,7 +1623,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.25;
|
||||
MARKETING_VERSION = 1.10.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1349,7 +1658,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.25;
|
||||
MARKETING_VERSION = 1.10.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1376,6 +1685,15 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
9F7788D82BE652B2004E6BEF /* Build configuration list for PBXNativeTarget "IceCubesAppWidgetsExtensionExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
9F7788D92BE652B2004E6BEF /* Debug */,
|
||||
9F7788DA2BE652B2004E6BEF /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
9FAD859329743F7400496AB1 /* Build configuration list for PBXNativeTarget "IceCubesShareExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
@ -1415,14 +1733,6 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/RevenueCat/purchases-ios.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.0.0;
|
||||
};
|
||||
};
|
||||
9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/evgenyneu/keychain-swift";
|
||||
|
@ -1436,7 +1746,15 @@
|
|||
repositoryURL = "https://github.com/Giphy/giphy-ios-sdk";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.2.7;
|
||||
minimumVersion = 2.2.8;
|
||||
};
|
||||
};
|
||||
9FE6A42C2BD043A80055D388 /* XCRemoteSwiftPackageReference "purchases-ios" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/RevenueCat/purchases-ios";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.40.1;
|
||||
};
|
||||
};
|
||||
DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||
|
@ -1454,16 +1772,6 @@
|
|||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Timeline;
|
||||
};
|
||||
9F2A540929699705009B2D7C /* ReceiptParser */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */;
|
||||
productName = ReceiptParser;
|
||||
};
|
||||
9F2A540B29699705009B2D7C /* RevenueCat */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */;
|
||||
productName = RevenueCat;
|
||||
};
|
||||
9F2A5423296AB67A009B2D7C /* Env */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Env;
|
||||
|
@ -1501,6 +1809,30 @@
|
|||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Conversations;
|
||||
};
|
||||
9F7788DD2BE6543D004E6BEF /* Account */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Account;
|
||||
};
|
||||
9F7788DF2BE6543D004E6BEF /* AppAccount */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = AppAccount;
|
||||
};
|
||||
9F7788E12BE6543D004E6BEF /* Env */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Env;
|
||||
};
|
||||
9F7788E32BE6543D004E6BEF /* Models */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Models;
|
||||
};
|
||||
9F7788E52BE6543D004E6BEF /* Network */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Network;
|
||||
};
|
||||
9F7788EF2BE78E77004E6BEF /* Timeline */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Timeline;
|
||||
};
|
||||
9F7D93932980063100EE6B7A /* AppAccount */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = AppAccount;
|
||||
|
@ -1569,6 +1901,11 @@
|
|||
package = 9FE4CCA92B4C848A00DA5F13 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */;
|
||||
productName = GiphyUISDK;
|
||||
};
|
||||
9FE6A42D2BD043A90055D388 /* RevenueCat */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9FE6A42C2BD043A80055D388 /* XCRemoteSwiftPackageReference "purchases-ios" */;
|
||||
productName = RevenueCat;
|
||||
};
|
||||
9FFF677B299B7B2C00FE700A /* Notifications */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Notifications;
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/divadretlaw/EmojiText",
|
||||
"state" : {
|
||||
"revision" : "d0664390e3236ff6241ea0586d80f4e92702973b",
|
||||
"version" : "3.2.1"
|
||||
"revision" : "c54000aa9ccc048619054a5a2da2ce0576ffea18",
|
||||
"version" : "4.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -32,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -68,17 +68,17 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke",
|
||||
"state" : {
|
||||
"revision" : "15fde63470d782c897816a74bdd516a907e33147",
|
||||
"version" : "12.3.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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -99,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",
|
||||
|
|
|
@ -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,6 +8,7 @@ import LinkPresentation
|
|||
import Lists
|
||||
import MediaUI
|
||||
import Models
|
||||
import Notifications
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
|
@ -60,9 +61,19 @@ extension View {
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +85,13 @@ extension View {
|
|||
StatusEditor.MainView(mode: .replyTo(status: status))
|
||||
.withEnvironments()
|
||||
case let .newStatusEditor(visibility):
|
||||
StatusEditor.MainView(mode: .new(visibility: visibility))
|
||||
StatusEditor.MainView(mode: .new(text: nil, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case let .prefilledStatusEditor(text, visibility):
|
||||
StatusEditor.MainView(mode: .new(text: text, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case let .imageURL(urls, visibility):
|
||||
StatusEditor.MainView(mode: .imageURL(urls: urls, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case let .editStatusEditor(status):
|
||||
StatusEditor.MainView(mode: .edit(status: status))
|
||||
|
@ -82,6 +99,9 @@ extension View {
|
|||
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()
|
||||
|
@ -137,6 +157,12 @@ extension View {
|
|||
.presentationDetents([.medium])
|
||||
.presentationBackground(.thinMaterial)
|
||||
.withEnvironments()
|
||||
case .accountEditInfo:
|
||||
EditAccountView()
|
||||
.withEnvironments()
|
||||
case .accountFiltersList:
|
||||
FiltersListView()
|
||||
.withEnvironments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,29 +17,29 @@ struct AppView: View {
|
|||
@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
|
||||
}
|
||||
#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()
|
||||
|
@ -49,7 +49,7 @@ struct AppView: View {
|
|||
} else if UIDevice.current.userInterfaceIdiom == .vision {
|
||||
return Tab.visionOSTab()
|
||||
}
|
||||
return sidebarTabs.tabs.map{ $0.tab }
|
||||
return sidebarTabs.tabs.map { $0.tab }
|
||||
}
|
||||
|
||||
var tabBarView: some View {
|
||||
|
@ -58,9 +58,9 @@ struct AppView: View {
|
|||
}, set: { newTab in
|
||||
if newTab == .post {
|
||||
#if os(visionOS)
|
||||
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||
#else
|
||||
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ struct AppView: View {
|
|||
}
|
||||
.tag(tab)
|
||||
.badge(badgeFor(tab: tab))
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .tabBar)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .tabBar)
|
||||
}
|
||||
}
|
||||
.id(appAccountsManager.currentClient.id)
|
||||
|
@ -104,40 +104,40 @@ struct AppView: View {
|
|||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
.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)
|
||||
}
|
||||
.environment(appRouterPath)
|
||||
}
|
||||
#endif
|
||||
|
||||
var notificationsSecondaryColumn: some View {
|
||||
|
|
|
@ -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,3 +1,4 @@
|
|||
import AppIntents
|
||||
import Env
|
||||
import MediaUI
|
||||
import StatusKit
|
||||
|
@ -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,6 +49,12 @@ extension IceCubesApp {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: appIntentService.handledIntent) { _, _ in
|
||||
if let intent = appIntentService.handledIntent?.intent {
|
||||
handleIntent(intent)
|
||||
appIntentService.handledIntent = nil
|
||||
}
|
||||
}
|
||||
.withModelContainer()
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
|
@ -74,7 +82,9 @@ extension IceCubesApp {
|
|||
Group {
|
||||
switch destination.wrappedValue {
|
||||
case let .newStatusEditor(visibility):
|
||||
StatusEditor.MainView(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):
|
||||
StatusEditor.MainView(mode: .edit(status: status))
|
||||
case let .quoteStatusEditor(status):
|
||||
|
@ -83,11 +93,14 @@ extension IceCubesApp {
|
|||
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)
|
||||
|
@ -113,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,13 +23,14 @@ struct IceCubesApp: App {
|
|||
@State var currentAccount = CurrentAccount.shared
|
||||
@State var userPreferences = UserPreferences.shared
|
||||
@State var pushNotificationsService = PushNotificationsService.shared
|
||||
@State var appIntentService = AppIntentService.shared
|
||||
@State var watcher = StreamWatcher.shared
|
||||
@State var quickLook = QuickLook.shared
|
||||
@State var theme = Theme.shared
|
||||
|
||||
|
||||
@State var selectedTab: Tab = .timeline
|
||||
@State var appRouterPath = RouterPath()
|
||||
|
||||
|
||||
@State var isSupporter: Bool = false
|
||||
|
||||
var body: some Scene {
|
||||
|
@ -79,7 +80,7 @@ struct IceCubesApp: App {
|
|||
}
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(_: UIApplication,
|
||||
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
|
||||
{
|
||||
|
@ -113,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -52,78 +55,78 @@ private struct SafariRouter: ViewModifier {
|
|||
return .systemAction
|
||||
}
|
||||
#if os(visionOS)
|
||||
return .systemAction
|
||||
return .systemAction
|
||||
#else
|
||||
return safariManager.open(url)
|
||||
return safariManager.open(url)
|
||||
#endif
|
||||
#else
|
||||
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 {
|
||||
|
|
|
@ -17,12 +17,12 @@ struct SideBarView<Content: View>: View {
|
|||
@Environment(StreamWatcher.self) private var watcher
|
||||
@Environment(UserPreferences.self) private var userPreferences
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
|
||||
@Binding var selectedTab: Tab
|
||||
@Binding var popToRootTab: Tab
|
||||
var tabs: [Tab]
|
||||
@ViewBuilder var content: () -> Content
|
||||
|
||||
|
||||
@State private var sidebarTabs = SidebarTabs.shared
|
||||
|
||||
private func badgeFor(tab: Tab) -> Int {
|
||||
|
@ -35,15 +35,23 @@ 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)
|
||||
}
|
||||
}
|
||||
.frame(width: .sidebarWidth - 24, height: 50)
|
||||
.frame(width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24, height: 50)
|
||||
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear,
|
||||
in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
@ -75,6 +83,7 @@ struct SideBarView<Content: View>: View {
|
|||
.offset(x: 2, y: -2)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.help(Tab.post.title)
|
||||
}
|
||||
|
||||
private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View {
|
||||
|
@ -91,9 +100,19 @@ struct SideBarView<Content: View>: View {
|
|||
}
|
||||
} label: {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
AppAccountView(viewModel: .init(appAccount: account, isCompact: true),
|
||||
isParentPresented: .constant(false))
|
||||
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
|
||||
|
@ -101,13 +120,23 @@ 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
|
||||
if tab != .profile && sidebarTabs.isEnabled(tab) {
|
||||
|
@ -132,6 +161,7 @@ struct SideBarView<Content: View>: View {
|
|||
} label: {
|
||||
makeIconForTab(tab: tab)
|
||||
}
|
||||
.help(tab.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,18 +185,25 @@ struct SideBarView<Content: View>: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.frame(width: .sidebarWidth)
|
||||
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(.thinMaterial)
|
||||
.safeAreaInset(edge: .bottom, content: {
|
||||
HStack {
|
||||
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: .sidebarWidth)
|
||||
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
|
||||
.background(.thinMaterial)
|
||||
})
|
||||
Divider().edgesIgnoringSafeArea(.all)
|
||||
Divider().edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
content()
|
||||
}
|
||||
|
@ -196,6 +233,7 @@ private struct SideBarIcon: View {
|
|||
self.isHovered = isHovered
|
||||
}
|
||||
}
|
||||
.frame(width: 50, height: 40)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ 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 {
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ struct MessagesTab: View {
|
|||
.toolbar {
|
||||
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,27 +1,27 @@
|
|||
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()
|
||||
|
@ -32,7 +32,7 @@ struct NavigationTab<Content: View>: View {
|
|||
.toolbar {
|
||||
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 = []
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -42,7 +42,7 @@ struct NotificationsTab: View {
|
|||
}
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
}
|
||||
.onAppear {
|
||||
|
@ -60,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) {
|
||||
|
|
|
@ -23,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,27 +27,27 @@ struct AboutView: View {
|
|||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
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")
|
||||
|
@ -57,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)
|
||||
|
@ -107,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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,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,7 +263,7 @@ struct AddAccountView: View {
|
|||
.redacted(reason: .placeholder)
|
||||
.allowsHitTesting(false)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -304,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,14 +5,14 @@ import Models
|
|||
import Network
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import Timeline
|
||||
import UserNotifications
|
||||
|
||||
@MainActor
|
||||
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 {
|
||||
|
@ -41,7 +41,7 @@ struct ContentSettingsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
|
@ -59,6 +59,7 @@ struct ContentSettingsView: View {
|
|||
userPreferences.appAutoExpandMedia = userPreferences.autoExpandMedia
|
||||
userPreferences.appDefaultPostsSensitive = userPreferences.postIsSensitive
|
||||
userPreferences.appDefaultPostVisibility = userPreferences.postVisibility
|
||||
userPreferences.appRequireAltText = userPreferences.appRequireAltText
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,11 +113,15 @@ 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")
|
||||
|
@ -137,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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import Observation
|
|||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable class DisplaySettingsLocalValues {
|
||||
var tintColor = Theme.shared.tintColor
|
||||
var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
||||
|
@ -36,11 +37,11 @@ struct DisplaySettingsView: View {
|
|||
ZStack(alignment: .top) {
|
||||
Form {
|
||||
#if !os(visionOS)
|
||||
StatusRowView(viewModel: previewStatusViewModel)
|
||||
.allowsHitTesting(false)
|
||||
.opacity(0)
|
||||
.hidden()
|
||||
themeSection
|
||||
StatusRowView(viewModel: previewStatusViewModel)
|
||||
.allowsHitTesting(false)
|
||||
.opacity(0)
|
||||
.hidden()
|
||||
themeSection
|
||||
#endif
|
||||
fontSection
|
||||
layoutSection
|
||||
|
@ -49,35 +50,35 @@ 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
|
||||
}
|
||||
.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
|
||||
examplePost
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,6 @@ struct IconSelectorView: View {
|
|||
.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) Louie Mantia, Jr.", icons: []),
|
||||
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]),
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import SwiftUI
|
||||
import SwiftData
|
||||
import Models
|
||||
import Env
|
||||
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
|
||||
|
@ -35,10 +35,10 @@ struct RecenTagsSettingView: View {
|
|||
.navigationTitle("settings.general.recent-tags")
|
||||
.scrollContentBackground(.hidden)
|
||||
#if !os(visionOS)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import SwiftUI
|
||||
import SwiftData
|
||||
import Models
|
||||
import Env
|
||||
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
|
||||
|
@ -36,10 +36,10 @@ struct RemoteTimelinesSettingView: View {
|
|||
.navigationTitle("settings.general.remote-timelines")
|
||||
.scrollContentBackground(.hidden)
|
||||
#if !os(visionOS)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,9 @@ struct SettingsTabs: View {
|
|||
@Binding var popToRootTab: Tab
|
||||
|
||||
let isModal: Bool
|
||||
|
||||
|
||||
@State private var startingPoint: SettingsStartingPoint? = nil
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
Form {
|
||||
|
@ -43,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 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, !isModal {
|
||||
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
|
||||
|
|
|
@ -2,10 +2,11 @@ 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 {
|
||||
|
@ -28,11 +29,11 @@ struct SidebarEntriesSettingsView: View {
|
|||
.environment(\.editMode, .constant(.active))
|
||||
.navigationTitle("settings.general.sidebarEntries")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
func move(from source: IndexSet, to destination: Int) {
|
||||
sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination)
|
||||
}
|
||||
|
|
|
@ -69,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 {
|
||||
|
|
|
@ -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,7 +6,7 @@ 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 {
|
||||
|
@ -42,7 +42,7 @@ struct TabbarEntriesSettingsView: View {
|
|||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
|
||||
Section {
|
||||
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
|
||||
}
|
||||
|
@ -52,8 +52,8 @@ struct TabbarEntriesSettingsView: View {
|
|||
}
|
||||
.navigationTitle("settings.general.tabbarEntries")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import SwiftUI
|
||||
import SwiftData
|
||||
import Models
|
||||
import Env
|
||||
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
|
||||
|
@ -41,10 +41,10 @@ struct TagsGroupSettingView: View {
|
|||
.navigationTitle("timeline.filter.tag-groups")
|
||||
.scrollContentBackground(.hidden)
|
||||
#if !os(visionOS)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
.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,4 +1,5 @@
|
|||
import Account
|
||||
import AppIntents
|
||||
import DesignSystem
|
||||
import Explore
|
||||
import Foundation
|
||||
|
@ -15,6 +16,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
case post
|
||||
case followedTags
|
||||
case lists
|
||||
case links
|
||||
|
||||
nonisolated var id: Int {
|
||||
rawValue
|
||||
|
@ -23,9 +25,9 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
static func loggedOutTab() -> [Tab] {
|
||||
[.timeline, .settings]
|
||||
}
|
||||
|
||||
|
||||
static func visionOSTab() -> [Tab] {
|
||||
[.profile, .timeline, .notifications, .mentions, .explore, .messages, .post, .settings]
|
||||
[.profile, .timeline, .notifications, .mentions, .explore, .post, .settings]
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -67,8 +69,10 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
NavigationTab {
|
||||
ListsListView()
|
||||
}
|
||||
case .links:
|
||||
NavigationTab { TrendingLinksListView(cards: []) }
|
||||
case .post:
|
||||
VStack { }
|
||||
VStack {}
|
||||
case .other:
|
||||
EmptyView()
|
||||
}
|
||||
|
@ -76,40 +80,47 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
|
||||
@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:
|
||||
Label("menu.new-post", systemImage: iconName)
|
||||
"menu.new-post"
|
||||
case .followedTags:
|
||||
Label("timeline.filter.tags", systemImage: iconName)
|
||||
"timeline.filter.tags"
|
||||
case .lists:
|
||||
Label("timeline.filter.lists", systemImage: iconName)
|
||||
"timeline.filter.lists"
|
||||
case .links:
|
||||
"explore.section.trending.links"
|
||||
case .other:
|
||||
EmptyView()
|
||||
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,19 +156,22 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
"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),
|
||||
|
@ -172,36 +186,37 @@ class SidebarTabs {
|
|||
.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
|
||||
|
@ -209,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
|
||||
|
|
|
@ -38,7 +38,9 @@ struct EditTagGroupView: View {
|
|||
focusedField: $focusedField
|
||||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section("add-tag-groups.edit.tags") {
|
||||
TagsInputView(
|
||||
|
@ -47,7 +49,9 @@ struct EditTagGroupView: View {
|
|||
focusedField: $focusedField
|
||||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(
|
||||
|
@ -57,22 +61,20 @@ struct EditTagGroupView: View {
|
|||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,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 {
|
||||
|
@ -95,7 +95,7 @@ struct TimelineTab: View {
|
|||
}
|
||||
switch newValue {
|
||||
case let .tagGroup(title, _, _):
|
||||
if let group = tagGroups.first(where: { $0.title == title}) {
|
||||
if let group = tagGroups.first(where: { $0.title == title }) {
|
||||
selectedTagGroup = group
|
||||
}
|
||||
default:
|
||||
|
@ -123,16 +123,14 @@ struct TimelineTab: View {
|
|||
|
||||
@ViewBuilder
|
||||
private var timelineFilterButton: some View {
|
||||
latestOrResumeButtons
|
||||
contentFilterButton
|
||||
Divider()
|
||||
pinMenuButton
|
||||
Divider()
|
||||
headerGroup
|
||||
timelineFiltersButtons
|
||||
listsFiltersButons
|
||||
tagsFiltersButtons
|
||||
localTimelinesFiltersButtons
|
||||
tagGroupsFiltersButtons
|
||||
Divider()
|
||||
contentFilterButton
|
||||
}
|
||||
|
||||
private var addAccountButton: some View {
|
||||
|
@ -187,14 +185,16 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var latestOrResumeButtons: some View {
|
||||
if timeline.supportNewestPagination {
|
||||
Button {
|
||||
timeline = .latest
|
||||
} label: {
|
||||
Label(TimelineFilter.latest.localizedTitle(), systemImage: TimelineFilter.latest.iconName())
|
||||
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 {
|
||||
|
@ -206,12 +206,13 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
pinButton
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var pinMenuButton: some View {
|
||||
let index = pinnedFilters.firstIndex(where: { $0.id == timeline.id})
|
||||
private var pinButton: some View {
|
||||
let index = pinnedFilters.firstIndex(where: { $0.id == timeline.id })
|
||||
Button {
|
||||
withAnimation {
|
||||
if let index {
|
||||
|
@ -221,14 +222,14 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
} label: {
|
||||
if index != nil {
|
||||
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 {
|
||||
|
@ -238,7 +239,7 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var listsFiltersButons: some View {
|
||||
Menu("timeline.filter.lists") {
|
||||
|
@ -256,7 +257,7 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var tagsFiltersButtons: some View {
|
||||
if !currentAccount.tags.isEmpty {
|
||||
|
@ -271,7 +272,7 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var localTimelinesFiltersButtons: some View {
|
||||
Menu("timeline.filter.local") {
|
||||
ForEach(localTimelines) { remoteLocal in
|
||||
|
@ -290,7 +291,7 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var tagGroupsFiltersButtons: some View {
|
||||
Menu("timeline.filter.tag-groups") {
|
||||
ForEach(tagGroups) { group in
|
||||
|
@ -311,7 +312,7 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var contentFilterButton: some View {
|
||||
Button(action: {
|
||||
routerPath.presentedSheet = .timelineContentFilter
|
||||
|
|
|
@ -1,23 +1,39 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
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) {
|
||||
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
|
||||
{
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routerPath: routerPath)
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 991 KiB After Width: | Height: | Size: 991 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-fs8.png",
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"filename" : "AppIcon-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
|
File diff suppressed because it is too large
Load diff
43
IceCubesAppIntents/AppAccountEntity.swift
Normal file
43
IceCubesAppIntents/AppAccountEntity.swift
Normal file
|
@ -0,0 +1,43 @@
|
|||
import AppAccount
|
||||
import AppIntents
|
||||
import Env
|
||||
import Foundation
|
||||
import Models
|
||||
import Network
|
||||
|
||||
extension IntentDescription: @unchecked Sendable {}
|
||||
extension TypeDisplayRepresentation: @unchecked Sendable {}
|
||||
|
||||
public struct AppAccountEntity: Identifiable, AppEntity {
|
||||
public var id: String { account.id }
|
||||
|
||||
public let account: AppAccount
|
||||
|
||||
public static let defaultQuery = DefaultAppAccountEntityQuery()
|
||||
|
||||
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "AppAccount"
|
||||
|
||||
public var displayRepresentation: DisplayRepresentation {
|
||||
DisplayRepresentation(title: "\(account.accountName ?? account.server)")
|
||||
}
|
||||
}
|
||||
|
||||
public struct DefaultAppAccountEntityQuery: EntityQuery {
|
||||
public init() {}
|
||||
|
||||
public func entities(for identifiers: [AppAccountEntity.ID]) async throws -> [AppAccountEntity] {
|
||||
return await AppAccountsManager.shared.availableAccounts.filter { account in
|
||||
identifiers.contains { id in
|
||||
id == account.id
|
||||
}
|
||||
}.map { AppAccountEntity(account: $0) }
|
||||
}
|
||||
|
||||
public func suggestedEntities() async throws -> [AppAccountEntity] {
|
||||
await AppAccountsManager.shared.availableAccounts.map { .init(account: $0) }
|
||||
}
|
||||
|
||||
public func defaultResult() async -> AppAccountEntity? {
|
||||
await .init(account: AppAccountsManager.shared.currentAccount)
|
||||
}
|
||||
}
|
25
IceCubesAppIntents/AppIntentService.swift
Normal file
25
IceCubesAppIntents/AppIntentService.swift
Normal file
|
@ -0,0 +1,25 @@
|
|||
import AppIntents
|
||||
import SwiftUI
|
||||
|
||||
@Observable
|
||||
public class AppIntentService: @unchecked Sendable {
|
||||
struct HandledIntent: Equatable {
|
||||
static func == (lhs: AppIntentService.HandledIntent, rhs: AppIntentService.HandledIntent) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
let id: String
|
||||
let intent: any AppIntent
|
||||
|
||||
init(intent: any AppIntent) {
|
||||
id = UUID().uuidString
|
||||
self.intent = intent
|
||||
}
|
||||
}
|
||||
|
||||
public static let shared = AppIntentService()
|
||||
|
||||
var handledIntent: HandledIntent?
|
||||
|
||||
private init() {}
|
||||
}
|
42
IceCubesAppIntents/AppShortcuts.swift
Normal file
42
IceCubesAppIntents/AppShortcuts.swift
Normal file
|
@ -0,0 +1,42 @@
|
|||
import AppIntents
|
||||
|
||||
struct AppShortcuts: AppShortcutsProvider {
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
AppShortcut(
|
||||
intent: PostIntent(),
|
||||
phrases: [
|
||||
"Post \(\.$content) in \(.applicationName)",
|
||||
"Post a status on Mastodon with \(.applicationName)",
|
||||
],
|
||||
shortTitle: "Compose a post",
|
||||
systemImageName: "square.and.pencil"
|
||||
)
|
||||
AppShortcut(
|
||||
intent: InlinePostIntent(),
|
||||
phrases: [
|
||||
"Write a post with \(.applicationName)",
|
||||
"Send on post on Mastodon with \(.applicationName)",
|
||||
],
|
||||
shortTitle: "Send a post",
|
||||
systemImageName: "square.and.pencil"
|
||||
)
|
||||
AppShortcut(
|
||||
intent: TabIntent(),
|
||||
phrases: [
|
||||
"Open \(\.$tab) in \(.applicationName)",
|
||||
"Open \(.applicationName)",
|
||||
],
|
||||
shortTitle: "Open Ice Cubes",
|
||||
systemImageName: "cube"
|
||||
)
|
||||
AppShortcut(
|
||||
intent: PostImageIntent(),
|
||||
phrases: [
|
||||
"Post images \(\.$images) in \(.applicationName)",
|
||||
"Send photos \(\.$images) with \(.applicationName)",
|
||||
],
|
||||
shortTitle: "Post a status with an image",
|
||||
systemImageName: "photo"
|
||||
)
|
||||
}
|
||||
}
|
61
IceCubesAppIntents/InlinePostIntent.swift
Normal file
61
IceCubesAppIntents/InlinePostIntent.swift
Normal file
|
@ -0,0 +1,61 @@
|
|||
import AppAccount
|
||||
import AppIntents
|
||||
import Env
|
||||
import Foundation
|
||||
import Models
|
||||
import Network
|
||||
|
||||
enum PostVisibility: String, AppEnum {
|
||||
case direct, priv, unlisted, pub
|
||||
|
||||
public static var caseDisplayRepresentations: [PostVisibility: DisplayRepresentation] {
|
||||
[.direct: "Private",
|
||||
.priv: "Followers Only",
|
||||
.unlisted: "Quiet Public",
|
||||
.pub: "Public"]
|
||||
}
|
||||
|
||||
static var typeDisplayName: LocalizedStringResource { "Visibility" }
|
||||
|
||||
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "Visibility"
|
||||
|
||||
var toAppVisibility: Models.Visibility {
|
||||
switch self {
|
||||
case .direct:
|
||||
.direct
|
||||
case .priv:
|
||||
.priv
|
||||
case .unlisted:
|
||||
.unlisted
|
||||
case .pub:
|
||||
.pub
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InlinePostIntent: AppIntent {
|
||||
static let title: LocalizedStringResource = "Send post to Mastodon"
|
||||
static let description: IntentDescription = "Send a text post to Mastodon with Ice Cubes"
|
||||
static let openAppWhenRun: Bool = false
|
||||
|
||||
@Parameter(title: "Account", requestValueDialog: IntentDialog("Account"))
|
||||
var account: AppAccountEntity
|
||||
|
||||
@Parameter(title: "Post visibility", requestValueDialog: IntentDialog("Visibility of your post"))
|
||||
var visibility: PostVisibility
|
||||
|
||||
@Parameter(title: "Post content", requestValueDialog: IntentDialog("Content of the post to be sent to Mastodon"))
|
||||
var content: String
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
|
||||
let client = Client(server: account.account.server, version: .v1, oauthToken: account.account.oauthToken)
|
||||
let status = StatusData(status: content, visibility: visibility.toAppVisibility)
|
||||
do {
|
||||
let status: Status = try await client.post(endpoint: Statuses.postStatus(json: status))
|
||||
return .result(dialog: "\(status.content.asRawText) was posted on Mastodon")
|
||||
} catch {
|
||||
return .result(dialog: "An error occured while posting to Mastodon, please try again.")
|
||||
}
|
||||
}
|
||||
}
|
55
IceCubesAppIntents/ListEntity.swift
Normal file
55
IceCubesAppIntents/ListEntity.swift
Normal file
|
@ -0,0 +1,55 @@
|
|||
import AppAccount
|
||||
import AppIntents
|
||||
import Env
|
||||
import Foundation
|
||||
import Models
|
||||
import Network
|
||||
import Timeline
|
||||
|
||||
public struct ListEntity: Identifiable, AppEntity {
|
||||
public var id: String { list.id }
|
||||
|
||||
public let list: Models.List
|
||||
|
||||
public static let defaultQuery = DefaultListEntityQuery()
|
||||
|
||||
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "List"
|
||||
|
||||
public var displayRepresentation: DisplayRepresentation {
|
||||
DisplayRepresentation(title: "\(list.title)")
|
||||
}
|
||||
}
|
||||
|
||||
public struct DefaultListEntityQuery: EntityQuery {
|
||||
public init() {}
|
||||
|
||||
@IntentParameterDependency<ListsWidgetConfiguration>(
|
||||
\.$account
|
||||
)
|
||||
var account
|
||||
|
||||
public func entities(for _: [ListEntity.ID]) async throws -> [ListEntity] {
|
||||
await fetchLists().map{ .init(list: $0 )}
|
||||
}
|
||||
|
||||
public func suggestedEntities() async throws -> [ListEntity] {
|
||||
await fetchLists().map{ .init(list: $0 )}
|
||||
}
|
||||
|
||||
public func defaultResult() async -> ListEntity? {
|
||||
nil
|
||||
}
|
||||
|
||||
private func fetchLists() async -> [Models.List] {
|
||||
guard let account = account?.account.account else {
|
||||
return []
|
||||
}
|
||||
let client = Client(server: account.server, oauthToken: account.oauthToken)
|
||||
do {
|
||||
let lists: [Models.List] = try await client.get(endpoint: Lists.lists)
|
||||
return lists
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
19
IceCubesAppIntents/PostImageIntent.swift
Normal file
19
IceCubesAppIntents/PostImageIntent.swift
Normal file
|
@ -0,0 +1,19 @@
|
|||
import AppIntents
|
||||
import Foundation
|
||||
|
||||
struct PostImageIntent: AppIntent {
|
||||
static let title: LocalizedStringResource = "Post an image to Mastodon"
|
||||
static let description: IntentDescription = "Use Ice Cubes to compose a post with an image to Mastodon"
|
||||
static let openAppWhenRun: Bool = true
|
||||
|
||||
@Parameter(title: "Image",
|
||||
description: "Image to post on Mastodon",
|
||||
supportedTypeIdentifiers: ["public.image"],
|
||||
inputConnectionBehavior: .connectToPreviousIntentResult)
|
||||
var images: [IntentFile]?
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
AppIntentService.shared.handledIntent = .init(intent: self)
|
||||
return .result()
|
||||
}
|
||||
}
|
16
IceCubesAppIntents/PostIntent.swift
Normal file
16
IceCubesAppIntents/PostIntent.swift
Normal file
|
@ -0,0 +1,16 @@
|
|||
import AppIntents
|
||||
import Foundation
|
||||
|
||||
struct PostIntent: AppIntent {
|
||||
static let title: LocalizedStringResource = "Compose a post to Mastodon"
|
||||
static let description: IntentDescription = "Use Ice Cubes to compose a post for Mastodon"
|
||||
static let openAppWhenRun: Bool = true
|
||||
|
||||
@Parameter(title: "Post content", inputConnectionBehavior: .connectToPreviousIntentResult)
|
||||
var content: String?
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
AppIntentService.shared.handledIntent = .init(intent: self)
|
||||
return .result()
|
||||
}
|
||||
}
|
89
IceCubesAppIntents/TabIntent.swift
Normal file
89
IceCubesAppIntents/TabIntent.swift
Normal file
|
@ -0,0 +1,89 @@
|
|||
import AppIntents
|
||||
import Foundation
|
||||
|
||||
enum TabEnum: String, AppEnum, Sendable {
|
||||
case timeline, notifications, mentions, explore, messages, settings
|
||||
case trending, federated, local
|
||||
case profile
|
||||
case bookmarks
|
||||
case favorites
|
||||
case post
|
||||
case followedTags
|
||||
case lists
|
||||
case links
|
||||
|
||||
static var typeDisplayName: LocalizedStringResource { "Tab" }
|
||||
|
||||
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Tab"
|
||||
|
||||
nonisolated static var caseDisplayRepresentations: [TabEnum: DisplayRepresentation] {
|
||||
[.timeline: .init(title: "Home Timeline"),
|
||||
.trending: .init(title: "Trending Timeline"),
|
||||
.federated: .init(title: "Federated Timeline"),
|
||||
.local: .init(title: "Local Timeline"),
|
||||
.notifications: .init(title: "Notifications"),
|
||||
.mentions: .init(title: "Mentions"),
|
||||
.explore: .init(title: "Explore & Trending"),
|
||||
.messages: .init(title: "Private Messages"),
|
||||
.settings: .init(title: "Settings"),
|
||||
.profile: .init(title: "Profile"),
|
||||
.bookmarks: .init(title: "Bookmarks"),
|
||||
.favorites: .init(title: "Favorites"),
|
||||
.followedTags: .init(title: "Followed Tags"),
|
||||
.lists: .init(title: "Lists"),
|
||||
.links: .init(title: "Trending Links"),
|
||||
.post: .init(title: "New post")]
|
||||
}
|
||||
|
||||
var toAppTab: Tab {
|
||||
switch self {
|
||||
case .timeline:
|
||||
.timeline
|
||||
case .notifications:
|
||||
.notifications
|
||||
case .mentions:
|
||||
.mentions
|
||||
case .explore:
|
||||
.explore
|
||||
case .messages:
|
||||
.messages
|
||||
case .settings:
|
||||
.settings
|
||||
case .trending:
|
||||
.trending
|
||||
case .federated:
|
||||
.federated
|
||||
case .local:
|
||||
.local
|
||||
case .profile:
|
||||
.profile
|
||||
case .bookmarks:
|
||||
.bookmarks
|
||||
case .favorites:
|
||||
.favorites
|
||||
case .post:
|
||||
.post
|
||||
case .followedTags:
|
||||
.followedTags
|
||||
case .lists:
|
||||
.lists
|
||||
case .links:
|
||||
.links
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TabIntent: AppIntent {
|
||||
static let title: LocalizedStringResource = "Open on a tab"
|
||||
static let description: IntentDescription = "Open the app on a specific tab"
|
||||
static let openAppWhenRun: Bool = true
|
||||
|
||||
@Parameter(title: "Selected tab")
|
||||
var tab: TabEnum
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
AppIntentService.shared.handledIntent = .init(intent: self)
|
||||
return .result()
|
||||
}
|
||||
}
|
37
IceCubesAppIntents/TimelineFilterEntity.swift
Normal file
37
IceCubesAppIntents/TimelineFilterEntity.swift
Normal file
|
@ -0,0 +1,37 @@
|
|||
import AppAccount
|
||||
import AppIntents
|
||||
import Env
|
||||
import Foundation
|
||||
import Models
|
||||
import Network
|
||||
import Timeline
|
||||
|
||||
public struct TimelineFilterEntity: Identifiable, AppEntity {
|
||||
public var id: String { timeline.id }
|
||||
|
||||
public let timeline: TimelineFilter
|
||||
|
||||
public static let defaultQuery = DefaultTimelineEntityQuery()
|
||||
|
||||
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "TimelineFilter"
|
||||
|
||||
public var displayRepresentation: DisplayRepresentation {
|
||||
DisplayRepresentation(title: "\(timeline.title)")
|
||||
}
|
||||
}
|
||||
|
||||
public struct DefaultTimelineEntityQuery: EntityQuery {
|
||||
public init() {}
|
||||
|
||||
public func entities(for _: [TimelineFilter.ID]) async throws -> [TimelineFilterEntity] {
|
||||
[.home, .trending, .federated, .local].map { .init(timeline: $0) }
|
||||
}
|
||||
|
||||
public func suggestedEntities() async throws -> [TimelineFilterEntity] {
|
||||
[.home, .trending, .federated, .local].map { .init(timeline: $0) }
|
||||
}
|
||||
|
||||
public func defaultResult() async -> TimelineFilterEntity? {
|
||||
.init(timeline: .home)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.859",
|
||||
"green" : "0.267",
|
||||
"red" : "0.675"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.949",
|
||||
"green" : "0.945",
|
||||
"red" : "0.941"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.133",
|
||||
"green" : "0.082",
|
||||
"red" : "0.067"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import WidgetKit
|
||||
|
||||
struct HashtagPostsWidgetProvider: AppIntentTimelineProvider {
|
||||
func placeholder(in _: Context) -> PostsWidgetEntry {
|
||||
.init(date: Date(),
|
||||
title: "#Mastodon",
|
||||
statuses: [.placeholder()],
|
||||
images: [:])
|
||||
}
|
||||
|
||||
func snapshot(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry {
|
||||
if let entry = await timeline(for: configuration, context: context).entries.first {
|
||||
return entry
|
||||
}
|
||||
return .init(date: Date(),
|
||||
title: "#Mastodon",
|
||||
statuses: [],
|
||||
images: [:])
|
||||
}
|
||||
|
||||
func timeline(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> Timeline<PostsWidgetEntry> {
|
||||
await timeline(for: configuration, context: context)
|
||||
}
|
||||
|
||||
private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline<PostsWidgetEntry> {
|
||||
do {
|
||||
let timeline: TimelineFilter = .hashtag(tag: configuration.hashgtag, accountId: nil)
|
||||
let statuses = await loadStatuses(for: timeline,
|
||||
account: configuration.account,
|
||||
widgetFamily: context.family)
|
||||
let images = try await loadImages(urls: statuses.map { $0.account.avatar })
|
||||
return Timeline(entries: [.init(date: Date(),
|
||||
title: timeline.title,
|
||||
statuses: statuses,
|
||||
images: images)], policy: .atEnd)
|
||||
} catch {
|
||||
return Timeline(entries: [.init(date: Date(),
|
||||
title: "#Mastodon",
|
||||
statuses: [],
|
||||
images: [:])],
|
||||
policy: .atEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HashtagPostsWidget: Widget {
|
||||
let kind: String = "HashtagPostsWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(kind: kind,
|
||||
intent: HashtagPostsWidgetConfiguration.self,
|
||||
provider: HashtagPostsWidgetProvider())
|
||||
{ entry in
|
||||
PostsWidgetView(entry: entry)
|
||||
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
|
||||
}
|
||||
.configurationDisplayName("Hashtag timeline")
|
||||
.description("Show the latest post for the selected hashtag")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemMedium) {
|
||||
HashtagPostsWidget()
|
||||
} timeline: {
|
||||
PostsWidgetEntry(date: .now,
|
||||
title: "#Mastodon",
|
||||
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
|
||||
images: [:])
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import AppIntents
|
||||
import WidgetKit
|
||||
|
||||
struct HashtagPostsWidgetConfiguration: WidgetConfigurationIntent {
|
||||
static let title: LocalizedStringResource = "Configuration"
|
||||
static let description = IntentDescription("Choose the account and hashtag for this widget")
|
||||
|
||||
@Parameter(title: "Account")
|
||||
var account: AppAccountEntity
|
||||
|
||||
@Parameter(title: "Hashtag")
|
||||
var hashgtag: String
|
||||
}
|
||||
|
||||
extension HashtagPostsWidgetConfiguration {
|
||||
static var previewAccount: HashtagPostsWidgetConfiguration {
|
||||
let intent = HashtagPostsWidgetConfiguration()
|
||||
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
|
||||
intent.hashgtag = "Mastodon"
|
||||
return intent
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@main
|
||||
struct IceCubesAppWidgetsExtensionBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
LatestPostsWidget()
|
||||
HashtagPostsWidget()
|
||||
ListsPostWidget()
|
||||
MentionsWidget()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)$(BUNDLE_ID_PREFIX).IceCubesApp</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(BUNDLE_ID_PREFIX).IceCubesApp</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -2,7 +2,10 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>DEEPL_SECRET</key>
|
||||
<string>NICE_TRY_AGAIN</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,93 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import WidgetKit
|
||||
|
||||
struct LatestPostsWidgetProvider: AppIntentTimelineProvider {
|
||||
func placeholder(in _: Context) -> PostsWidgetEntry {
|
||||
.init(date: Date(),
|
||||
title: "Home",
|
||||
statuses: [.placeholder()],
|
||||
images: [:])
|
||||
}
|
||||
|
||||
func snapshot(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry {
|
||||
if let entry = await timeline(for: configuration, context: context).entries.first {
|
||||
return entry
|
||||
}
|
||||
return .init(date: Date(),
|
||||
title: configuration.timeline.timeline.title,
|
||||
statuses: [],
|
||||
images: [:])
|
||||
}
|
||||
|
||||
func timeline(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> Timeline<PostsWidgetEntry> {
|
||||
await timeline(for: configuration, context: context)
|
||||
}
|
||||
|
||||
private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline<PostsWidgetEntry> {
|
||||
do {
|
||||
let statuses = await loadStatuses(for: configuration.timeline.timeline,
|
||||
account: configuration.account,
|
||||
widgetFamily: context.family)
|
||||
let images = try await loadImages(urls: statuses.map { $0.account.avatar })
|
||||
return Timeline(entries: [.init(date: Date(),
|
||||
title: configuration.timeline.timeline.title,
|
||||
statuses: statuses,
|
||||
images: images)], policy: .atEnd)
|
||||
} catch {
|
||||
return Timeline(entries: [.init(date: Date(),
|
||||
title: configuration.timeline.timeline.title,
|
||||
statuses: [],
|
||||
images: [:])],
|
||||
policy: .atEnd)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImages(urls: [URL]) async throws -> [URL: UIImage] {
|
||||
try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in
|
||||
for url in urls {
|
||||
group.addTask {
|
||||
let response = try await URLSession.shared.data(from: url)
|
||||
return (url, UIImage(data: response.0))
|
||||
}
|
||||
}
|
||||
|
||||
var images: [URL: UIImage] = [:]
|
||||
|
||||
for try await (url, image) in group {
|
||||
images[url] = image
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LatestPostsWidget: Widget {
|
||||
let kind: String = "LatestPostsWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(kind: kind,
|
||||
intent: LatestPostsWidgetConfiguration.self,
|
||||
provider: LatestPostsWidgetProvider())
|
||||
{ entry in
|
||||
PostsWidgetView(entry: entry)
|
||||
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
|
||||
}
|
||||
.configurationDisplayName("Latest posts")
|
||||
.description("Show the latest post for the selected timeline")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemMedium) {
|
||||
LatestPostsWidget()
|
||||
} timeline: {
|
||||
PostsWidgetEntry(date: .now,
|
||||
title: "Mastodon",
|
||||
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
|
||||
images: [:])
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import AppIntents
|
||||
import WidgetKit
|
||||
|
||||
struct LatestPostsWidgetConfiguration: WidgetConfigurationIntent {
|
||||
static let title: LocalizedStringResource = "Configuration"
|
||||
static let description = IntentDescription("Choose the account and timeline for this widget")
|
||||
|
||||
@Parameter(title: "Account")
|
||||
var account: AppAccountEntity
|
||||
|
||||
@Parameter(title: "Timeline")
|
||||
var timeline: TimelineFilterEntity
|
||||
}
|
||||
|
||||
extension LatestPostsWidgetConfiguration {
|
||||
static var previewAccount: LatestPostsWidgetConfiguration {
|
||||
let intent = LatestPostsWidgetConfiguration()
|
||||
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
|
||||
intent.timeline = .init(timeline: .home)
|
||||
return intent
|
||||
}
|
||||
}
|
75
IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift
Normal file
75
IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift
Normal file
|
@ -0,0 +1,75 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import WidgetKit
|
||||
|
||||
struct ListsWidgetProvider: AppIntentTimelineProvider {
|
||||
func placeholder(in _: Context) -> PostsWidgetEntry {
|
||||
.init(date: Date(),
|
||||
title: "List name",
|
||||
statuses: [.placeholder()],
|
||||
images: [:])
|
||||
}
|
||||
|
||||
func snapshot(for configuration: ListsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry {
|
||||
if let entry = await timeline(for: configuration, context: context).entries.first {
|
||||
return entry
|
||||
}
|
||||
return .init(date: Date(),
|
||||
title: "List name",
|
||||
statuses: [],
|
||||
images: [:])
|
||||
}
|
||||
|
||||
func timeline(for configuration: ListsWidgetConfiguration, in context: Context) async -> Timeline<PostsWidgetEntry> {
|
||||
await timeline(for: configuration, context: context)
|
||||
}
|
||||
|
||||
private func timeline(for configuration: ListsWidgetConfiguration, context: Context) async -> Timeline<PostsWidgetEntry> {
|
||||
do {
|
||||
let timeline: TimelineFilter = .list(list: configuration.timeline.list)
|
||||
let statuses = await loadStatuses(for: timeline,
|
||||
account: configuration.account,
|
||||
widgetFamily: context.family)
|
||||
let images = try await loadImages(urls: statuses.map { $0.account.avatar })
|
||||
return Timeline(entries: [.init(date: Date(),
|
||||
title: timeline.title,
|
||||
statuses: statuses,
|
||||
images: images)], policy: .atEnd)
|
||||
} catch {
|
||||
return Timeline(entries: [.init(date: Date(),
|
||||
title: "List name",
|
||||
statuses: [],
|
||||
images: [:])],
|
||||
policy: .atEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ListsPostWidget: Widget {
|
||||
let kind: String = "ListsPostWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(kind: kind,
|
||||
intent: ListsWidgetConfiguration.self,
|
||||
provider: ListsWidgetProvider())
|
||||
{ entry in
|
||||
PostsWidgetView(entry: entry)
|
||||
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
|
||||
}
|
||||
.configurationDisplayName("List timeline")
|
||||
.description("Show the latest post for the selected list")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemMedium) {
|
||||
ListsPostWidget()
|
||||
} timeline: {
|
||||
PostsWidgetEntry(date: .now,
|
||||
title: "List name",
|
||||
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
|
||||
images: [:])
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import AppIntents
|
||||
import WidgetKit
|
||||
|
||||
struct ListsWidgetConfiguration: WidgetConfigurationIntent {
|
||||
static let title: LocalizedStringResource = "Configuration"
|
||||
static let description = IntentDescription("Choose the account and list for this widget")
|
||||
|
||||
@Parameter(title: "Account")
|
||||
var account: AppAccountEntity
|
||||
|
||||
@Parameter(title: "List")
|
||||
var timeline: ListEntity
|
||||
}
|
||||
|
||||
extension ListsWidgetConfiguration {
|
||||
static var previewAccount: LatestPostsWidgetConfiguration {
|
||||
let intent = LatestPostsWidgetConfiguration()
|
||||
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
|
||||
return intent
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import WidgetKit
|
||||
|
||||
struct MentionsWidgetProvider: AppIntentTimelineProvider {
|
||||
func placeholder(in _: Context) -> PostsWidgetEntry {
|
||||
.init(date: Date(),
|
||||
title: "Mentions",
|
||||
statuses: [.placeholder()],
|
||||
images: [:])
|
||||
}
|
||||
|
||||
func snapshot(for configuration: MentionsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry {
|
||||
if let entry = await timeline(for: configuration, context: context).entries.first {
|
||||
return entry
|
||||
}
|
||||
return .init(date: Date(),
|
||||
title: "Mentions",
|
||||
statuses: [],
|
||||
images: [:])
|
||||
}
|
||||
|
||||
func timeline(for configuration: MentionsWidgetConfiguration, in context: Context) async -> Timeline<PostsWidgetEntry> {
|
||||
await timeline(for: configuration, context: context)
|
||||
}
|
||||
|
||||
private func timeline(for configuration: MentionsWidgetConfiguration, context _: Context) async -> Timeline<PostsWidgetEntry> {
|
||||
do {
|
||||
let client = Client(server: configuration.account.account.server,
|
||||
oauthToken: configuration.account.account.oauthToken)
|
||||
var excludedTypes = Models.Notification.NotificationType.allCases
|
||||
excludedTypes.removeAll(where: { $0 == .mention })
|
||||
let notifications: [Models.Notification] =
|
||||
try await client.get(endpoint: Notifications.notifications(minId: nil,
|
||||
maxId: nil,
|
||||
types: excludedTypes.map(\.rawValue),
|
||||
limit: 5))
|
||||
let statuses = notifications.compactMap { $0.status }
|
||||
let images = try await loadImages(urls: statuses.map { $0.account.avatar })
|
||||
return Timeline(entries: [.init(date: Date(),
|
||||
title: "Mentions",
|
||||
statuses: statuses,
|
||||
images: images)], policy: .atEnd)
|
||||
} catch {
|
||||
return Timeline(entries: [.init(date: Date(),
|
||||
title: "Mentions",
|
||||
statuses: [],
|
||||
images: [:])],
|
||||
policy: .atEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MentionsWidget: Widget {
|
||||
let kind: String = "MentionsWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(kind: kind,
|
||||
intent: MentionsWidgetConfiguration.self,
|
||||
provider: MentionsWidgetProvider())
|
||||
{ entry in
|
||||
PostsWidgetView(entry: entry)
|
||||
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
|
||||
}
|
||||
.configurationDisplayName("Mentions")
|
||||
.description("Show the latest mentions for the selected account.")
|
||||
.supportedFamilies([.systemLarge, .systemExtraLarge])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemMedium) {
|
||||
MentionsWidget()
|
||||
} timeline: {
|
||||
PostsWidgetEntry(date: .now,
|
||||
title: "Mentions",
|
||||
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
|
||||
images: [:])
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import AppIntents
|
||||
import WidgetKit
|
||||
|
||||
struct MentionsWidgetConfiguration: WidgetConfigurationIntent {
|
||||
static let title: LocalizedStringResource = "Configuration"
|
||||
static let description = IntentDescription("Choose the account for this widget")
|
||||
|
||||
@Parameter(title: "Account")
|
||||
var account: AppAccountEntity
|
||||
}
|
||||
|
||||
extension MentionsWidgetConfiguration {
|
||||
static var previewAccount: MentionsWidgetConfiguration {
|
||||
let intent = MentionsWidgetConfiguration()
|
||||
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
|
||||
return intent
|
||||
}
|
||||
}
|
96
IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift
Normal file
96
IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift
Normal file
|
@ -0,0 +1,96 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import WidgetKit
|
||||
|
||||
struct PostsWidgetEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let title: String
|
||||
let statuses: [Status]
|
||||
let images: [URL: UIImage]
|
||||
}
|
||||
|
||||
struct PostsWidgetView: View {
|
||||
var entry: LatestPostsWidgetProvider.Entry
|
||||
|
||||
@Environment(\.widgetFamily) var family
|
||||
@Environment(\.redactionReasons) var redacted
|
||||
|
||||
var contentLineLimit: Int {
|
||||
switch family {
|
||||
case .systemSmall, .systemMedium:
|
||||
return 5
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
headerView
|
||||
ForEach(entry.statuses) { status in
|
||||
makeStatusView(status)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private var headerView: some View {
|
||||
HStack {
|
||||
Text(entry.title)
|
||||
Spacer()
|
||||
Image(systemName: "cube")
|
||||
}
|
||||
.font(.subheadline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(Color("AccentColor"))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeStatusView(_ status: Status) -> some View {
|
||||
if let url = URL(string: status.url ?? "") {
|
||||
Link(destination: url, label: {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
makeStatusHeaderView(status)
|
||||
Text(status.content.asSafeMarkdownAttributedString)
|
||||
.font(.footnote)
|
||||
.lineLimit(contentLineLimit)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.leading, 20)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func makeStatusHeaderView(_ status: Status) -> some View {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
if let image = entry.images[status.account.avatar] {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.frame(width: 16, height: 16)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Circle()
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 16, height: 16)
|
||||
}
|
||||
HStack(spacing: 0) {
|
||||
Text(status.account.safeDisplayName)
|
||||
.foregroundStyle(.primary)
|
||||
if family != .systemSmall {
|
||||
Text(" @")
|
||||
.foregroundStyle(.tertiary)
|
||||
Text(status.account.username)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.font(.footnote)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
56
IceCubesAppWidgetsExtension/Shared/SharedUtils.swift
Normal file
56
IceCubesAppWidgetsExtension/Shared/SharedUtils.swift
Normal file
|
@ -0,0 +1,56 @@
|
|||
import AppAccount
|
||||
import Foundation
|
||||
import Models
|
||||
import Network
|
||||
import StatusKit
|
||||
import Timeline
|
||||
import UIKit
|
||||
import WidgetKit
|
||||
|
||||
func loadStatuses(for timeline: TimelineFilter,
|
||||
account: AppAccountEntity,
|
||||
widgetFamily: WidgetFamily) async -> [Status]
|
||||
{
|
||||
let client = Client(server: account.account.server, oauthToken: account.account.oauthToken)
|
||||
do {
|
||||
var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||
maxId: nil,
|
||||
minId: nil,
|
||||
offset: nil))
|
||||
statuses = statuses.filter { $0.reblog == nil && !$0.content.asRawText.isEmpty }
|
||||
switch widgetFamily {
|
||||
case .systemSmall, .systemMedium:
|
||||
if statuses.count >= 1 {
|
||||
statuses = statuses.prefix(upTo: 1).map { $0 }
|
||||
}
|
||||
case .systemLarge, .systemExtraLarge:
|
||||
if statuses.count >= 5 {
|
||||
statuses = statuses.prefix(upTo: 5).map { $0 }
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return statuses
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func loadImages(urls: [URL]) async throws -> [URL: UIImage] {
|
||||
try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in
|
||||
for url in urls {
|
||||
group.addTask {
|
||||
let response = try await URLSession.shared.data(from: url)
|
||||
return (url, UIImage(data: response.0))
|
||||
}
|
||||
}
|
||||
|
||||
var images: [URL: UIImage] = [:]
|
||||
|
||||
for try await (url, image) in group {
|
||||
images[url] = image
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
|
@ -9,12 +9,11 @@ import Notifications
|
|||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
@MainActor
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
@MainActor override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.contentHandler = contentHandler
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
|
||||
|
@ -124,6 +123,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func toRemoteNotification(localNotification: MastodonPushNotification) async -> Models.Notification? {
|
||||
do {
|
||||
if let account = AppAccountsManager.shared.availableAccounts.first(where: { $0.oauthToken?.accessToken == localNotification.accessToken }) {
|
||||
|
@ -137,6 +137,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func buildMessageIntent(remoteNotification: Models.Notification,
|
||||
currentUser: String,
|
||||
avatarURL: URL) -> INSendMessageIntent
|
||||
|
|
|
@ -24,16 +24,13 @@ extension NotificationService {
|
|||
var _plaintext: Data?
|
||||
do {
|
||||
_plaintext = try AES.GCM.open(sealedBox, using: key)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
} catch {}
|
||||
guard let plaintext = _plaintext else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1])
|
||||
guard plaintext.count >= 2 + paddingLength else {
|
||||
print("1")
|
||||
fatalError()
|
||||
}
|
||||
let unpadded = plaintext.suffix(from: paddingLength + 2)
|
||||
|
|
|
@ -2,11 +2,11 @@ import Account
|
|||
import AppAccount
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Models
|
||||
|
||||
class ShareViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
|
|
798
LICENSE
798
LICENSE
|
@ -1,201 +1,661 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
1. Definitions.
|
||||
Preamble
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
0. Definitions.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
1. Source Code.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
2. Basic Permissions.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
|
|
@ -31,7 +31,7 @@ let package = Package(
|
|||
.product(name: "Models", package: "Models"),
|
||||
.product(name: "StatusKit", package: "StatusKit"),
|
||||
.product(name: "Env", package: "Env"),
|
||||
.product(name: "ButtonKit", package: "ButtonKit")
|
||||
.product(name: "ButtonKit", package: "ButtonKit"),
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency"),
|
||||
|
|
|
@ -9,6 +9,7 @@ public struct AccountDetailContextMenu: View {
|
|||
@Environment(UserPreferences.self) private var preferences
|
||||
|
||||
@Binding var showBlockConfirmation: Bool
|
||||
@Binding var showTranslateView: Bool
|
||||
|
||||
var viewModel: AccountDetailViewModel
|
||||
|
||||
|
@ -28,18 +29,16 @@ public struct AccountDetailContextMenu: View {
|
|||
Label("account.action.message", systemImage: "tray.full")
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
Divider()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if viewModel.relationship?.blocking == true {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
viewModel.relationship = try await client.post(endpoint: Accounts.unblock(id: account.id))
|
||||
} catch {
|
||||
print("Error while unblocking: \(error.localizedDescription)")
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} label: {
|
||||
Label("account.action.unblock", systemImage: "person.crop.circle.badge.exclamationmark")
|
||||
|
@ -57,9 +56,7 @@ public struct AccountDetailContextMenu: View {
|
|||
Task {
|
||||
do {
|
||||
viewModel.relationship = try await client.post(endpoint: Accounts.unmute(id: account.id))
|
||||
} catch {
|
||||
print("Error while unmuting: \(error.localizedDescription)")
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} label: {
|
||||
Label("account.action.unmute", systemImage: "speaker")
|
||||
|
@ -71,9 +68,7 @@ public struct AccountDetailContextMenu: View {
|
|||
Task {
|
||||
do {
|
||||
viewModel.relationship = try await client.post(endpoint: Accounts.mute(id: account.id, json: MuteData(duration: duration.rawValue)))
|
||||
} catch {
|
||||
print("Error while muting: \(error.localizedDescription)")
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,9 +87,7 @@ public struct AccountDetailContextMenu: View {
|
|||
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
||||
notify: false,
|
||||
reblogs: relationship.showingReblogs))
|
||||
} catch {
|
||||
print("Error while disabling notifications: \(error.localizedDescription)")
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} label: {
|
||||
Label("account.action.notify-disable", systemImage: "bell.fill")
|
||||
|
@ -106,9 +99,7 @@ public struct AccountDetailContextMenu: View {
|
|||
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
||||
notify: true,
|
||||
reblogs: relationship.showingReblogs))
|
||||
} catch {
|
||||
print("Error while enabling notifications: \(error.localizedDescription)")
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} label: {
|
||||
Label("account.action.notify-enable", systemImage: "bell")
|
||||
|
@ -121,9 +112,7 @@ public struct AccountDetailContextMenu: View {
|
|||
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
||||
notify: relationship.notifying,
|
||||
reblogs: false))
|
||||
} catch {
|
||||
print("Error while disabling reboosts: \(error.localizedDescription)")
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} label: {
|
||||
Label("account.action.reboosts-hide", image: "Rocket.Fill")
|
||||
|
@ -135,9 +124,7 @@ public struct AccountDetailContextMenu: View {
|
|||
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
||||
notify: relationship.notifying,
|
||||
reblogs: true))
|
||||
} catch {
|
||||
print("Error while enabling reboosts: \(error.localizedDescription)")
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} label: {
|
||||
Label("account.action.reboosts-show", image: "Rocket")
|
||||
|
@ -145,20 +132,20 @@ public struct AccountDetailContextMenu: View {
|
|||
}
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
Divider()
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier {
|
||||
Button {
|
||||
Task {
|
||||
await viewModel.translate(userLang: lang)
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
if #available(iOS 17.4, *) {
|
||||
Button {
|
||||
showTranslateView = true
|
||||
} label: {
|
||||
Label("status.action.translate", systemImage: "captions.bubble")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("status.action.translate", systemImage: "captions.bubble")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if viewModel.relationship?.following == true {
|
||||
Button {
|
||||
|
@ -178,7 +165,7 @@ public struct AccountDetailContextMenu: View {
|
|||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
Divider()
|
||||
Divider()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,8 +172,8 @@ struct AccountDetailHeaderView: View {
|
|||
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.emojiSize(Font.scaledHeadlineFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledHeadlineFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
|
||||
.accessibilityAddTraits(.isHeader)
|
||||
|
||||
// The views here are wrapped in ZStacks as a Text(Image) does not provide an `accessibilityLabel`.
|
||||
|
@ -207,6 +207,7 @@ struct AccountDetailHeaderView: View {
|
|||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
.accessibilityRespondsToUserInteraction(false)
|
||||
movedToView
|
||||
joinedAtView
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
|
@ -236,8 +237,8 @@ struct AccountDetailHeaderView: View {
|
|||
EmojiTextApp(account.note, emojis: account.emojis)
|
||||
.font(.scaledBody)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.emojiSize(Font.scaledBodyFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledBodyFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.padding(.top, 8)
|
||||
.textSelection(.enabled)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
|
@ -311,6 +312,17 @@ struct AccountDetailHeaderView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var movedToView: some View {
|
||||
if let movedTo = viewModel.account?.moved {
|
||||
Button("account.movedto.redirect-\("@\(movedTo.acct)")") {
|
||||
routerPath.navigate(to: .accountDetailWithAccount(account: movedTo))
|
||||
}
|
||||
.font(.scaledCallout)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeNoteView(_ note: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
@ -338,8 +350,8 @@ struct AccountDetailHeaderView: View {
|
|||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
EmojiTextApp(.init(stringValue: field.name), emojis: viewModel.account?.emojis ?? [])
|
||||
.emojiSize(Font.scaledHeadlineFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledHeadlineFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
|
||||
.font(.scaledHeadline)
|
||||
HStack {
|
||||
if field.verifiedAt != nil {
|
||||
|
@ -348,8 +360,8 @@ struct AccountDetailHeaderView: View {
|
|||
.accessibilityHidden(true)
|
||||
}
|
||||
EmojiTextApp(field.value, emojis: viewModel.account?.emojis ?? [])
|
||||
.emojiSize(Font.scaledBodyFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledBodyFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.foregroundColor(theme.tintColor)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
|
@ -372,15 +384,15 @@ struct AccountDetailHeaderView: View {
|
|||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel("accessibility.tabs.profile.fields.container.label")
|
||||
#if os(visionOS)
|
||||
.background(Material.thick)
|
||||
.background(Material.thick)
|
||||
#else
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.cornerRadius(4)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
||||
)
|
||||
.cornerRadius(4)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,9 @@ public struct AccountDetailView: View {
|
|||
@State private var viewModel: AccountDetailViewModel
|
||||
@State private var isCurrentUser: Bool = false
|
||||
@State private var showBlockConfirmation: Bool = false
|
||||
|
||||
@State private var isEditingAccount: Bool = false
|
||||
@State private var isEditingFilters: Bool = false
|
||||
@State private var isEditingRelationshipNote: Bool = false
|
||||
|
||||
@State private var showTranslateView: Bool = false
|
||||
|
||||
@State private var displayTitle: Bool = false
|
||||
|
||||
@Binding var scrollToTopSignal: Int
|
||||
|
@ -88,14 +86,14 @@ public struct AccountDetailView: View {
|
|||
.environment(\.defaultMinListRowHeight, 1)
|
||||
.listStyle(.plain)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.onChange(of: scrollToTopSignal) {
|
||||
withAnimation {
|
||||
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
||||
.onChange(of: scrollToTopSignal) {
|
||||
withAnimation {
|
||||
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
guard reasons != .placeholder else { return }
|
||||
|
@ -136,20 +134,14 @@ public struct AccountDetailView: View {
|
|||
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
|
||||
}
|
||||
}
|
||||
.onChange(of: isEditingAccount) { _, newValue in
|
||||
if !newValue {
|
||||
.onChange(of: routerPath.presentedSheet) { oldValue, newValue in
|
||||
if oldValue == .accountEditInfo || newValue == .accountEditInfo {
|
||||
Task {
|
||||
await viewModel.fetchAccount()
|
||||
await preferences.refreshServerPreferences()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isEditingAccount, content: {
|
||||
EditAccountView()
|
||||
})
|
||||
.sheet(isPresented: $isEditingFilters, content: {
|
||||
FiltersListView()
|
||||
})
|
||||
.sheet(isPresented: $isEditingRelationshipNote, content: {
|
||||
EditRelationshipNoteView(accountDetailViewModel: viewModel)
|
||||
})
|
||||
|
@ -220,7 +212,6 @@ public struct AccountDetailView: View {
|
|||
AvatarView(account.avatar, config: .badge)
|
||||
.padding(.leading, -4)
|
||||
.accessibilityLabel(account.safeDisplayName)
|
||||
|
||||
}
|
||||
.accessibilityAddTraits(.isImage)
|
||||
.buttonStyle(.plain)
|
||||
|
@ -247,14 +238,18 @@ public struct AccountDetailView: View {
|
|||
bottom: 0,
|
||||
trailing: .layoutPadding))
|
||||
.listRowSeparator(.hidden)
|
||||
#if !os(visionOS)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
#endif
|
||||
ForEach(viewModel.pinned) { status in
|
||||
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
|
||||
}
|
||||
Rectangle()
|
||||
#if os(visionOS)
|
||||
.fill(Color.clear)
|
||||
#else
|
||||
.fill(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.frame(height: 12)
|
||||
.listRowInsets(.init())
|
||||
.listRowSeparator(.hidden)
|
||||
|
@ -284,7 +279,6 @@ public struct AccountDetailView: View {
|
|||
routerPath.presentedSheet = .mentionStatusEditor(account: account,
|
||||
visibility: preferences.postVisibility)
|
||||
#endif
|
||||
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrowshape.turn.up.left")
|
||||
|
@ -292,7 +286,9 @@ public struct AccountDetailView: View {
|
|||
}
|
||||
|
||||
Menu {
|
||||
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, viewModel: viewModel)
|
||||
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
|
||||
showTranslateView: $showTranslateView,
|
||||
viewModel: viewModel)
|
||||
|
||||
if !viewModel.isCurrentUser {
|
||||
Button {
|
||||
|
@ -304,7 +300,7 @@ public struct AccountDetailView: View {
|
|||
|
||||
if isCurrentUser {
|
||||
Button {
|
||||
isEditingAccount = true
|
||||
routerPath.presentedSheet = .accountEditInfo
|
||||
} label: {
|
||||
Label("account.action.edit-info", systemImage: "pencil")
|
||||
}
|
||||
|
@ -319,7 +315,7 @@ public struct AccountDetailView: View {
|
|||
|
||||
if currentInstance.isFiltersSupported {
|
||||
Button {
|
||||
isEditingFilters = true
|
||||
routerPath.presentedSheet = .accountFiltersList
|
||||
} label: {
|
||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||
}
|
||||
|
@ -334,6 +330,20 @@ public struct AccountDetailView: View {
|
|||
if let account = viewModel.account {
|
||||
Divider()
|
||||
|
||||
Button {
|
||||
routerPath.navigate(to: .blockedAccounts)
|
||||
} label: {
|
||||
Label("account.blocked", systemImage: "person.crop.circle.badge.xmark")
|
||||
}
|
||||
|
||||
Button {
|
||||
routerPath.navigate(to: .mutedAccounts)
|
||||
} label: {
|
||||
Label("account.muted", systemImage: "person.crop.circle.badge.moon")
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button {
|
||||
if let url = URL(string: "https://mastometrics.com/auth/login?username=\(account.acct)@\(client.server)&instance=\(client.server)&auto=true") {
|
||||
openURL(url)
|
||||
|
@ -366,26 +376,28 @@ public struct AccountDetailView: View {
|
|||
Task {
|
||||
do {
|
||||
viewModel.relationship = try await client.post(endpoint: Accounts.block(id: account.id))
|
||||
} catch {
|
||||
print("Error while blocking: \(error.localizedDescription)")
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text("account.action.block-user-confirmation")
|
||||
}
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account?.note.asRawText ?? "")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
@MainActor
|
||||
func applyAccountDetailsRowStyle(theme: Theme) -> some View {
|
||||
listRowInsets(.init())
|
||||
.listRowSeparator(.hidden)
|
||||
#if !os(visionOS)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ import SwiftUI
|
|||
private var tabTask: Task<Void, Never>?
|
||||
|
||||
private(set) var statuses: [Status] = []
|
||||
|
||||
|
||||
var boosts: [Status] = []
|
||||
|
||||
/// When coming from a URL like a mention tap in a status.
|
||||
|
@ -151,7 +151,7 @@ import SwiftUI
|
|||
self.familiarFollowers = familiarFollowers?.first?.accounts ?? []
|
||||
}
|
||||
|
||||
func fetchNewestStatuses(pullToRefresh: Bool) async {
|
||||
func fetchNewestStatuses(pullToRefresh _: Bool) async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
statusesState = .loading
|
||||
|
@ -166,7 +166,7 @@ import SwiftUI
|
|||
pinned: nil))
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||
if selectedTab == .boosts {
|
||||
boosts = statuses.filter{ $0.reblog != nil }
|
||||
boosts = statuses.filter { $0.reblog != nil }
|
||||
}
|
||||
if selectedTab == .statuses {
|
||||
pinned =
|
||||
|
@ -191,55 +191,46 @@ import SwiftUI
|
|||
}
|
||||
}
|
||||
|
||||
func fetchNextPage() async {
|
||||
func fetchNextPage() async throws {
|
||||
guard let client else { return }
|
||||
do {
|
||||
switch selectedTab {
|
||||
case .statuses, .replies, .boosts, .media:
|
||||
guard let lastId = statuses.last?.id else { return }
|
||||
if selectedTab == .boosts {
|
||||
statusesState = .display(statuses: boosts, nextPageState: .loadingNextPage)
|
||||
} else {
|
||||
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
||||
}
|
||||
let newStatuses: [Status] =
|
||||
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
||||
sinceId: lastId,
|
||||
tag: nil,
|
||||
onlyMedia: selectedTab == .media,
|
||||
excludeReplies: selectedTab != .replies,
|
||||
excludeReblogs: selectedTab != .boosts,
|
||||
pinned: nil))
|
||||
statuses.append(contentsOf: newStatuses)
|
||||
if selectedTab == .boosts {
|
||||
let newBoosts = statuses.filter{ $0.reblog != nil }
|
||||
self.boosts.append(contentsOf: newBoosts)
|
||||
}
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||
if selectedTab == .boosts {
|
||||
statusesState = .display(statuses: boosts,
|
||||
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
|
||||
} else {
|
||||
statusesState = .display(statuses: statuses,
|
||||
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
|
||||
}
|
||||
case .favorites:
|
||||
guard let nextPageId = favoritesNextPage?.maxId else { return }
|
||||
let newFavorites: [Status]
|
||||
(newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId))
|
||||
favorites.append(contentsOf: newFavorites)
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client)
|
||||
statusesState = .display(statuses: favorites, nextPageState: .hasNextPage)
|
||||
case .bookmarks:
|
||||
guard let nextPageId = bookmarksNextPage?.maxId else { return }
|
||||
let newBookmarks: [Status]
|
||||
(newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId))
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client)
|
||||
bookmarks.append(contentsOf: newBookmarks)
|
||||
statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage)
|
||||
switch selectedTab {
|
||||
case .statuses, .replies, .boosts, .media:
|
||||
guard let lastId = statuses.last?.id else { return }
|
||||
let newStatuses: [Status] =
|
||||
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
||||
sinceId: lastId,
|
||||
tag: nil,
|
||||
onlyMedia: selectedTab == .media,
|
||||
excludeReplies: selectedTab != .replies,
|
||||
excludeReblogs: selectedTab != .boosts,
|
||||
pinned: nil))
|
||||
statuses.append(contentsOf: newStatuses)
|
||||
if selectedTab == .boosts {
|
||||
let newBoosts = statuses.filter { $0.reblog != nil }
|
||||
boosts.append(contentsOf: newBoosts)
|
||||
}
|
||||
} catch {
|
||||
statusesState = .error(error: error)
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||
if selectedTab == .boosts {
|
||||
statusesState = .display(statuses: boosts,
|
||||
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
|
||||
} else {
|
||||
statusesState = .display(statuses: statuses,
|
||||
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
|
||||
}
|
||||
case .favorites:
|
||||
guard let nextPageId = favoritesNextPage?.maxId else { return }
|
||||
let newFavorites: [Status]
|
||||
(newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId))
|
||||
favorites.append(contentsOf: newFavorites)
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client)
|
||||
statusesState = .display(statuses: favorites, nextPageState: .hasNextPage)
|
||||
case .bookmarks:
|
||||
guard let nextPageId = bookmarksNextPage?.maxId else { return }
|
||||
let newBookmarks: [Status]
|
||||
(newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId))
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client)
|
||||
bookmarks.append(contentsOf: newBookmarks)
|
||||
statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,9 +251,13 @@ import SwiftUI
|
|||
|
||||
func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) {
|
||||
if let event = event as? StreamEventUpdate {
|
||||
if event.status.account.id == currentAccount.account?.id, selectedTab == .statuses {
|
||||
statuses.insert(event.status, at: 0)
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
if event.status.account.id == currentAccount.account?.id {
|
||||
if (event.status.inReplyToId == nil && selectedTab == .statuses) ||
|
||||
(event.status.inReplyToId != nil && selectedTab == .replies)
|
||||
{
|
||||
statuses.insert(event.status, at: 0)
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
}
|
||||
}
|
||||
} else if let event = event as? StreamEventDelete {
|
||||
statuses.removeAll(where: { $0.id == event.status })
|
||||
|
@ -278,22 +273,4 @@ import SwiftUI
|
|||
func statusDidAppear(status _: Models.Status) {}
|
||||
|
||||
func statusDidDisappear(status _: Status) {}
|
||||
|
||||
func translate(userLang: String) async {
|
||||
guard let account else { return }
|
||||
withAnimation {
|
||||
isLoadingTranslation = true
|
||||
}
|
||||
|
||||
let userAPIKey = DeepLUserAPIHandler.readIfAllowed()
|
||||
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
|
||||
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
|
||||
|
||||
let translation = try? await deeplClient.request(target: userLang, text: account.note.asRawText)
|
||||
|
||||
withAnimation {
|
||||
self.translation = translation
|
||||
isLoadingTranslation = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ public struct AccountsListRow: View {
|
|||
|
||||
@State private var isEditingRelationshipNote: Bool = false
|
||||
@State private var showBlockConfirmation: Bool = false
|
||||
@State private var showTranslateView: Bool = false
|
||||
|
||||
let isFollowRequest: Bool
|
||||
let requestUpdated: (() -> Void)?
|
||||
|
@ -48,8 +49,8 @@ public struct AccountsListRow: View {
|
|||
VStack(alignment: .leading, spacing: 2) {
|
||||
EmojiTextApp(.init(stringValue: viewModel.account.safeDisplayName), emojis: viewModel.account.emojis)
|
||||
.font(.scaledSubheadline)
|
||||
.emojiSize(Font.scaledSubheadlineFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledSubheadlineFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset)
|
||||
.fontWeight(.semibold)
|
||||
Text("@\(viewModel.account.acct)")
|
||||
.font(.scaledFootnote)
|
||||
|
@ -68,8 +69,8 @@ public struct AccountsListRow: View {
|
|||
|
||||
EmojiTextApp(field.value, emojis: viewModel.account.emojis)
|
||||
.font(.scaledFootnote)
|
||||
.emojiSize(Font.scaledFootnoteFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledFootnoteFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
|
@ -78,8 +79,8 @@ public struct AccountsListRow: View {
|
|||
|
||||
EmojiTextApp(viewModel.account.note, emojis: viewModel.account.emojis, lineLimit: 2)
|
||||
.font(.scaledCaption)
|
||||
.emojiSize(Font.scaledFootnoteFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledFootnoteFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
|
@ -108,8 +109,13 @@ public struct AccountsListRow: View {
|
|||
.onTapGesture {
|
||||
routerPath.navigate(to: .accountDetailWithAccount(account: viewModel.account))
|
||||
}
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account.note.asRawText)
|
||||
#endif
|
||||
.contextMenu {
|
||||
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, viewModel: .init(account: viewModel.account))
|
||||
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
|
||||
showTranslateView: $showTranslateView,
|
||||
viewModel: .init(account: viewModel.account))
|
||||
} preview: {
|
||||
List {
|
||||
AccountDetailHeaderView(viewModel: .init(account: viewModel.account),
|
||||
|
|
|
@ -45,16 +45,19 @@ public struct AccountsListView: View {
|
|||
await viewModel.fetch()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var listView: some View {
|
||||
if currentAccount.account?.id == viewModel.accountId {
|
||||
searchableList
|
||||
} else {
|
||||
standardList
|
||||
.refreshable {
|
||||
await viewModel.fetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var searchableList: some View {
|
||||
List {
|
||||
listContent
|
||||
|
@ -74,13 +77,13 @@ public struct AccountsListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var standardList: some View {
|
||||
List {
|
||||
listContent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var listContent: some View {
|
||||
switch viewModel.state {
|
||||
|
@ -89,9 +92,9 @@ public struct AccountsListView: View {
|
|||
AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder()))
|
||||
.redacted(reason: .placeholder)
|
||||
.allowsHitTesting(false)
|
||||
#if !os(visionOS)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
case let .display(accounts, relationships, nextPageState):
|
||||
if case .followers = viewModel.mode,
|
||||
|
@ -121,51 +124,52 @@ public struct AccountsListView: View {
|
|||
}
|
||||
}
|
||||
Section {
|
||||
ForEach(accounts) { account in
|
||||
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
||||
AccountsListRow(viewModel: .init(account: account,
|
||||
relationShip: relationship))
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
if accounts.isEmpty {
|
||||
PlaceholderView(iconName: "person.icloud",
|
||||
title: "No accounts found",
|
||||
message: "This list of accounts is empty")
|
||||
.listRowSeparator(.hidden)
|
||||
} else {
|
||||
ForEach(accounts) { account in
|
||||
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
||||
AccountsListRow(viewModel: .init(account: account,
|
||||
relationShip: relationship))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
switch nextPageState {
|
||||
case .hasNextPage:
|
||||
loadingRow
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.onAppear {
|
||||
Task {
|
||||
await viewModel.fetchNextPage()
|
||||
}
|
||||
}
|
||||
NextPageView {
|
||||
try await viewModel.fetchNextPage()
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
case .loadingNextPage:
|
||||
loadingRow
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
case let .error(error):
|
||||
Text(error.localizedDescription)
|
||||
#if !os(visionOS)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private var loadingRow: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
AccountsListRow(viewModel: .init(account: .placeholder(),
|
||||
relationShip: .placeholder()))
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.withPreviewsEnv()
|
||||
.environment(Theme.shared)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import Models
|
||||
import Network
|
||||
import Observation
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
public enum AccountsListMode {
|
||||
case following(accountId: String), followers(accountId: String)
|
||||
case favoritedBy(statusId: String), rebloggedBy(statusId: String)
|
||||
case accountsList(accounts: [Account])
|
||||
case blocked, muted
|
||||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
|
@ -20,6 +22,10 @@ public enum AccountsListMode {
|
|||
"account.boosted-by"
|
||||
case .accountsList:
|
||||
""
|
||||
case .blocked:
|
||||
"account.blocked"
|
||||
case .muted:
|
||||
"account.muted"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +38,7 @@ public enum AccountsListMode {
|
|||
|
||||
public enum State {
|
||||
public enum PagingState {
|
||||
case hasNextPage, loadingNextPage, none
|
||||
case hasNextPage, none
|
||||
}
|
||||
|
||||
case loading
|
||||
|
@ -48,7 +54,7 @@ public enum AccountsListMode {
|
|||
var state = State.loading
|
||||
var totalCount: Int?
|
||||
var accountId: String?
|
||||
|
||||
|
||||
var searchQuery: String = ""
|
||||
|
||||
private var nextPageId: String?
|
||||
|
@ -83,6 +89,12 @@ public enum AccountsListMode {
|
|||
case let .accountsList(accounts):
|
||||
self.accounts = accounts
|
||||
link = nil
|
||||
|
||||
case .blocked:
|
||||
(accounts, link) = try await client.getWithLink(endpoint: Accounts.blockList)
|
||||
|
||||
case .muted:
|
||||
(accounts, link) = try await client.getWithLink(endpoint: Accounts.muteList)
|
||||
}
|
||||
nextPageId = link?.maxId
|
||||
relationships = try await client.get(endpoint:
|
||||
|
@ -93,43 +105,45 @@ public enum AccountsListMode {
|
|||
} catch {}
|
||||
}
|
||||
|
||||
func fetchNextPage() async {
|
||||
func fetchNextPage() async throws {
|
||||
guard let client, let nextPageId else { return }
|
||||
do {
|
||||
state = .display(accounts: accounts, relationships: relationships, nextPageState: .loadingNextPage)
|
||||
let newAccounts: [Account]
|
||||
let link: LinkHandler?
|
||||
switch mode {
|
||||
case let .followers(accountId):
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId,
|
||||
let newAccounts: [Account]
|
||||
let link: LinkHandler?
|
||||
switch mode {
|
||||
case let .followers(accountId):
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId,
|
||||
maxId: nextPageId))
|
||||
case let .following(accountId):
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId,
|
||||
maxId: nextPageId))
|
||||
case let .rebloggedBy(statusId):
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId,
|
||||
maxId: nextPageId))
|
||||
case let .following(accountId):
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId,
|
||||
case let .favoritedBy(statusId):
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
|
||||
maxId: nextPageId))
|
||||
case let .rebloggedBy(statusId):
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId,
|
||||
maxId: nextPageId))
|
||||
case let .favoritedBy(statusId):
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
|
||||
maxId: nextPageId))
|
||||
case .accountsList:
|
||||
newAccounts = []
|
||||
link = nil
|
||||
}
|
||||
accounts.append(contentsOf: newAccounts)
|
||||
let newRelationships: [Relationship] =
|
||||
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map(\.id)))
|
||||
case .accountsList:
|
||||
newAccounts = []
|
||||
link = nil
|
||||
|
||||
relationships.append(contentsOf: newRelationships)
|
||||
self.nextPageId = link?.maxId
|
||||
state = .display(accounts: accounts,
|
||||
relationships: relationships,
|
||||
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
|
||||
} catch {
|
||||
print(error)
|
||||
case .blocked:
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.blockList)
|
||||
|
||||
case .muted:
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.muteList)
|
||||
}
|
||||
|
||||
accounts.append(contentsOf: newAccounts)
|
||||
let newRelationships: [Relationship] =
|
||||
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map(\.id)))
|
||||
|
||||
relationships.append(contentsOf: newRelationships)
|
||||
self.nextPageId = link?.maxId
|
||||
state = .display(accounts: accounts,
|
||||
relationships: relationships,
|
||||
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
|
||||
}
|
||||
|
||||
|
||||
func search() async {
|
||||
guard let client, !searchQuery.isEmpty else { return }
|
||||
do {
|
||||
|
@ -148,8 +162,6 @@ public enum AccountsListMode {
|
|||
relationships: relationships,
|
||||
nextPageState: .none)
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import DesignSystem
|
|||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct EditAccountView: View {
|
||||
|
@ -13,8 +13,8 @@ public struct EditAccountView: View {
|
|||
@Environment(UserPreferences.self) private var userPrefs
|
||||
|
||||
@State private var viewModel = EditAccountViewModel()
|
||||
|
||||
public init() { }
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
NavigationStack {
|
||||
|
@ -31,24 +31,24 @@ public struct EditAccountView: View {
|
|||
}
|
||||
.environment(\.editMode, .constant(.active))
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
#endif
|
||||
.navigationTitle("account.edit.navigation-title")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
toolbarContent
|
||||
}
|
||||
.alert("account.edit.error.save.title",
|
||||
isPresented: $viewModel.saveError,
|
||||
actions: {
|
||||
Button("alert.button.ok", action: {})
|
||||
}, message: { Text("account.edit.error.save.message") })
|
||||
.task {
|
||||
viewModel.client = client
|
||||
await viewModel.fetchAccount()
|
||||
}
|
||||
.navigationTitle("account.edit.navigation-title")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
toolbarContent
|
||||
}
|
||||
.alert("account.edit.error.save.title",
|
||||
isPresented: $viewModel.saveError,
|
||||
actions: {
|
||||
Button("alert.button.ok", action: {})
|
||||
}, message: { Text("account.edit.error.save.message") })
|
||||
.task {
|
||||
viewModel.client = client
|
||||
await viewModel.fetchAccount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ public struct EditAccountView: View {
|
|||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
private var imagesSection: some View {
|
||||
Section {
|
||||
ZStack(alignment: .center) {
|
||||
|
@ -189,8 +189,8 @@ public struct EditAccountView: View {
|
|||
TextField("account.edit.metadata-name-placeholder", text: $field.name)
|
||||
.font(.scaledHeadline)
|
||||
TextField("account.edit.metadata-value-placeholder", text: $field.value)
|
||||
.emojiSize(Font.scaledBodyFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledBodyFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.foregroundColor(theme.tintColor)
|
||||
}
|
||||
}
|
||||
|
@ -220,11 +220,7 @@ public struct EditAccountView: View {
|
|||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbarContent: some ToolbarContent {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("action.cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
CancelToolbarItem()
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Models
|
||||
import Network
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable class EditAccountViewModel {
|
||||
|
@ -30,22 +30,23 @@ import StatusKit
|
|||
var fields: [FieldEditViewModel] = []
|
||||
var avatar: URL?
|
||||
var header: URL?
|
||||
|
||||
|
||||
var isPhotoPickerPresented: Bool = false {
|
||||
didSet {
|
||||
if !isPhotoPickerPresented && mediaPickers.isEmpty {
|
||||
if !isPhotoPickerPresented, mediaPickers.isEmpty {
|
||||
isChangingAvatar = false
|
||||
isChangingHeader = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isChangingAvatar: Bool = false
|
||||
var isChangingHeader: Bool = false
|
||||
|
||||
var isLoading: Bool = true
|
||||
var isSaving: Bool = false
|
||||
var saveError: Bool = false
|
||||
|
||||
|
||||
var mediaPickers: [PhotosPickerItem] = [] {
|
||||
didSet {
|
||||
if let item = mediaPickers.first {
|
||||
|
@ -108,47 +109,47 @@ import StatusKit
|
|||
saveError = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func uploadHeader(data: Data) async -> Bool {
|
||||
guard let client else { return false }
|
||||
do {
|
||||
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
|
||||
version: .v1,
|
||||
method: "PATCH",
|
||||
mimeType: "image/jpeg",
|
||||
filename: "header",
|
||||
data: data)
|
||||
version: .v1,
|
||||
method: "PATCH",
|
||||
mimeType: "image/jpeg",
|
||||
filename: "header",
|
||||
data: data)
|
||||
return response?.statusCode == 200
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func uploadAvatar(data: Data) async -> Bool {
|
||||
guard let client else { return false }
|
||||
do {
|
||||
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
|
||||
version: .v1,
|
||||
method: "PATCH",
|
||||
mimeType: "image/jpeg",
|
||||
filename: "avatar",
|
||||
data: data)
|
||||
version: .v1,
|
||||
method: "PATCH",
|
||||
mimeType: "image/jpeg",
|
||||
filename: "avatar",
|
||||
data: data)
|
||||
return response?.statusCode == 200
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func getItemImageData(item: PhotosPickerItem) async -> Data? {
|
||||
guard let imageFile = try? await item.loadTransferable(type: StatusEditor.ImageFileTranseferable.self) else { return nil }
|
||||
|
||||
let compressor = StatusEditor.Compressor()
|
||||
|
||||
guard let compressedData = await compressor.compressImageFrom(url: imageFile.url),
|
||||
let image = UIImage(data: compressedData),
|
||||
let uploadData = try? await compressor.compressImageForUpload(image)
|
||||
let image = UIImage(data: compressedData),
|
||||
let uploadData = try? await compressor.compressImageForUpload(image)
|
||||
else { return nil }
|
||||
|
||||
|
||||
return uploadData
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,20 +71,20 @@ struct EditFilterView: View {
|
|||
.navigationTitle(filter?.title ?? NSLocalizedString("filter.new", comment: ""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.onAppear {
|
||||
if filter == nil {
|
||||
focusedField = .title
|
||||
.onAppear {
|
||||
if filter == nil {
|
||||
focusedField = .title
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
saveButton
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
saveButton
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var expirySection: some View {
|
||||
|
|
|
@ -74,18 +74,18 @@ public struct FiltersListView: View {
|
|||
.navigationTitle("filter.filters")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.task {
|
||||
do {
|
||||
isLoading = true
|
||||
filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2)
|
||||
isLoading = false
|
||||
} catch {
|
||||
isLoading = false
|
||||
.task {
|
||||
do {
|
||||
isLoading = true
|
||||
filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2)
|
||||
isLoading = false
|
||||
} catch {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import Models
|
||||
import Network
|
||||
import Observation
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -32,7 +33,6 @@ import SwiftUI
|
|||
relationship = try await client.post(endpoint: Accounts.follow(id: accountId, notify: false, reblogs: true))
|
||||
relationshipUpdated(relationship)
|
||||
} catch {
|
||||
print("Error while following: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ import SwiftUI
|
|||
relationship = try await client.post(endpoint: Accounts.unfollow(id: accountId))
|
||||
relationshipUpdated(relationship)
|
||||
} catch {
|
||||
print("Error while unfollowing: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +55,6 @@ import SwiftUI
|
|||
reblogs: relationship.showingReblogs))
|
||||
relationshipUpdated(relationship)
|
||||
} catch {
|
||||
print("Error while following: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +67,6 @@ import SwiftUI
|
|||
reblogs: !relationship.showingReblogs))
|
||||
relationshipUpdated(relationship)
|
||||
} catch {
|
||||
print("Error while switching reboosts: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
import Env
|
||||
|
||||
public struct ListsListView: View {
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(Theme.self) private var theme
|
||||
|
||||
public init() {}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
List {
|
||||
ForEach(currentAccount.lists) { list in
|
||||
|
@ -43,4 +43,3 @@ public struct ListsListView: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,50 +1,50 @@
|
|||
import StatusKit
|
||||
import Network
|
||||
import SwiftUI
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import DesignSystem
|
||||
import Network
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct AccountStatusesListView: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
|
||||
@State private var viewModel: AccountStatusesListViewModel
|
||||
@State private var isLoaded = false
|
||||
|
||||
|
||||
public init(mode: AccountStatusesListViewModel.Mode) {
|
||||
_viewModel = .init(initialValue: .init(mode: mode))
|
||||
}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
List {
|
||||
StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.navigationTitle(viewModel.mode.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.refreshable {
|
||||
await viewModel.fetchNewestStatuses(pullToRefresh: true)
|
||||
}
|
||||
.task {
|
||||
guard !isLoaded else { return }
|
||||
viewModel.client = client
|
||||
await viewModel.fetchNewestStatuses(pullToRefresh: false)
|
||||
isLoaded = true
|
||||
}
|
||||
.onChange(of: client.id) { _, _ in
|
||||
isLoaded = false
|
||||
viewModel.client = client
|
||||
Task {
|
||||
.navigationTitle(viewModel.mode.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.refreshable {
|
||||
await viewModel.fetchNewestStatuses(pullToRefresh: true)
|
||||
}
|
||||
.task {
|
||||
guard !isLoaded else { return }
|
||||
viewModel.client = client
|
||||
await viewModel.fetchNewestStatuses(pullToRefresh: false)
|
||||
isLoaded = true
|
||||
}
|
||||
}
|
||||
.onChange(of: client.id) { _, _ in
|
||||
isLoaded = false
|
||||
viewModel.client = client
|
||||
Task {
|
||||
await viewModel.fetchNewestStatuses(pullToRefresh: false)
|
||||
isLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import StatusKit
|
||||
import Network
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
public class AccountStatusesListViewModel: StatusesFetcher {
|
||||
public enum Mode {
|
||||
public enum Mode {
|
||||
case bookmarks, favorites
|
||||
|
||||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .bookmarks:
|
||||
|
@ -18,7 +18,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
|||
"accessibility.tabs.profile.picker.favorites"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func endpoint(sinceId: String?) -> Endpoint {
|
||||
switch self {
|
||||
case .bookmarks:
|
||||
|
@ -28,19 +28,19 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let mode: Mode
|
||||
public var statusesState: StatusesState = .loading
|
||||
var statuses: [Status] = []
|
||||
var nextPage: LinkHandler?
|
||||
|
||||
|
||||
var client: Client?
|
||||
|
||||
|
||||
init(mode: Mode) {
|
||||
self.mode = mode
|
||||
}
|
||||
|
||||
public func fetchNewestStatuses(pullToRefresh: Bool) async {
|
||||
|
||||
public func fetchNewestStatuses(pullToRefresh _: Bool) async {
|
||||
guard let client else { return }
|
||||
statusesState = .loading
|
||||
do {
|
||||
|
@ -52,26 +52,18 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
|||
statusesState = .error(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchNextPage() async {
|
||||
|
||||
public func fetchNextPage() async throws {
|
||||
guard let client, let nextId = nextPage?.maxId else { return }
|
||||
var newStatuses: [Status] = []
|
||||
(newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId))
|
||||
statuses.append(contentsOf: newStatuses)
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||
statusesState = .display(statuses: statuses,
|
||||
nextPageState: .loadingNextPage)
|
||||
do {
|
||||
var newStatuses: [Status] = []
|
||||
(newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId))
|
||||
statuses.append(contentsOf: newStatuses)
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||
statusesState = .display(statuses: statuses,
|
||||
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
|
||||
} catch { }
|
||||
}
|
||||
|
||||
public func statusDidAppear(status: Status) {
|
||||
|
||||
}
|
||||
|
||||
public func statusDidDisappear(status: Status) {
|
||||
|
||||
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
|
||||
}
|
||||
|
||||
public func statusDidAppear(status _: Status) {}
|
||||
|
||||
public func statusDidDisappear(status _: Status) {}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
import Env
|
||||
|
||||
public struct FollowedTagsListView: View {
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(Theme.self) private var theme
|
||||
|
||||
public init() {}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
List(currentAccount.tags) { tag in
|
||||
TagRowView(tag: tag)
|
||||
#if !os(visionOS)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
#endif
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.task {
|
||||
|
@ -32,4 +32,3 @@ public struct FollowedTagsListView: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ public struct AppAccountView: View {
|
|||
@State var viewModel: AppAccountViewModel
|
||||
|
||||
@Binding var isParentPresented: Bool
|
||||
|
||||
|
||||
public init(viewModel: AppAccountViewModel, isParentPresented: Binding<Bool>) {
|
||||
self.viewModel = viewModel
|
||||
_isParentPresented = isParentPresented
|
||||
|
@ -104,8 +104,8 @@ public struct AppAccountView: View {
|
|||
.foregroundColor(theme.labelColor)
|
||||
Text("\(account.username)@\(viewModel.appAccount.server)")
|
||||
.font(.scaledSubheadline)
|
||||
.emojiSize(Font.scaledSubheadlineFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledSubheadlineFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset)
|
||||
.foregroundStyle(Color.secondary)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,11 +33,11 @@ public struct AppAccountsSelectorView: View {
|
|||
|
||||
public init(routerPath: RouterPath,
|
||||
accountCreationEnabled: Bool = true,
|
||||
avatarConfig: AvatarView.FrameConfig = .badge)
|
||||
avatarConfig: AvatarView.FrameConfig? = nil)
|
||||
{
|
||||
self.routerPath = routerPath
|
||||
self.accountCreationEnabled = accountCreationEnabled
|
||||
self.avatarConfig = avatarConfig
|
||||
self.avatarConfig = avatarConfig ?? .badge
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
|
@ -96,9 +96,12 @@ public struct AppAccountsSelectorView: View {
|
|||
AppAccountView(viewModel: viewModel, isParentPresented: $isPresented)
|
||||
}
|
||||
addAccountButton
|
||||
#if os(visionOS)
|
||||
.foregroundStyle(theme.labelColor)
|
||||
#endif
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
if accountCreationEnabled {
|
||||
|
@ -107,8 +110,10 @@ public struct AppAccountsSelectorView: View {
|
|||
aboutButton
|
||||
supportButton
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#if os(visionOS)
|
||||
.foregroundStyle(theme.labelColor)
|
||||
#else
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -129,7 +134,7 @@ public struct AppAccountsSelectorView: View {
|
|||
.environment(routerPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var addAccountButton: some View {
|
||||
Button {
|
||||
isPresented = false
|
||||
|
@ -153,7 +158,7 @@ public struct AppAccountsSelectorView: View {
|
|||
Label("tab.settings", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var supportButton: some View {
|
||||
Button {
|
||||
isPresented = false
|
||||
|
@ -165,7 +170,7 @@ public struct AppAccountsSelectorView: View {
|
|||
Label("settings.app.support", systemImage: "wand.and.stars")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var aboutButton: some View {
|
||||
Button {
|
||||
isPresented = false
|
||||
|
|
|
@ -71,35 +71,35 @@ public struct ConversationDetailView: View {
|
|||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if viewModel.conversation.accounts.count == 1,
|
||||
let account = viewModel.conversation.accounts.first
|
||||
{
|
||||
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.emojiSize(Font.scaledHeadlineFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
|
||||
} else {
|
||||
Text("Direct message with \(viewModel.conversation.accounts.count) people")
|
||||
.font(.scaledHeadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: watcher.latestEvent?.id) {
|
||||
if let latestEvent = watcher.latestEvent {
|
||||
viewModel.handleEvent(event: latestEvent)
|
||||
DispatchQueue.main.async {
|
||||
withAnimation {
|
||||
scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if viewModel.conversation.accounts.count == 1,
|
||||
let account = viewModel.conversation.accounts.first
|
||||
{
|
||||
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.emojiText.size(Font.scaledHeadlineFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
|
||||
} else {
|
||||
Text("Direct message with \(viewModel.conversation.accounts.count) people")
|
||||
.font(.scaledHeadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: watcher.latestEvent?.id) {
|
||||
if let latestEvent = watcher.latestEvent {
|
||||
viewModel.handleEvent(event: latestEvent)
|
||||
DispatchQueue.main.async {
|
||||
withAnimation {
|
||||
scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadingView: some View {
|
||||
|
|
|
@ -36,8 +36,8 @@ struct ConversationMessageView: View {
|
|||
EmojiTextApp(message.content, emojis: message.emojis)
|
||||
.font(.scaledBody)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.emojiSize(Font.scaledBodyFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledBodyFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.padding(6)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handleStatus(status: message, url: url)
|
||||
|
@ -205,12 +205,12 @@ struct ConversationMessageView: View {
|
|||
.frame(height: 200)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
#if targetEnvironment(macCatalyst) || os(visionOS)
|
||||
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement],
|
||||
selectedAttachment: attachement))
|
||||
#else
|
||||
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
|
||||
#endif
|
||||
#if targetEnvironment(macCatalyst) || os(visionOS)
|
||||
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement],
|
||||
selectedAttachment: attachement))
|
||||
#else
|
||||
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import SwiftUI
|
|||
@MainActor
|
||||
struct ConversationsListRow: View {
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
|
||||
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
@Environment(Theme.self) private var theme
|
||||
|
@ -33,8 +33,8 @@ struct ConversationsListRow: View {
|
|||
emojis: conversation.accounts.flatMap(\.emojis))
|
||||
.font(.scaledSubheadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.emojiSize(Font.scaledSubheadlineFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledSubheadlineFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
@ -57,8 +57,8 @@ struct ConversationsListRow: View {
|
|||
.multilineTextAlignment(.leading)
|
||||
.font(.scaledBody)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.emojiSize(Font.scaledBodyFont.emojiSize)
|
||||
.emojiBaselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.emojiText.size(Font.scaledBodyFont.emojiSize)
|
||||
.emojiText.baselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
|
||||
.accessibilityLabel(conversation.lastStatus?.content.asRawText ?? "")
|
||||
}
|
||||
Spacer()
|
||||
|
|
|
@ -48,17 +48,15 @@ public struct ConversationsListView: View {
|
|||
Divider()
|
||||
}
|
||||
} else if conversations.isEmpty, !viewModel.isLoadingFirstPage, !viewModel.isError {
|
||||
EmptyView(iconName: "tray",
|
||||
title: "conversations.empty.title",
|
||||
message: "conversations.empty.message")
|
||||
PlaceholderView(iconName: "tray",
|
||||
title: "conversations.empty.title",
|
||||
message: "conversations.empty.message")
|
||||
} else if viewModel.isError {
|
||||
ErrorView(title: "conversations.error.title",
|
||||
message: "conversations.error.message",
|
||||
buttonTitle: "conversations.error.button")
|
||||
{
|
||||
Task {
|
||||
await viewModel.fetchConversations()
|
||||
}
|
||||
await viewModel.fetchConversations()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ let package = Package(
|
|||
dependencies: [
|
||||
.package(name: "Models", path: "../Models"),
|
||||
.package(name: "Env", path: "../Env"),
|
||||
.package(url: "https://github.com/kean/Nuke", from: "12.0.0"),
|
||||
.package(url: "https://github.com/divadretlaw/EmojiText", from: "3.2.1"),
|
||||
.package(url: "https://github.com/kean/Nuke", from: "12.4.0"),
|
||||
.package(url: "https://github.com/divadretlaw/EmojiText", from: "4.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue