mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-06-02 13:29:41 +00:00
Compare commits
36 commits
Author | SHA1 | Date | |
---|---|---|---|
d36930b7af | |||
c06e3b59e4 | |||
8cca261e43 | |||
f40aeb9cac | |||
1578896b3e | |||
ba3d8b1882 | |||
04af087c4b | |||
a9398c25af | |||
13d721912b | |||
e3d4e693d2 | |||
86c053344b | |||
a996aace80 | |||
18a1d17230 | |||
69cb9a20f9 | |||
bab2b4be9c | |||
bb005386df | |||
c77bb992b4 | |||
7caf00d07d | |||
6ed760a775 | |||
ecd149b3d2 | |||
9aaf0b2350 | |||
2d6cce6b01 | |||
48faddebea | |||
a8039df22d | |||
e21ec0bd1f | |||
9c42a3d7cc | |||
54a16b2c9a | |||
a6f3068728 | |||
f04258ec04 | |||
8468e51c17 | |||
e9a2d3e151 | |||
1f56fa1b9b | |||
ccad00a094 | |||
51fecb01f5 | |||
c29de44d8c | |||
1d79832544 |
|
@ -57,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 */; };
|
||||
|
@ -86,8 +89,10 @@
|
|||
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 */; };
|
||||
9F8B92122BF77DBE003D37A2 /* AccountWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B920E2BF77DB4003D37A2 /* AccountWidgetConfiguration.swift */; };
|
||||
9F8B92132BF77DBE003D37A2 /* AccountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B92102BF77DBB003D37A2 /* AccountWidget.swift */; };
|
||||
9F8B92162BF77F0B003D37A2 /* AccountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B92142BF77F05003D37A2 /* AccountWidgetView.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, ); }; };
|
||||
|
@ -246,6 +251,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>"; };
|
||||
|
@ -270,8 +278,10 @@
|
|||
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>"; };
|
||||
9F8B920E2BF77DB4003D37A2 /* AccountWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWidgetConfiguration.swift; sourceTree = "<group>"; };
|
||||
9F8B92102BF77DBB003D37A2 /* AccountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWidget.swift; sourceTree = "<group>"; };
|
||||
9F8B92142BF77F05003D37A2 /* AccountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWidgetView.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>"; };
|
||||
|
@ -435,6 +445,7 @@
|
|||
9F37BDDA2BE36E22007F28AD /* PostIntent.swift */,
|
||||
9F37BDDE2BE37C35007F28AD /* TabIntent.swift */,
|
||||
9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */,
|
||||
9F5BE6252BF48FE10074387E /* ListEntity.swift */,
|
||||
);
|
||||
path = IceCubesAppIntents;
|
||||
sourceTree = "<group>";
|
||||
|
@ -463,6 +474,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 = (
|
||||
|
@ -483,6 +503,8 @@
|
|||
9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F8B920D2BF77DA8003D37A2 /* AccountWidget */,
|
||||
9F5BE6202BF48FB20074387E /* ListsWidget */,
|
||||
9FF2FB6B2BE8AE78001560CE /* MentionWidget */,
|
||||
9FF2FB642BE7F7FA001560CE /* Shared */,
|
||||
9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */,
|
||||
|
@ -495,6 +517,16 @@
|
|||
path = IceCubesAppWidgetsExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9F8B920D2BF77DA8003D37A2 /* AccountWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F8B920E2BF77DB4003D37A2 /* AccountWidgetConfiguration.swift */,
|
||||
9F8B92102BF77DBB003D37A2 /* AccountWidget.swift */,
|
||||
9F8B92142BF77F05003D37A2 /* AccountWidgetView.swift */,
|
||||
);
|
||||
path = AccountWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9FA0D2AC29921C1F008A143B /* Embeds */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -602,7 +634,6 @@
|
|||
9FAE4AC8293774FF00772766 /* Info.plist */,
|
||||
9F398AB429360A5800A889F2 /* App */,
|
||||
9F398AB529360A6100A889F2 /* Resources */,
|
||||
9FAD85822971BF7200496AB1 /* Secret.plist */,
|
||||
);
|
||||
path = IceCubesApp;
|
||||
sourceTree = "<group>";
|
||||
|
@ -960,7 +991,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 */,
|
||||
|
@ -995,17 +1025,23 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */,
|
||||
9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */,
|
||||
9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */,
|
||||
9F8B92162BF77F0B003D37A2 /* AccountWidgetView.swift in Sources */,
|
||||
9F8B92122BF77DBE003D37A2 /* AccountWidgetConfiguration.swift in Sources */,
|
||||
9F8B92132BF77DBE003D37A2 /* AccountWidget.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;
|
||||
};
|
||||
|
@ -1157,7 +1193,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.37;
|
||||
MARKETING_VERSION = 1.10.42;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1192,7 +1228,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.37;
|
||||
MARKETING_VERSION = 1.10.42;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1300,7 +1336,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.37;
|
||||
MARKETING_VERSION = 1.10.42;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1334,7 +1370,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.37;
|
||||
MARKETING_VERSION = 1.10.42;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1482,6 +1518,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";
|
||||
|
@ -1515,7 +1552,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.37;
|
||||
MARKETING_VERSION = 1.10.42;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||
PRODUCT_NAME = "Ice Cubes";
|
||||
SDKROOT = auto;
|
||||
|
@ -1537,6 +1574,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";
|
||||
|
@ -1570,7 +1608,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.37;
|
||||
MARKETING_VERSION = 1.10.42;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||
PRODUCT_NAME = "Ice Cubes";
|
||||
SDKROOT = auto;
|
||||
|
@ -1605,7 +1643,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.37;
|
||||
MARKETING_VERSION = 1.10.42;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1640,7 +1678,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.37;
|
||||
MARKETING_VERSION = 1.10.42;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
|
|
@ -4,6 +4,12 @@ import SwiftUI
|
|||
extension IceCubesApp {
|
||||
@CommandsBuilder
|
||||
var appMenu: some Commands {
|
||||
CommandGroup(replacing: .appSettings) {
|
||||
Button("menu.settings") {
|
||||
appRouterPath.presentedSheet = .settings
|
||||
}
|
||||
.keyboardShortcut(",", modifiers: .command)
|
||||
}
|
||||
CommandGroup(replacing: .newItem) {
|
||||
Button("menu.new-window") {
|
||||
openWindow(id: "MainWindow")
|
||||
|
|
|
@ -100,6 +100,7 @@ extension IceCubesApp {
|
|||
}
|
||||
}
|
||||
.withEnvironments()
|
||||
.environment(RouterPath())
|
||||
.withModelContainer()
|
||||
.applyTheme(theme)
|
||||
.frame(minWidth: 300, minHeight: 400)
|
||||
|
|
|
@ -13,6 +13,7 @@ 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
|
||||
|
@ -25,10 +26,12 @@ private struct SafariRouter: ViewModifier {
|
|||
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.handleDeepLink(url: url)
|
||||
|
|
|
@ -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,15 +185,22 @@ 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)
|
||||
|
@ -196,6 +233,7 @@ private struct SideBarIcon: View {
|
|||
self.isHovered = isHovered
|
||||
}
|
||||
}
|
||||
.frame(width: 50, height: 40)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ struct AccountSettingsView: View {
|
|||
Form {
|
||||
Section {
|
||||
Button {
|
||||
routerPath.presentedSheet = .accountFiltersList
|
||||
routerPath.presentedSheet = .accountEditInfo
|
||||
} label: {
|
||||
Label("account.action.edit-info", systemImage: "pencil")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
|
|
@ -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 {
|
||||
|
@ -64,6 +66,32 @@ struct SettingsTabs: View {
|
|||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
routerPath.client = client
|
||||
|
|
|
@ -11,16 +11,13 @@ 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
|
||||
|
@ -37,6 +34,7 @@ struct TranslationSettingsView: View {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
backgroundAPIKey
|
||||
autoDetectSection
|
||||
}
|
||||
.navigationTitle("settings.translation.navigation-title")
|
||||
|
@ -48,19 +46,39 @@ struct TranslationSettingsView: View {
|
|||
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() {
|
||||
|
|
|
@ -80,41 +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:
|
||||
Label("explore.section.trending.links", systemImage: iconName)
|
||||
"explore.section.trending.links"
|
||||
case .other:
|
||||
EmptyView()
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,21 @@ struct ToolbarTab: ToolbarContent {
|
|||
|
||||
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 ||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,8 +0,0 @@
|
|||
<?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>DEEPL_SECRET</key>
|
||||
<string>NICE_TRY_AGAIN</string>
|
||||
</dict>
|
||||
</plist>
|
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 []
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import WidgetKit
|
||||
|
||||
struct AccountWidgetProvider: AppIntentTimelineProvider {
|
||||
func placeholder(in _: Context) -> AccountWidgetEntry {
|
||||
.init(date: Date(), account: .placeholder(), avatar: nil)
|
||||
}
|
||||
|
||||
func snapshot(for configuration: AccountWidgetConfiguration, in context: Context) async -> AccountWidgetEntry {
|
||||
let account = await fetchAccount(configuration: configuration)
|
||||
return .init(date: Date(), account: account, avatar: nil)
|
||||
}
|
||||
|
||||
func timeline(for configuration: AccountWidgetConfiguration, in context: Context) async -> Timeline<AccountWidgetEntry> {
|
||||
let account = await fetchAccount(configuration: configuration)
|
||||
let images = try? await loadImages(urls: [account.avatar])
|
||||
return .init(entries: [.init(date: Date(), account: account, avatar: images?.first?.value)],
|
||||
policy: .atEnd)
|
||||
}
|
||||
|
||||
private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account {
|
||||
let client = Client(server: configuration.account.account.server,
|
||||
oauthToken: configuration.account.account.oauthToken)
|
||||
do {
|
||||
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
|
||||
return account
|
||||
} catch {
|
||||
return .placeholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AccountWidgetEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let account: Account
|
||||
let avatar: UIImage?
|
||||
}
|
||||
|
||||
struct AccountWidget: Widget {
|
||||
let kind: String = "AccountWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(kind: kind,
|
||||
intent: AccountWidgetConfiguration.self,
|
||||
provider: AccountWidgetProvider())
|
||||
{ entry in
|
||||
AccountWidgetView(entry: entry)
|
||||
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
|
||||
}
|
||||
.configurationDisplayName("Account")
|
||||
.description("Show information about your Mastodon account")
|
||||
.supportedFamilies([.systemSmall])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
AccountWidget()
|
||||
} timeline: {
|
||||
AccountWidgetEntry(date: Date(), account: .placeholder(), avatar: nil)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import AppIntents
|
||||
import WidgetKit
|
||||
|
||||
struct AccountWidgetConfiguration: WidgetConfigurationIntent {
|
||||
static let title: LocalizedStringResource = "Configuration"
|
||||
static let description = IntentDescription("Choose the account for this widget")
|
||||
|
||||
@Parameter(title: "Account")
|
||||
var account: AppAccountEntity
|
||||
}
|
||||
|
||||
extension AccountWidgetConfiguration {
|
||||
static var previewAccount: AccountWidgetConfiguration {
|
||||
let intent = AccountWidgetConfiguration()
|
||||
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
|
||||
return intent
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import WidgetKit
|
||||
|
||||
struct AccountWidgetView: View {
|
||||
var entry: AccountWidgetProvider.Entry
|
||||
|
||||
@Environment(\.widgetFamily) var family
|
||||
@Environment(\.redactionReasons) var redacted
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 4) {
|
||||
if let avatar = entry.avatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.frame(width: 64, height: 64)
|
||||
.clipShape(Circle())
|
||||
Text("\(entry.account.followersCount ?? 0)")
|
||||
.font(.title)
|
||||
.fontDesign(.rounded)
|
||||
.fontWeight(.bold)
|
||||
.monospacedDigit()
|
||||
Text("Followers")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@ struct IceCubesAppWidgetsExtensionBundle: WidgetBundle {
|
|||
var body: some Widget {
|
||||
LatestPostsWidget()
|
||||
HashtagPostsWidget()
|
||||
ListsPostWidget()
|
||||
MentionsWidget()
|
||||
AccountWidget()
|
||||
}
|
||||
}
|
||||
|
|
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
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ struct MentionsWidgetProvider: AppIntentTimelineProvider {
|
|||
oauthToken: configuration.account.account.oauthToken)
|
||||
var excludedTypes = Models.Notification.NotificationType.allCases
|
||||
excludedTypes.removeAll(where: { $0 == .mention })
|
||||
var notifications: [Models.Notification] =
|
||||
let notifications: [Models.Notification] =
|
||||
try await client.get(endpoint: Notifications.notifications(minId: nil,
|
||||
maxId: nil,
|
||||
types: excludedTypes.map(\.rawValue),
|
||||
|
@ -67,7 +67,7 @@ struct MentionsWidget: Widget {
|
|||
}
|
||||
.configurationDisplayName("Mentions")
|
||||
.description("Show the latest mentions for the selected account.")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
|
||||
.supportedFamilies([.systemLarge, .systemExtraLarge])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -136,15 +137,15 @@ public struct AccountDetailContextMenu: View {
|
|||
#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 {
|
||||
|
|
|
@ -24,6 +24,7 @@ public struct AccountDetailView: View {
|
|||
@State private var isCurrentUser: Bool = false
|
||||
@State private var showBlockConfirmation: Bool = false
|
||||
@State private var isEditingRelationshipNote: Bool = false
|
||||
@State private var showTranslateView: Bool = false
|
||||
|
||||
@State private var displayTitle: Bool = false
|
||||
|
||||
|
@ -285,7 +286,9 @@ public struct AccountDetailView: View {
|
|||
}
|
||||
|
||||
Menu {
|
||||
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, viewModel: viewModel)
|
||||
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
|
||||
showTranslateView: $showTranslateView,
|
||||
viewModel: viewModel)
|
||||
|
||||
if !viewModel.isCurrentUser {
|
||||
Button {
|
||||
|
@ -380,6 +383,9 @@ public struct AccountDetailView: View {
|
|||
} message: {
|
||||
Text("account.action.block-user-confirmation")
|
||||
}
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account?.note.asRawText ?? "")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -273,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)?
|
||||
|
@ -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),
|
||||
|
|
|
@ -124,16 +124,23 @@ 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:
|
||||
|
|
|
@ -50,7 +50,7 @@ public struct AppAccountsSelectorView: View {
|
|||
}
|
||||
.sheet(isPresented: $isPresented, content: {
|
||||
accountsView.presentationDetents([.height(preferredHeight), .large])
|
||||
.presentationBackground(.thinMaterial)
|
||||
.presentationBackground(.ultraThinMaterial)
|
||||
.presentationCornerRadius(16)
|
||||
.onAppear {
|
||||
refreshAccounts()
|
||||
|
@ -101,7 +101,7 @@ public struct AppAccountsSelectorView: View {
|
|||
#endif
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor.opacity(0.4))
|
||||
#endif
|
||||
|
||||
if accountCreationEnabled {
|
||||
|
@ -113,7 +113,7 @@ public struct AppAccountsSelectorView: View {
|
|||
#if os(visionOS)
|
||||
.foregroundStyle(theme.labelColor)
|
||||
#else
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor.opacity(0.4))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ public extension CGFloat {
|
|||
static let statusComponentSpacing: CGFloat = 6
|
||||
static let secondaryColumnWidth: CGFloat = 400
|
||||
static let sidebarWidth: CGFloat = 90
|
||||
static let sidebarWidthExpanded: CGFloat = 220
|
||||
static let pollBarHeight: CGFloat = 30
|
||||
}
|
||||
|
|
|
@ -23,22 +23,30 @@ public enum DeepLUserAPIHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public static func readIfAllowed() -> String? {
|
||||
guard UserPreferences.shared.alwaysUseDeepl else { return nil }
|
||||
public static func readKeyIfAllowed() -> String? {
|
||||
guard UserPreferences.shared.preferredTranslationType == .useDeepl else { return nil }
|
||||
|
||||
return readValue()
|
||||
return readKeyInternal()
|
||||
}
|
||||
|
||||
private static func readValue() -> String? {
|
||||
public static func readKey() -> String {
|
||||
return readKeyInternal() ?? ""
|
||||
}
|
||||
|
||||
private static func readKeyInternal() -> String? {
|
||||
keychain.synchronizable = true
|
||||
return keychain.get(key)
|
||||
}
|
||||
|
||||
public static func deactivateToggleIfNoKey() {
|
||||
UserPreferences.shared.alwaysUseDeepl = shouldAlwaysUseDeepl
|
||||
if UserPreferences.shared.preferredTranslationType == .useDeepl {
|
||||
if readKeyInternal() == nil {
|
||||
UserPreferences.shared.preferredTranslationType = .useServerIfPossible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static var shouldAlwaysUseDeepl: Bool {
|
||||
readIfAllowed() != nil
|
||||
readKeyIfAllowed() != nil
|
||||
}
|
||||
}
|
||||
|
|
15
Packages/Env/Sources/Env/Ext/TranslationView.swift
Normal file
15
Packages/Env/Sources/Env/Ext/TranslationView.swift
Normal file
|
@ -0,0 +1,15 @@
|
|||
import SwiftUI
|
||||
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
import Translation
|
||||
|
||||
extension View {
|
||||
public func addTranslateView(isPresented: Binding<Bool>, text: String) -> some View {
|
||||
if #available(iOS 17.4, *) {
|
||||
return self.translationPresentation(isPresented: isPresented, text: text)
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -115,6 +115,18 @@ public enum SheetDestination: Identifiable, Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
public enum SettingsStartingPoint {
|
||||
case display
|
||||
case haptic
|
||||
case remoteTimelines
|
||||
case tagGroups
|
||||
case recentTags
|
||||
case content
|
||||
case swipeActions
|
||||
case tabAndSidebarEntries
|
||||
case translation
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable public class RouterPath {
|
||||
public var client: Client?
|
||||
|
@ -123,6 +135,8 @@ public enum SheetDestination: Identifiable, Hashable {
|
|||
public var path: [RouterDestination] = []
|
||||
public var presentedSheet: SheetDestination?
|
||||
|
||||
public static var settingsStartingPoint: SettingsStartingPoint? = nil
|
||||
|
||||
public init() {}
|
||||
|
||||
public func navigate(to: RouterDestination) {
|
||||
|
|
18
Packages/Env/Sources/Env/TranslationType.swift
Normal file
18
Packages/Env/Sources/Env/TranslationType.swift
Normal file
|
@ -0,0 +1,18 @@
|
|||
import SwiftUI
|
||||
|
||||
public enum TranslationType: String, CaseIterable {
|
||||
case useServerIfPossible
|
||||
case useDeepl
|
||||
case useApple
|
||||
|
||||
public var description: LocalizedStringKey {
|
||||
switch self {
|
||||
case .useServerIfPossible:
|
||||
"Instance"
|
||||
case .useDeepl:
|
||||
"DeepL"
|
||||
case .useApple:
|
||||
"Apple Translate"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ import SwiftUI
|
|||
@AppStorage("app_require_alt_text") public var appRequireAltText = false
|
||||
@AppStorage("autoplay_video") public var autoPlayVideo = true
|
||||
@AppStorage("mute_video") public var muteVideo = true
|
||||
@AppStorage("always_use_deepl") public var alwaysUseDeepl = false
|
||||
@AppStorage("preferred_translation_type") public var preferredTranslationType = TranslationType.useServerIfPossible
|
||||
@AppStorage("user_deepl_api_free") public var userDeeplAPIFree = true
|
||||
@AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true
|
||||
|
||||
|
@ -61,7 +61,32 @@ import SwiftUI
|
|||
|
||||
@AppStorage("show_account_popover") public var showAccountPopover: Bool = true
|
||||
|
||||
init() {}
|
||||
@AppStorage("sidebar_expanded") public var isSidebarExpanded: Bool = false
|
||||
|
||||
init() {
|
||||
prepareTranslationType()
|
||||
}
|
||||
|
||||
private func prepareTranslationType() {
|
||||
let sharedDefault = UserDefaults.standard
|
||||
if let alwaysUseDeepl = (sharedDefault.object(forKey: "always_use_deepl") as? Bool) {
|
||||
if alwaysUseDeepl {
|
||||
preferredTranslationType = .useDeepl
|
||||
}
|
||||
sharedDefault.removeObject(forKey: "always_use_deepl")
|
||||
}
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
if #unavailable(iOS 17.4),
|
||||
preferredTranslationType == .useApple
|
||||
{
|
||||
preferredTranslationType = .useServerIfPossible
|
||||
}
|
||||
#else
|
||||
if preferredTranslationType == .useApple {
|
||||
preferredTranslationType = .useServerIfPossible
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public static let sharedDefault = UserDefaults(suiteName: "group.com.thomasricouard.IceCubesApp")
|
||||
|
@ -183,9 +208,9 @@ import SwiftUI
|
|||
}
|
||||
}
|
||||
|
||||
public var alwaysUseDeepl: Bool {
|
||||
public var preferredTranslationType: TranslationType {
|
||||
didSet {
|
||||
storage.alwaysUseDeepl = alwaysUseDeepl
|
||||
storage.preferredTranslationType = preferredTranslationType
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,6 +352,12 @@ import SwiftUI
|
|||
}
|
||||
}
|
||||
|
||||
public var isSidebarExpanded: Bool {
|
||||
didSet {
|
||||
storage.isSidebarExpanded = isSidebarExpanded
|
||||
}
|
||||
}
|
||||
|
||||
public func getRealMaxIndent() -> UInt {
|
||||
showReplyIndentation ? maxReplyIndentation : 0
|
||||
}
|
||||
|
@ -474,7 +505,7 @@ import SwiftUI
|
|||
appDefaultPostsSensitive = storage.appDefaultPostsSensitive
|
||||
appRequireAltText = storage.appRequireAltText
|
||||
autoPlayVideo = storage.autoPlayVideo
|
||||
alwaysUseDeepl = storage.alwaysUseDeepl
|
||||
preferredTranslationType = storage.preferredTranslationType
|
||||
userDeeplAPIFree = storage.userDeeplAPIFree
|
||||
autoDetectPostLanguage = storage.autoDetectPostLanguage
|
||||
inAppBrowserReaderView = storage.inAppBrowserReaderView
|
||||
|
@ -501,6 +532,7 @@ import SwiftUI
|
|||
showReplyIndentation = storage.showReplyIndentation
|
||||
showAccountPopover = storage.showAccountPopover
|
||||
muteVideo = storage.muteVideo
|
||||
isSidebarExpanded = storage.isSidebarExpanded
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
|
|||
_ = text.removeLast()
|
||||
_ = text.removeLast()
|
||||
}
|
||||
asRawText = text
|
||||
asRawText = (try? Entities.unescape(text)) ?? text
|
||||
|
||||
if asMarkdown.hasPrefix("\n") {
|
||||
_ = asMarkdown.removeFirst()
|
||||
|
@ -175,7 +175,9 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
|
|||
return
|
||||
} else if node.nodeName() == "#text" {
|
||||
var txt = node.description
|
||||
|
||||
|
||||
txt = (try? Entities.unescape(txt)) ?? txt
|
||||
|
||||
if let underscore_regex, let main_regex {
|
||||
// This is the markdown escaper
|
||||
txt = main_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1")
|
||||
|
|
|
@ -12,20 +12,8 @@ public struct DeepLClient: Sendable {
|
|||
"https://api\(deeplUserAPIFree && (deeplUserAPIKey != nil) ? "-free" : "").deepl.com/v2/translate"
|
||||
}
|
||||
|
||||
private var APIKey: String {
|
||||
if let deeplUserAPIKey {
|
||||
return deeplUserAPIKey
|
||||
}
|
||||
|
||||
if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") {
|
||||
let secret = NSDictionary(contentsOfFile: path)
|
||||
return secret?["DEEPL_SECRET"] as? String ?? ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private var authorizationHeaderValue: String {
|
||||
"DeepL-Auth-Key \(APIKey)"
|
||||
"DeepL-Auth-Key \(deeplUserAPIKey ?? "")"
|
||||
}
|
||||
|
||||
public struct Response: Decodable {
|
||||
|
@ -49,26 +37,22 @@ public struct DeepLClient: Sendable {
|
|||
}
|
||||
|
||||
public func request(target: String, text: String) async throws -> Translation {
|
||||
do {
|
||||
var components = URLComponents(string: endpoint)!
|
||||
var queryItems: [URLQueryItem] = []
|
||||
queryItems.append(.init(name: "text", value: text))
|
||||
queryItems.append(.init(name: "target_lang", value: target.uppercased()))
|
||||
components.queryItems = queryItems
|
||||
var request = URLRequest(url: components.url!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
let (result, _) = try await URLSession.shared.data(for: request)
|
||||
let response = try decoder.decode(Response.self, from: result)
|
||||
if let translation = response.translations.first {
|
||||
return .init(content: translation.text.removingPercentEncoding ?? "",
|
||||
detectedSourceLanguage: translation.detectedSourceLanguage,
|
||||
provider: "DeepL.com")
|
||||
}
|
||||
throw DeepLError.notFound
|
||||
} catch {
|
||||
throw error
|
||||
var components = URLComponents(string: endpoint)!
|
||||
var queryItems: [URLQueryItem] = []
|
||||
queryItems.append(.init(name: "text", value: text))
|
||||
queryItems.append(.init(name: "target_lang", value: target.uppercased()))
|
||||
components.queryItems = queryItems
|
||||
var request = URLRequest(url: components.url!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
let (result, _) = try await URLSession.shared.data(for: request)
|
||||
let response = try decoder.decode(Response.self, from: result)
|
||||
if let translation = response.translations.first {
|
||||
return .init(content: translation.text.removingPercentEncoding ?? "",
|
||||
detectedSourceLanguage: translation.detectedSourceLanguage,
|
||||
provider: "DeepL.com")
|
||||
}
|
||||
throw DeepLError.notFound
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ public struct OpenAIClient {
|
|||
public let content: String
|
||||
}
|
||||
|
||||
let model = "gpt-3.5-turbo"
|
||||
let model = "gpt-4o"
|
||||
let messages: [Message]
|
||||
|
||||
let temperature: CGFloat
|
||||
|
@ -52,7 +52,7 @@ public struct OpenAIClient {
|
|||
public let content: [MessageContent]
|
||||
}
|
||||
|
||||
let model = "gpt-4-vision-preview"
|
||||
let model = "gpt-4o"
|
||||
let messages: [Message]
|
||||
let maxTokens = 50
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ struct NotificationsPolicyView: View {
|
|||
})
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor.opacity(0.3))
|
||||
#endif
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
|
|
|
@ -23,7 +23,7 @@ extension StatusEditor {
|
|||
@State private var didAppear: Bool = false
|
||||
@State private var isGeneratingDescription: Bool = false
|
||||
|
||||
@State private var showTranslateButton: Bool = false
|
||||
@State private var showTranslateView: Bool = false
|
||||
@State private var isTranslating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
|
@ -34,8 +34,14 @@ extension StatusEditor {
|
|||
text: $imageDescription,
|
||||
axis: .vertical)
|
||||
.focused($isFieldFocused)
|
||||
generateButton
|
||||
translateButton
|
||||
if imageDescription.isEmpty {
|
||||
generateButton
|
||||
}
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
if #available(iOS 17.4, *), !imageDescription.isEmpty {
|
||||
translateButton
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
Section {
|
||||
|
@ -111,12 +117,6 @@ extension StatusEditor {
|
|||
Task {
|
||||
if let description = await generateDescription(url: url) {
|
||||
imageDescription = description
|
||||
let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
|
||||
if lang != nil, lang != "en" {
|
||||
withAnimation {
|
||||
showTranslateButton = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
|
@ -131,24 +131,18 @@ extension StatusEditor {
|
|||
|
||||
@ViewBuilder
|
||||
private var translateButton: some View {
|
||||
if showTranslateButton {
|
||||
Button {
|
||||
Task {
|
||||
if let description = await translateDescription() {
|
||||
imageDescription = description
|
||||
withAnimation {
|
||||
showTranslateButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if isTranslating {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("status.action.translate")
|
||||
}
|
||||
Button {
|
||||
showTranslateView = true
|
||||
} label: {
|
||||
if isTranslating {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("status.action.translate")
|
||||
}
|
||||
}
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
.addTranslateView(isPresented: $showTranslateView, text: imageDescription)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func generateDescription(url: URL) async -> String? {
|
||||
|
@ -158,17 +152,5 @@ extension StatusEditor {
|
|||
isGeneratingDescription = false
|
||||
return response?.trimmedText
|
||||
}
|
||||
|
||||
private func translateDescription() async -> String? {
|
||||
isTranslating = true
|
||||
let userAPIKey = DeepLUserAPIHandler.readIfAllowed()
|
||||
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
|
||||
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
|
||||
let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
|
||||
guard let lang else { return nil }
|
||||
let translation = try? await deeplClient.request(target: lang, text: imageDescription)
|
||||
isTranslating = false
|
||||
return translation?.content.asRawText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,9 +45,7 @@ extension TextView.Representable {
|
|||
textView.allowsEditingTextAttributes = false
|
||||
textView.returnKeyType = .default
|
||||
textView.allowsEditingTextAttributes = true
|
||||
#if targetEnvironment(macCatalyst)
|
||||
textView.inlinePredictionType = .no
|
||||
#endif
|
||||
textView.inlinePredictionType = .no
|
||||
|
||||
self.getTextView?(textView)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ public struct StatusRowView: View {
|
|||
@Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled
|
||||
@Environment(\.isStatusFocused) private var isFocused
|
||||
@Environment(\.indentationLevel) private var indentationLevel
|
||||
@Environment(RouterPath.self) private var routerPath: RouterPath
|
||||
|
||||
@Environment(QuickLook.self) private var quickLook
|
||||
@Environment(Theme.self) private var theme
|
||||
|
@ -219,6 +220,23 @@ public struct StatusRowView: View {
|
|||
StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus,
|
||||
client: viewModel.client)
|
||||
)
|
||||
.alert("DeepL couldn't be reached!\nIs the API Key correct?", isPresented: $viewModel.deeplTranslationError) {
|
||||
Button("alert.button.ok", role: .cancel) {}
|
||||
Button("settings.general.translate") {
|
||||
RouterPath.settingsStartingPoint = .translation
|
||||
routerPath.presentedSheet = .settings
|
||||
}
|
||||
}
|
||||
.alert("The Translation Service of your Instance couldn't be reached!", isPresented: $viewModel.instanceTranslationError) {
|
||||
Button("alert.button.ok", role: .cancel) {}
|
||||
Button("settings.general.translate") {
|
||||
RouterPath.settingsStartingPoint = .translation
|
||||
routerPath.presentedSheet = .settings
|
||||
}
|
||||
}
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
.addTranslateView(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
@ -31,6 +31,18 @@ import SwiftUI
|
|||
var translation: Translation?
|
||||
var isLoadingTranslation: Bool = false
|
||||
var showDeleteAlert: Bool = false
|
||||
var showAppleTranslation = false
|
||||
var preferredTranslationType = TranslationType.useServerIfPossible {
|
||||
didSet {
|
||||
if oldValue != preferredTranslationType {
|
||||
translation = nil
|
||||
showAppleTranslation = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var deeplTranslationError = false
|
||||
var instanceTranslationError = false
|
||||
|
||||
private(set) var actionsAccountsFetched: Bool = false
|
||||
var favoriters: [Account] = []
|
||||
|
@ -297,25 +309,43 @@ import SwiftUI
|
|||
}
|
||||
|
||||
func translate(userLang: String) async {
|
||||
withAnimation {
|
||||
isLoadingTranslation = true
|
||||
}
|
||||
if !alwaysTranslateWithDeepl {
|
||||
do {
|
||||
// We first use instance translation API if available.
|
||||
let translation: Translation = try await client.post(endpoint: Statuses.translate(id: finalStatus.id,
|
||||
lang: userLang))
|
||||
withAnimation {
|
||||
self.translation = translation
|
||||
isLoadingTranslation = false
|
||||
}
|
||||
|
||||
return
|
||||
} catch {}
|
||||
updatePreferredTranslation()
|
||||
if preferredTranslationType == .useApple {
|
||||
showAppleTranslation = true
|
||||
return
|
||||
}
|
||||
|
||||
// If not or fail we use Ice Cubes own DeepL client.
|
||||
await translateWithDeepL(userLang: userLang)
|
||||
if preferredTranslationType != .useDeepl {
|
||||
await translateWithInstance(userLang: userLang)
|
||||
|
||||
if translation == nil {
|
||||
await translateWithDeepL(userLang: userLang)
|
||||
}
|
||||
} else {
|
||||
await translateWithDeepL(userLang: userLang)
|
||||
|
||||
if translation == nil {
|
||||
await translateWithInstance(userLang: userLang)
|
||||
}
|
||||
}
|
||||
|
||||
var hasShown = false
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
if translation == nil,
|
||||
#available(iOS 17.4, *) {
|
||||
showAppleTranslation = true
|
||||
hasShown = true
|
||||
}
|
||||
#endif
|
||||
|
||||
if !hasShown,
|
||||
translation == nil {
|
||||
if preferredTranslationType == .useDeepl {
|
||||
deeplTranslationError = true
|
||||
} else {
|
||||
instanceTranslationError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func translateWithDeepL(userLang: String) async {
|
||||
|
@ -331,6 +361,20 @@ import SwiftUI
|
|||
isLoadingTranslation = false
|
||||
}
|
||||
}
|
||||
|
||||
func translateWithInstance(userLang: String) async {
|
||||
withAnimation {
|
||||
isLoadingTranslation = true
|
||||
}
|
||||
|
||||
let translation: Translation? = try? await client.post(endpoint: Statuses.translate(id: finalStatus.id,
|
||||
lang: userLang))
|
||||
|
||||
withAnimation {
|
||||
self.translation = translation
|
||||
isLoadingTranslation = false
|
||||
}
|
||||
}
|
||||
|
||||
private func getDeepLClient() -> DeepLClient {
|
||||
let userAPIfree = UserPreferences.shared.userDeeplAPIFree
|
||||
|
@ -339,11 +383,17 @@ import SwiftUI
|
|||
}
|
||||
|
||||
private var userAPIKey: String? {
|
||||
DeepLUserAPIHandler.readIfAllowed()
|
||||
DeepLUserAPIHandler.readKey()
|
||||
}
|
||||
|
||||
var alwaysTranslateWithDeepl: Bool {
|
||||
DeepLUserAPIHandler.shouldAlwaysUseDeepl
|
||||
func updatePreferredTranslation() {
|
||||
if DeepLUserAPIHandler.shouldAlwaysUseDeepl {
|
||||
preferredTranslationType = .useDeepl
|
||||
} else if UserPreferences.shared.preferredTranslationType == .useApple {
|
||||
preferredTranslationType = .useApple
|
||||
} else {
|
||||
preferredTranslationType = .useServerIfPossible
|
||||
}
|
||||
}
|
||||
|
||||
func fetchRemoteStatus() async -> Bool {
|
||||
|
|
|
@ -31,7 +31,7 @@ struct StatusRowContextMenu: View {
|
|||
}
|
||||
|
||||
if statusDataController.isReblogged {
|
||||
return Label("status.action.unboost", image: "Rocket.fill")
|
||||
return Label("status.action.unboost", image: "Rocket.Fill")
|
||||
}
|
||||
return Label("status.action.boost", image: "Rocket")
|
||||
}
|
||||
|
|
|
@ -258,6 +258,7 @@ struct AltTextButton: View {
|
|||
@Environment(Theme.self) private var theme
|
||||
|
||||
@State private var isDisplayingAlert = false
|
||||
@State private var isDisplayingTranslation = false
|
||||
|
||||
var body: some View {
|
||||
if !isInCaptureMode,
|
||||
|
@ -278,6 +279,9 @@ struct AltTextButton: View {
|
|||
.buttonStyle(.borderless)
|
||||
.padding(EdgeInsets(top: 5, leading: 7, bottom: 5, trailing: 7))
|
||||
.background(.thinMaterial)
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
.addTranslateView(isPresented: $isDisplayingTranslation, text: text)
|
||||
#endif
|
||||
#if os(visionOS)
|
||||
.clipShape(Capsule())
|
||||
#endif
|
||||
|
@ -288,6 +292,12 @@ struct AltTextButton: View {
|
|||
isPresented: $isDisplayingAlert
|
||||
) {
|
||||
Button("alert.button.ok", action: {})
|
||||
Button("status.action.copy-text", action: { UIPasteboard.general.string = text })
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
if #available(iOS 17.4, *) {
|
||||
Button("status.action.translate", action: { isDisplayingTranslation = true })
|
||||
}
|
||||
#endif
|
||||
} message: {
|
||||
Text(text)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,8 @@ struct StatusRowTranslateView: View {
|
|||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ViewBuilder
|
||||
var translateButton: some View {
|
||||
if !isInCaptureMode,
|
||||
!isCompact,
|
||||
let userLang = preferences.serverPreferences?.postLanguage,
|
||||
|
@ -54,8 +55,22 @@ struct StatusRowTranslateView: View {
|
|||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
|
||||
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
|
||||
@ViewBuilder
|
||||
var generalTranslateButton: some View {
|
||||
translateButton
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
generalTranslateButton
|
||||
.onChange(of: preferences.preferredTranslationType) { _, _ in
|
||||
withAnimation {
|
||||
_ = viewModel.updatePreferredTranslation()
|
||||
}
|
||||
}
|
||||
|
||||
if let translation = viewModel.translation, !viewModel.isLoadingTranslation, preferences.preferredTranslationType != .useApple {
|
||||
GroupBox {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(translation.content.asSafeMarkdownAttributedString)
|
||||
|
|
|
@ -30,7 +30,7 @@ public struct TimelineContentFilterView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor.opacity(0.3))
|
||||
#endif
|
||||
|
||||
Section {
|
||||
|
@ -46,7 +46,7 @@ public struct TimelineContentFilterView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor.opacity(0.3))
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("timeline.content-filter.title")
|
||||
|
|
Loading…
Reference in a new issue