mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-06-03 05:49:25 +00:00
Merge remote-tracking branch 'upstream/main' into zh-Hant-localization
This commit is contained in:
commit
45878a4d91
|
@ -940,7 +940,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.10.32;
|
MARKETING_VERSION = 1.10.33;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -975,7 +975,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.10.32;
|
MARKETING_VERSION = 1.10.33;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -1011,7 +1011,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.10.32;
|
MARKETING_VERSION = 1.10.33;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -1045,7 +1045,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.10.32;
|
MARKETING_VERSION = 1.10.33;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -1225,7 +1225,7 @@
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 1.10.32;
|
MARKETING_VERSION = 1.10.33;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||||
PRODUCT_NAME = "Ice Cubes";
|
PRODUCT_NAME = "Ice Cubes";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
|
@ -1279,7 +1279,7 @@
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 1.10.32;
|
MARKETING_VERSION = 1.10.33;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||||
PRODUCT_NAME = "Ice Cubes";
|
PRODUCT_NAME = "Ice Cubes";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
|
@ -1314,7 +1314,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.10.32;
|
MARKETING_VERSION = 1.10.33;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -1349,7 +1349,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.10.32;
|
MARKETING_VERSION = 1.10.33;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|
|
@ -68,8 +68,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/kean/Nuke",
|
"location" : "https://github.com/kean/Nuke",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "15fde63470d782c897816a74bdd516a907e33147",
|
"revision" : "8ecbfc886da39bccb01c34abef5f2ff4073ad633",
|
||||||
"version" : "12.3.0"
|
"version" : "12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -140,6 +140,12 @@ extension View {
|
||||||
.presentationDetents([.medium])
|
.presentationDetents([.medium])
|
||||||
.presentationBackground(.thinMaterial)
|
.presentationBackground(.thinMaterial)
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
|
case .accountEditInfo:
|
||||||
|
EditAccountView()
|
||||||
|
.withEnvironments()
|
||||||
|
case .accountFiltersList:
|
||||||
|
FiltersListView()
|
||||||
|
.withEnvironments()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,29 +17,29 @@ struct AppView: View {
|
||||||
@Environment(UserPreferences.self) private var userPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(StreamWatcher.self) private var watcher
|
@Environment(StreamWatcher.self) private var watcher
|
||||||
|
|
||||||
@Environment(\.openWindow) var openWindow
|
@Environment(\.openWindow) var openWindow
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
@Binding var selectedTab: Tab
|
@Binding var selectedTab: Tab
|
||||||
@Binding var appRouterPath: RouterPath
|
@Binding var appRouterPath: RouterPath
|
||||||
|
|
||||||
@State var popToRootTab: Tab = .other
|
@State var popToRootTab: Tab = .other
|
||||||
@State var iosTabs = iOSTabs.shared
|
@State var iosTabs = iOSTabs.shared
|
||||||
@State var sidebarTabs = SidebarTabs.shared
|
@State var sidebarTabs = SidebarTabs.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
tabBarView
|
|
||||||
#else
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
|
||||||
sidebarView
|
|
||||||
} else {
|
|
||||||
tabBarView
|
tabBarView
|
||||||
}
|
#else
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
sidebarView
|
||||||
|
} else {
|
||||||
|
tabBarView
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
var availableTabs: [Tab] {
|
var availableTabs: [Tab] {
|
||||||
guard appAccountsManager.currentClient.isAuth else {
|
guard appAccountsManager.currentClient.isAuth else {
|
||||||
return Tab.loggedOutTab()
|
return Tab.loggedOutTab()
|
||||||
|
@ -49,7 +49,7 @@ struct AppView: View {
|
||||||
} else if UIDevice.current.userInterfaceIdiom == .vision {
|
} else if UIDevice.current.userInterfaceIdiom == .vision {
|
||||||
return Tab.visionOSTab()
|
return Tab.visionOSTab()
|
||||||
}
|
}
|
||||||
return sidebarTabs.tabs.map{ $0.tab }
|
return sidebarTabs.tabs.map { $0.tab }
|
||||||
}
|
}
|
||||||
|
|
||||||
var tabBarView: some View {
|
var tabBarView: some View {
|
||||||
|
@ -58,9 +58,9 @@ struct AppView: View {
|
||||||
}, set: { newTab in
|
}, set: { newTab in
|
||||||
if newTab == .post {
|
if newTab == .post {
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||||
#else
|
#else
|
||||||
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||||
#endif
|
#endif
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -104,40 +104,40 @@ struct AppView: View {
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
var sidebarView: some View {
|
var sidebarView: some View {
|
||||||
SideBarView(selectedTab: $selectedTab,
|
SideBarView(selectedTab: $selectedTab,
|
||||||
popToRootTab: $popToRootTab,
|
popToRootTab: $popToRootTab,
|
||||||
tabs: availableTabs)
|
tabs: availableTabs)
|
||||||
{
|
{
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
TabView(selection: $selectedTab) {
|
TabView(selection: $selectedTab) {
|
||||||
ForEach(availableTabs) { tab in
|
ForEach(availableTabs) { tab in
|
||||||
tab
|
tab
|
||||||
.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
|
.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
tab.label
|
tab.label
|
||||||
}
|
}
|
||||||
.tag(tab)
|
.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
|
#endif
|
||||||
|
|
||||||
var notificationsSecondaryColumn: some View {
|
var notificationsSecondaryColumn: some View {
|
||||||
|
|
|
@ -26,10 +26,10 @@ struct IceCubesApp: App {
|
||||||
@State var watcher = StreamWatcher.shared
|
@State var watcher = StreamWatcher.shared
|
||||||
@State var quickLook = QuickLook.shared
|
@State var quickLook = QuickLook.shared
|
||||||
@State var theme = Theme.shared
|
@State var theme = Theme.shared
|
||||||
|
|
||||||
@State var selectedTab: Tab = .timeline
|
@State var selectedTab: Tab = .timeline
|
||||||
@State var appRouterPath = RouterPath()
|
@State var appRouterPath = RouterPath()
|
||||||
|
|
||||||
@State var isSupporter: Bool = false
|
@State var isSupporter: Bool = false
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
|
|
@ -36,37 +36,37 @@ public struct ReportView: View {
|
||||||
.navigationTitle("report.title")
|
.navigationTitle("report.title")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
isSendingReport = true
|
isSendingReport = true
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let _: ReportSent =
|
let _: ReportSent =
|
||||||
try await client.post(endpoint: Statuses.report(accountId: status.account.id,
|
try await client.post(endpoint: Statuses.report(accountId: status.account.id,
|
||||||
statusId: status.id,
|
statusId: status.id,
|
||||||
comment: commentText))
|
comment: commentText))
|
||||||
dismiss()
|
dismiss()
|
||||||
isSendingReport = false
|
isSendingReport = false
|
||||||
} catch {
|
} catch {
|
||||||
isSendingReport = false
|
isSendingReport = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if isSendingReport {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Text("report.action.send")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
|
||||||
if isSendingReport {
|
|
||||||
ProgressView()
|
|
||||||
} else {
|
|
||||||
Text("report.action.send")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
CancelToolbarItem()
|
CancelToolbarItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ private struct SafariRouter: ViewModifier {
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
@State private var safariManager = InAppSafariManager()
|
@State private var safariManager = InAppSafariManager()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
|
@ -52,78 +52,78 @@ private struct SafariRouter: ViewModifier {
|
||||||
return .systemAction
|
return .systemAction
|
||||||
}
|
}
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
return .systemAction
|
return .systemAction
|
||||||
#else
|
#else
|
||||||
return safariManager.open(url)
|
return safariManager.open(url)
|
||||||
#endif
|
#endif
|
||||||
#else
|
#else
|
||||||
return .systemAction
|
return .systemAction
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.background {
|
.background {
|
||||||
WindowReader { window in
|
WindowReader { window in
|
||||||
safariManager.windowScene = window.windowScene
|
safariManager.windowScene = window.windowScene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
@MainActor
|
|
||||||
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
|
|
||||||
var windowScene: UIWindowScene?
|
|
||||||
let viewController: UIViewController = .init()
|
|
||||||
var window: UIWindow?
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func open(_ url: URL) -> OpenURLAction.Result {
|
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
|
||||||
guard let windowScene else { return .systemAction }
|
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()
|
window = setupWindow(windowScene: windowScene)
|
||||||
configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView
|
|
||||||
|
|
||||||
let safari = SFSafariViewController(url: url, configuration: configuration)
|
let configuration = SFSafariViewController.Configuration()
|
||||||
safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor)
|
configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView
|
||||||
safari.preferredControlTintColor = UIColor(Theme.shared.tintColor)
|
|
||||||
safari.delegate = self
|
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
let safari = SFSafariViewController(url: url, configuration: configuration)
|
||||||
self?.viewController.present(safari, animated: true)
|
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 {
|
window.rootViewController = viewController
|
||||||
let window = window ?? UIWindow(windowScene: windowScene)
|
window.makeKeyAndVisible()
|
||||||
|
|
||||||
window.rootViewController = viewController
|
switch Theme.shared.selectedScheme {
|
||||||
window.makeKeyAndVisible()
|
case .dark:
|
||||||
|
window.overrideUserInterfaceStyle = .dark
|
||||||
|
case .light:
|
||||||
|
window.overrideUserInterfaceStyle = .light
|
||||||
|
}
|
||||||
|
|
||||||
switch Theme.shared.selectedScheme {
|
self.window = window
|
||||||
case .dark:
|
return window
|
||||||
window.overrideUserInterfaceStyle = .dark
|
|
||||||
case .light:
|
|
||||||
window.overrideUserInterfaceStyle = .light
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.window = window
|
nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) {
|
||||||
return window
|
Task { @MainActor in
|
||||||
}
|
window?.resignKey()
|
||||||
|
window?.isHidden = false
|
||||||
nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) {
|
window = nil
|
||||||
Task { @MainActor in
|
}
|
||||||
window?.resignKey()
|
|
||||||
window?.isHidden = false
|
|
||||||
window = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private struct WindowReader: UIViewRepresentable {
|
private struct WindowReader: UIViewRepresentable {
|
||||||
|
|
|
@ -17,12 +17,12 @@ struct SideBarView<Content: View>: View {
|
||||||
@Environment(StreamWatcher.self) private var watcher
|
@Environment(StreamWatcher.self) private var watcher
|
||||||
@Environment(UserPreferences.self) private var userPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
|
||||||
@Binding var selectedTab: Tab
|
@Binding var selectedTab: Tab
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
var tabs: [Tab]
|
var tabs: [Tab]
|
||||||
@ViewBuilder var content: () -> Content
|
@ViewBuilder var content: () -> Content
|
||||||
|
|
||||||
@State private var sidebarTabs = SidebarTabs.shared
|
@State private var sidebarTabs = SidebarTabs.shared
|
||||||
|
|
||||||
private func badgeFor(tab: Tab) -> Int {
|
private func badgeFor(tab: Tab) -> Int {
|
||||||
|
@ -166,7 +166,7 @@ struct SideBarView<Content: View>: View {
|
||||||
.frame(width: .sidebarWidth)
|
.frame(width: .sidebarWidth)
|
||||||
.background(.thinMaterial)
|
.background(.thinMaterial)
|
||||||
})
|
})
|
||||||
Divider().edgesIgnoringSafeArea(.all)
|
Divider().edgesIgnoringSafeArea(.all)
|
||||||
}
|
}
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import AppAccount
|
import AppAccount
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct NavigationSheet<Content: View>: View {
|
struct NavigationSheet<Content: View>: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
var content: () -> Content
|
var content: () -> Content
|
||||||
|
|
||||||
init(@ViewBuilder content: @escaping () -> Content) {
|
init(@ViewBuilder content: @escaping () -> Content) {
|
||||||
self.content = content
|
self.content = content
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
content()
|
content()
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import AppAccount
|
import AppAccount
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct NavigationTab<Content: View>: View {
|
struct NavigationTab<Content: View>: View {
|
||||||
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
||||||
|
|
||||||
@Environment(AppAccountsManager.self) private var appAccount
|
@Environment(AppAccountsManager.self) private var appAccount
|
||||||
@Environment(CurrentAccount.self) private var currentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@Environment(UserPreferences.self) private var userPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(Client.self) private var client
|
@Environment(Client.self) private var client
|
||||||
|
|
||||||
var content: () -> Content
|
var content: () -> Content
|
||||||
|
|
||||||
@State private var routerPath = RouterPath()
|
@State private var routerPath = RouterPath()
|
||||||
|
|
||||||
init(@ViewBuilder content: @escaping () -> Content) {
|
init(@ViewBuilder content: @escaping () -> Content) {
|
||||||
self.content = content
|
self.content = content
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routerPath.path) {
|
NavigationStack(path: $routerPath.path) {
|
||||||
content()
|
content()
|
||||||
|
|
|
@ -21,7 +21,7 @@ struct NotificationsTab: View {
|
||||||
@Environment(PushNotificationsService.self) private var pushNotificationsService
|
@Environment(PushNotificationsService.self) private var pushNotificationsService
|
||||||
@State private var routerPath = RouterPath()
|
@State private var routerPath = RouterPath()
|
||||||
@State private var scrollToTopSignal: Int = 0
|
@State private var scrollToTopSignal: Int = 0
|
||||||
|
|
||||||
@Binding var selectedTab: Tab
|
@Binding var selectedTab: Tab
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
|
|
||||||
|
@ -60,9 +60,9 @@ struct NotificationsTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: selectedTab, { _, newValue in
|
.onChange(of: selectedTab) { _, _ in
|
||||||
clearNotifications()
|
clearNotifications()
|
||||||
})
|
}
|
||||||
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
|
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
|
||||||
if let newValue, let type = newValue.notification.supportedType {
|
if let newValue, let type = newValue.notification.supportedType {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
|
|
|
@ -28,26 +28,26 @@ struct AboutView: View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Image(uiImage: .init(named: "AppIconAlternate0")!)
|
Image(uiImage: .init(named: "AppIconAlternate0")!)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
Image(uiImage: .init(named: "AppIconAlternate4")!)
|
Image(uiImage: .init(named: "AppIconAlternate4")!)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
Image(uiImage: .init(named: "AppIconAlternate17")!)
|
Image(uiImage: .init(named: "AppIconAlternate17")!)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
Image(uiImage: .init(named: "AppIconAlternate23")!)
|
Image(uiImage: .init(named: "AppIconAlternate23")!)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
|
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
|
||||||
Label("settings.support.privacy-policy", systemImage: "lock")
|
Label("settings.support.privacy-policy", systemImage: "lock")
|
||||||
|
@ -107,14 +107,14 @@ struct AboutView: View {
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.navigationTitle(Text("settings.about.title"))
|
.navigationTitle(Text("settings.about.title"))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
.environment(\.openURL, OpenURLAction { url in
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
routerPath.handle(url: url)
|
routerPath.handle(url: url)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -17,9 +17,8 @@ struct AccountSettingsView: View {
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(AppAccountsManager.self) private var appAccountsManager
|
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||||
@Environment(Client.self) private var client
|
@Environment(Client.self) private var client
|
||||||
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
|
||||||
@State private var isEditingAccount: Bool = false
|
|
||||||
@State private var isEditingFilters: Bool = false
|
|
||||||
@State private var cachedPostsCount: Int = 0
|
@State private var cachedPostsCount: Int = 0
|
||||||
@State private var timelineCache = TimelineCache()
|
@State private var timelineCache = TimelineCache()
|
||||||
|
|
||||||
|
@ -30,7 +29,7 @@ struct AccountSettingsView: View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
isEditingAccount = true
|
routerPath.presentedSheet = .accountFiltersList
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.edit-info", systemImage: "pencil")
|
Label("account.action.edit-info", systemImage: "pencil")
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
@ -40,7 +39,7 @@ struct AccountSettingsView: View {
|
||||||
|
|
||||||
if currentInstance.isFiltersSupported {
|
if currentInstance.isFiltersSupported {
|
||||||
Button {
|
Button {
|
||||||
isEditingFilters = true
|
routerPath.presentedSheet = .accountFiltersList
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
@ -96,12 +95,6 @@ struct AccountSettingsView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isEditingAccount, content: {
|
|
||||||
EditAccountView()
|
|
||||||
})
|
|
||||||
.sheet(isPresented: $isEditingFilters, content: {
|
|
||||||
FiltersListView()
|
|
||||||
})
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -116,8 +109,8 @@ struct AccountSettingsView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle(account.safeDisplayName)
|
.navigationTitle(account.safeDisplayName)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,75 +82,75 @@ struct AddAccountView: View {
|
||||||
.navigationTitle("account.add.navigation-title")
|
.navigationTitle("account.add.navigation-title")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
if !appAccountsManager.availableAccounts.isEmpty {
|
if !appAccountsManager.availableAccounts.isEmpty {
|
||||||
CancelToolbarItem()
|
CancelToolbarItem()
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
isInstanceURLFieldFocused = true
|
|
||||||
Task {
|
|
||||||
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
|
||||||
withAnimation {
|
|
||||||
self.instances = instances
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isSigninIn = false
|
.onAppear {
|
||||||
}
|
isInstanceURLFieldFocused = true
|
||||||
.onChange(of: instanceName) {
|
Task {
|
||||||
searchingTask.cancel()
|
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||||
searchingTask = Task {
|
withAnimation {
|
||||||
try? await Task.sleep(for: .seconds(0.1))
|
self.instances = instances
|
||||||
guard !Task.isCancelled else { return }
|
}
|
||||||
|
}
|
||||||
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
isSigninIn = false
|
||||||
withAnimation {
|
}
|
||||||
self.instances = instances
|
.onChange(of: instanceName) {
|
||||||
}
|
searchingTask.cancel()
|
||||||
}
|
searchingTask = Task {
|
||||||
|
try? await Task.sleep(for: .seconds(0.1))
|
||||||
getInstanceDetailTask.cancel()
|
guard !Task.isCancelled else { return }
|
||||||
getInstanceDetailTask = Task {
|
|
||||||
try? await Task.sleep(for: .seconds(0.1))
|
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||||
guard !Task.isCancelled else { return }
|
withAnimation {
|
||||||
|
self.instances = instances
|
||||||
do {
|
}
|
||||||
// bare bones preflight for domain validity
|
}
|
||||||
let instanceDetailClient = Client(server: sanitizedName)
|
|
||||||
if
|
getInstanceDetailTask.cancel()
|
||||||
instanceDetailClient.server.contains("."),
|
getInstanceDetailTask = Task {
|
||||||
instanceDetailClient.server.last != "."
|
try? await Task.sleep(for: .seconds(0.1))
|
||||||
{
|
guard !Task.isCancelled else { return }
|
||||||
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
|
|
||||||
withAnimation {
|
do {
|
||||||
self.instance = instance
|
// bare bones preflight for domain validity
|
||||||
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
|
let instanceDetailClient = Client(server: sanitizedName)
|
||||||
}
|
if
|
||||||
instanceFetchError = nil
|
instanceDetailClient.server.contains("."),
|
||||||
} else {
|
instanceDetailClient.server.last != "."
|
||||||
instance = nil
|
{
|
||||||
instanceFetchError = nil
|
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
|
||||||
.onChange(of: scenePhase) { _, newValue in
|
switch newValue {
|
||||||
switch newValue {
|
case .active:
|
||||||
case .active:
|
isSigninIn = false
|
||||||
isSigninIn = false
|
default:
|
||||||
default:
|
break
|
||||||
break
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,9 +214,9 @@ struct AddAccountView: View {
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
(Text("instance.list.users-\(formatAsNumber(instance.users))")
|
(Text("instance.list.users-\(formatAsNumber(instance.users))")
|
||||||
+ Text(" ⸱ ")
|
+ Text(" ⸱ ")
|
||||||
+ Text("instance.list.posts-\(formatAsNumber(instance.statuses))"))
|
+ Text("instance.list.posts-\(formatAsNumber(instance.statuses))"))
|
||||||
.foregroundStyle(theme.tintColor)
|
.foregroundStyle(theme.tintColor)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, 5)
|
||||||
Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
|
Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
|
||||||
|
@ -263,7 +263,7 @@ struct AddAccountView: View {
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,14 @@ import Models
|
||||||
import Network
|
import Network
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UserNotifications
|
|
||||||
import Timeline
|
import Timeline
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct ContentSettingsView: View {
|
struct ContentSettingsView: View {
|
||||||
@Environment(UserPreferences.self) private var userPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
@State private var contentFilter = TimelineContentFilter.shared
|
@State private var contentFilter = TimelineContentFilter.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -41,7 +41,7 @@ struct ContentSettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ struct ContentSettingsView: View {
|
||||||
Text("settings.content.default-sensitive")
|
Text("settings.content.default-sensitive")
|
||||||
}
|
}
|
||||||
.disabled(userPreferences.useInstanceContentSettings)
|
.disabled(userPreferences.useInstanceContentSettings)
|
||||||
|
|
||||||
Toggle(isOn: $userPreferences.appRequireAltText) {
|
Toggle(isOn: $userPreferences.appRequireAltText) {
|
||||||
Text("settings.content.require-alt-text")
|
Text("settings.content.require-alt-text")
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ struct ContentSettingsView: View {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Section("timeline.content-filter.title") {
|
Section("timeline.content-filter.title") {
|
||||||
Toggle(isOn: $contentFilter.showBoosts) {
|
Toggle(isOn: $contentFilter.showBoosts) {
|
||||||
Label("timeline.filter.show-boosts", image: "Rocket")
|
Label("timeline.filter.show-boosts", image: "Rocket")
|
||||||
|
@ -142,8 +142,8 @@ struct ContentSettingsView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.content.navigation-title")
|
.navigationTitle("settings.content.navigation-title")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import Observation
|
||||||
import StatusKit
|
import StatusKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
@Observable class DisplaySettingsLocalValues {
|
@Observable class DisplaySettingsLocalValues {
|
||||||
var tintColor = Theme.shared.tintColor
|
var tintColor = Theme.shared.tintColor
|
||||||
var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
||||||
|
@ -36,11 +37,11 @@ struct DisplaySettingsView: View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
Form {
|
Form {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
StatusRowView(viewModel: previewStatusViewModel)
|
StatusRowView(viewModel: previewStatusViewModel)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
.opacity(0)
|
.opacity(0)
|
||||||
.hidden()
|
.hidden()
|
||||||
themeSection
|
themeSection
|
||||||
#endif
|
#endif
|
||||||
fontSection
|
fontSection
|
||||||
layoutSection
|
layoutSection
|
||||||
|
@ -49,35 +50,35 @@ struct DisplaySettingsView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.display.navigation-title")
|
.navigationTitle("settings.display.navigation-title")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.task(id: localValues.tintColor) {
|
.task(id: localValues.tintColor) {
|
||||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
theme.tintColor = localValues.tintColor
|
theme.tintColor = localValues.tintColor
|
||||||
}
|
}
|
||||||
.task(id: localValues.primaryBackgroundColor) {
|
.task(id: localValues.primaryBackgroundColor) {
|
||||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
theme.primaryBackgroundColor = localValues.primaryBackgroundColor
|
theme.primaryBackgroundColor = localValues.primaryBackgroundColor
|
||||||
}
|
}
|
||||||
.task(id: localValues.secondaryBackgroundColor) {
|
.task(id: localValues.secondaryBackgroundColor) {
|
||||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor
|
theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor
|
||||||
}
|
}
|
||||||
.task(id: localValues.labelColor) {
|
.task(id: localValues.labelColor) {
|
||||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
theme.labelColor = localValues.labelColor
|
theme.labelColor = localValues.labelColor
|
||||||
}
|
}
|
||||||
.task(id: localValues.lineSpacing) {
|
.task(id: localValues.lineSpacing) {
|
||||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
theme.lineSpacing = localValues.lineSpacing
|
theme.lineSpacing = localValues.lineSpacing
|
||||||
}
|
}
|
||||||
.task(id: localValues.fontSizeScale) {
|
.task(id: localValues.fontSizeScale) {
|
||||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||||
theme.fontSizeScale = localValues.fontSizeScale
|
theme.fontSizeScale = localValues.fontSizeScale
|
||||||
}
|
}
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
examplePost
|
examplePost
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@ struct HapticSettingsView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.haptic.navigation-title")
|
.navigationTitle("settings.haptic.navigation-title")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@ struct InstanceInfoView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("instance.info.navigation-title")
|
.navigationTitle("instance.info.navigation-title")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,12 +111,12 @@ struct PushNotificationsView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.push.navigation-title")
|
.navigationTitle("settings.push.navigation-title")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.task {
|
.task {
|
||||||
await subscription.fetchSubscription()
|
await subscription.fetchSubscription()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSubscription() {
|
private func updateSubscription() {
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
import Models
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct RecenTagsSettingView: View {
|
struct RecenTagsSettingView: View {
|
||||||
@Environment(\.modelContext) private var context
|
@Environment(\.modelContext) private var context
|
||||||
|
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
@Query(sort: \RecentTag.lastUse, order: .reverse) var tags: [RecentTag]
|
@Query(sort: \RecentTag.lastUse, order: .reverse) var tags: [RecentTag]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
ForEach(tags) { tag in
|
ForEach(tags) { tag in
|
||||||
|
@ -35,10 +35,10 @@ struct RecenTagsSettingView: View {
|
||||||
.navigationTitle("settings.general.recent-tags")
|
.navigationTitle("settings.general.recent-tags")
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
EditButton()
|
EditButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
import Models
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct RemoteTimelinesSettingView: View {
|
struct RemoteTimelinesSettingView: View {
|
||||||
@Environment(\.modelContext) private var context
|
@Environment(\.modelContext) private var context
|
||||||
|
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
|
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
ForEach(localTimelines) { timeline in
|
ForEach(localTimelines) { timeline in
|
||||||
|
@ -36,10 +36,10 @@ struct RemoteTimelinesSettingView: View {
|
||||||
.navigationTitle("settings.general.remote-timelines")
|
.navigationTitle("settings.general.remote-timelines")
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
EditButton()
|
EditButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,27 +43,27 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.navigationTitle(Text("settings.title"))
|
.navigationTitle(Text("settings.title"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
if isModal {
|
if isModal {
|
||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
Button {
|
Button {
|
||||||
dismiss()
|
dismiss()
|
||||||
} label: {
|
} label: {
|
||||||
Text("action.done").bold()
|
Text("action.done").bold()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal {
|
||||||
|
SecondaryColumnToolbarItem()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal {
|
.withAppRouter()
|
||||||
SecondaryColumnToolbarItem()
|
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||||
}
|
|
||||||
}
|
|
||||||
.withAppRouter()
|
|
||||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routerPath.client = client
|
routerPath.client = client
|
||||||
|
|
|
@ -2,10 +2,11 @@ import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct SidebarEntriesSettingsView: View {
|
struct SidebarEntriesSettingsView: View {
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(UserPreferences.self) private var userPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
|
||||||
@State private var sidebarTabs = SidebarTabs.shared
|
@State private var sidebarTabs = SidebarTabs.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -28,11 +29,11 @@ struct SidebarEntriesSettingsView: View {
|
||||||
.environment(\.editMode, .constant(.active))
|
.environment(\.editMode, .constant(.active))
|
||||||
.navigationTitle("settings.general.sidebarEntries")
|
.navigationTitle("settings.general.sidebarEntries")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func move(from source: IndexSet, to destination: Int) {
|
func move(from source: IndexSet, to destination: Int) {
|
||||||
sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination)
|
sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination)
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,24 +69,24 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.support.navigation-title")
|
.navigationTitle("settings.support.navigation-title")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
|
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
|
||||||
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
|
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
|
||||||
}, message: {
|
}, message: {
|
||||||
Text("settings.support.alert.message")
|
Text("settings.support.alert.message")
|
||||||
})
|
})
|
||||||
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
|
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
|
||||||
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
|
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
|
||||||
}, message: {
|
}, message: {
|
||||||
Text("settings.support.alert.error.message")
|
Text("settings.support.alert.error.message")
|
||||||
})
|
})
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadingProducts = true
|
loadingProducts = true
|
||||||
fetchStoreProducts()
|
fetchStoreProducts()
|
||||||
refreshUserInfo()
|
refreshUserInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func purchase(product: StoreProduct) async {
|
private func purchase(product: StoreProduct) async {
|
||||||
|
|
|
@ -49,7 +49,7 @@ struct SwipeActionsSettingsView: View {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) {
|
Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) {
|
||||||
ForEach(UserPreferences.SwipeActionsIconStyle.allCases, id: \.rawValue) { style in
|
ForEach(UserPreferences.SwipeActionsIconStyle.allCases, id: \.rawValue) { style in
|
||||||
|
@ -70,8 +70,8 @@ struct SwipeActionsSettingsView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.swipeactions.navigation-title")
|
.navigationTitle("settings.swipeactions.navigation-title")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import SwiftUI
|
||||||
struct TabbarEntriesSettingsView: View {
|
struct TabbarEntriesSettingsView: View {
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(UserPreferences.self) private var userPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
|
||||||
@State private var tabs = iOSTabs.shared
|
@State private var tabs = iOSTabs.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -42,7 +42,7 @@ struct TabbarEntriesSettingsView: View {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
|
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
|
||||||
}
|
}
|
||||||
|
@ -52,8 +52,8 @@ struct TabbarEntriesSettingsView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.general.tabbarEntries")
|
.navigationTitle("settings.general.tabbarEntries")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
import Models
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct TagsGroupSettingView: View {
|
struct TagsGroupSettingView: View {
|
||||||
@Environment(\.modelContext) private var context
|
@Environment(\.modelContext) private var context
|
||||||
|
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
|
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
ForEach(tagGroups) { group in
|
ForEach(tagGroups) { group in
|
||||||
|
@ -41,10 +41,10 @@ struct TagsGroupSettingView: View {
|
||||||
.navigationTitle("timeline.filter.tag-groups")
|
.navigationTitle("timeline.filter.tag-groups")
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
EditButton()
|
EditButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ struct TranslationSettingsView: View {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if apiKey.isEmpty {
|
if apiKey.isEmpty {
|
||||||
Section {
|
Section {
|
||||||
Link(destination: URL(string: "https://www.deepl.com/pro-api")!) {
|
Link(destination: URL(string: "https://www.deepl.com/pro-api")!) {
|
||||||
|
@ -41,13 +41,13 @@ struct TranslationSettingsView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.translation.navigation-title")
|
.navigationTitle("settings.translation.navigation-title")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.onChange(of: apiKey) {
|
.onChange(of: apiKey) {
|
||||||
writeNewValue()
|
writeNewValue()
|
||||||
}
|
}
|
||||||
.onAppear(perform: updatePrefs)
|
.onAppear(perform: updatePrefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -24,7 +24,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||||
static func loggedOutTab() -> [Tab] {
|
static func loggedOutTab() -> [Tab] {
|
||||||
[.timeline, .settings]
|
[.timeline, .settings]
|
||||||
}
|
}
|
||||||
|
|
||||||
static func visionOSTab() -> [Tab] {
|
static func visionOSTab() -> [Tab] {
|
||||||
[.profile, .timeline, .notifications, .mentions, .explore, .post, .settings]
|
[.profile, .timeline, .notifications, .mentions, .explore, .post, .settings]
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||||
case .links:
|
case .links:
|
||||||
NavigationTab { TrendingLinksListView(cards: []) }
|
NavigationTab { TrendingLinksListView(cards: []) }
|
||||||
case .post:
|
case .post:
|
||||||
VStack { }
|
VStack {}
|
||||||
case .other:
|
case .other:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,6 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||||
Label("explore.section.trending.links", systemImage: iconName)
|
Label("explore.section.trending.links", systemImage: iconName)
|
||||||
case .other:
|
case .other:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,13 +157,14 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
class SidebarTabs {
|
class SidebarTabs {
|
||||||
struct SidedebarTab: Hashable, Codable {
|
struct SidedebarTab: Hashable, Codable {
|
||||||
let tab: Tab
|
let tab: Tab
|
||||||
var enabled: Bool
|
var enabled: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
class Storage {
|
class Storage {
|
||||||
@AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [
|
@AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [
|
||||||
.init(tab: .timeline, enabled: true),
|
.init(tab: .timeline, enabled: true),
|
||||||
|
@ -179,36 +179,37 @@ class SidebarTabs {
|
||||||
.init(tab: .favorites, enabled: true),
|
.init(tab: .favorites, enabled: true),
|
||||||
.init(tab: .followedTags, enabled: true),
|
.init(tab: .followedTags, enabled: true),
|
||||||
.init(tab: .lists, enabled: true),
|
.init(tab: .lists, enabled: true),
|
||||||
|
|
||||||
.init(tab: .settings, enabled: true),
|
.init(tab: .settings, enabled: true),
|
||||||
.init(tab: .profile, enabled: true),
|
.init(tab: .profile, enabled: true),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private let storage = Storage()
|
private let storage = Storage()
|
||||||
public static let shared = SidebarTabs()
|
public static let shared = SidebarTabs()
|
||||||
|
|
||||||
var tabs: [SidedebarTab] {
|
var tabs: [SidedebarTab] {
|
||||||
didSet {
|
didSet {
|
||||||
storage.tabs = tabs
|
storage.tabs = tabs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isEnabled(_ tab: Tab) -> Bool {
|
func isEnabled(_ tab: Tab) -> Bool {
|
||||||
tabs.first(where: { $0.tab.id == tab.id })?.enabled == true
|
tabs.first(where: { $0.tab.id == tab.id })?.enabled == true
|
||||||
}
|
}
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
tabs = storage.tabs
|
tabs = storage.tabs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
class iOSTabs {
|
class iOSTabs {
|
||||||
enum TabEntries: String {
|
enum TabEntries: String {
|
||||||
case first, second, third, fourth, fifth
|
case first, second, third, fourth, fifth
|
||||||
}
|
}
|
||||||
|
|
||||||
class Storage {
|
class Storage {
|
||||||
@AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline
|
@AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline
|
||||||
@AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications
|
@AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications
|
||||||
|
@ -216,44 +217,44 @@ class iOSTabs {
|
||||||
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.messages
|
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.messages
|
||||||
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile
|
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile
|
||||||
}
|
}
|
||||||
|
|
||||||
private let storage = Storage()
|
private let storage = Storage()
|
||||||
public static let shared = iOSTabs()
|
public static let shared = iOSTabs()
|
||||||
|
|
||||||
var tabs: [Tab] {
|
var tabs: [Tab] {
|
||||||
[firstTab, secondTab, thirdTab, fourthTab, fifthTab]
|
[firstTab, secondTab, thirdTab, fourthTab, fifthTab]
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstTab: Tab {
|
var firstTab: Tab {
|
||||||
didSet {
|
didSet {
|
||||||
storage.firstTab = firstTab
|
storage.firstTab = firstTab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var secondTab: Tab {
|
var secondTab: Tab {
|
||||||
didSet {
|
didSet {
|
||||||
storage.secondTab = secondTab
|
storage.secondTab = secondTab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var thirdTab: Tab {
|
var thirdTab: Tab {
|
||||||
didSet {
|
didSet {
|
||||||
storage.thirdTab = thirdTab
|
storage.thirdTab = thirdTab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fourthTab: Tab {
|
var fourthTab: Tab {
|
||||||
didSet {
|
didSet {
|
||||||
storage.fourthTab = fourthTab
|
storage.fourthTab = fourthTab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fifthTab: Tab {
|
var fifthTab: Tab {
|
||||||
didSet {
|
didSet {
|
||||||
storage.fifthTab = fifthTab
|
storage.fifthTab = fifthTab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
firstTab = storage.firstTab
|
firstTab = storage.firstTab
|
||||||
secondTab = storage.secondTab
|
secondTab = storage.secondTab
|
||||||
|
|
|
@ -61,20 +61,20 @@ struct EditTagGroupView: View {
|
||||||
)
|
)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
.scrollDismissesKeyboard(.interactively)
|
.scrollDismissesKeyboard(.interactively)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
CancelToolbarItem()
|
CancelToolbarItem()
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("action.save", action: { save() })
|
Button("action.save", action: { save() })
|
||||||
.disabled(!tagGroup.isValid)
|
.disabled(!tagGroup.isValid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
focusedField = .title
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
focusedField = .title
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,29 +52,29 @@ struct AddRemoteTimelineView: View {
|
||||||
.navigationTitle("timeline.add-remote.title")
|
.navigationTitle("timeline.add-remote.title")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
CancelToolbarItem()
|
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
|
||||||
.onAppear {
|
instanceNamePublisher.send(newValue)
|
||||||
isInstanceURLFieldFocused = true
|
}
|
||||||
let client = InstanceSocialClient()
|
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
|
||||||
Task {
|
Task {
|
||||||
instances = await client.fetchInstances(keyword: instanceName)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
switch newValue {
|
switch newValue {
|
||||||
case let .tagGroup(title, _, _):
|
case let .tagGroup(title, _, _):
|
||||||
if let group = tagGroups.first(where: { $0.title == title}) {
|
if let group = tagGroups.first(where: { $0.title == title }) {
|
||||||
selectedTagGroup = group
|
selectedTagGroup = group
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -185,7 +185,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var headerGroup: some View {
|
private var headerGroup: some View {
|
||||||
ControlGroup {
|
ControlGroup {
|
||||||
|
@ -209,10 +209,10 @@ struct TimelineTab: View {
|
||||||
pinButton
|
pinButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var pinButton: some View {
|
private var pinButton: some View {
|
||||||
let index = pinnedFilters.firstIndex(where: { $0.id == timeline.id})
|
let index = pinnedFilters.firstIndex(where: { $0.id == timeline.id })
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
if let index {
|
if let index {
|
||||||
|
@ -222,14 +222,14 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
if index != nil {
|
if index != nil {
|
||||||
Label("status.action.unpin", systemImage: "pin.slash")
|
Label("status.action.unpin", systemImage: "pin.slash")
|
||||||
} else {
|
} else {
|
||||||
Label("status.action.pin", systemImage: "pin")
|
Label("status.action.pin", systemImage: "pin")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var timelineFiltersButtons: some View {
|
private var timelineFiltersButtons: some View {
|
||||||
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
|
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
|
||||||
Button {
|
Button {
|
||||||
|
@ -239,7 +239,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var listsFiltersButons: some View {
|
private var listsFiltersButons: some View {
|
||||||
Menu("timeline.filter.lists") {
|
Menu("timeline.filter.lists") {
|
||||||
|
@ -257,7 +257,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var tagsFiltersButtons: some View {
|
private var tagsFiltersButtons: some View {
|
||||||
if !currentAccount.tags.isEmpty {
|
if !currentAccount.tags.isEmpty {
|
||||||
|
@ -272,7 +272,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var localTimelinesFiltersButtons: some View {
|
private var localTimelinesFiltersButtons: some View {
|
||||||
Menu("timeline.filter.local") {
|
Menu("timeline.filter.local") {
|
||||||
ForEach(localTimelines) { remoteLocal in
|
ForEach(localTimelines) { remoteLocal in
|
||||||
|
@ -291,7 +291,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var tagGroupsFiltersButtons: some View {
|
private var tagGroupsFiltersButtons: some View {
|
||||||
Menu("timeline.filter.tag-groups") {
|
Menu("timeline.filter.tag-groups") {
|
||||||
ForEach(tagGroups) { group in
|
ForEach(tagGroups) { group in
|
||||||
|
@ -312,7 +312,7 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contentFilterButton: some View {
|
private var contentFilterButton: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
routerPath.presentedSheet = .timelineContentFilter
|
routerPath.presentedSheet = .timelineContentFilter
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import AppAccount
|
import AppAccount
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct ToolbarTab: ToolbarContent {
|
struct ToolbarTab: ToolbarContent {
|
||||||
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
@Environment(UserPreferences.self) private var userPreferences
|
@Environment(UserPreferences.self) private var userPreferences
|
||||||
|
|
||||||
@Binding var routerPath: RouterPath
|
@Binding var routerPath: RouterPath
|
||||||
|
|
||||||
var body: some ToolbarContent {
|
var body: some ToolbarContent {
|
||||||
if !isSecondaryColumn {
|
if !isSecondaryColumn {
|
||||||
statusEditorToolbarItem(routerPath: routerPath,
|
statusEditorToolbarItem(routerPath: routerPath,
|
||||||
visibility: userPreferences.postVisibility)
|
visibility: userPreferences.postVisibility)
|
||||||
if UIDevice.current.userInterfaceIdiom != .pad ||
|
if UIDevice.current.userInterfaceIdiom != .pad ||
|
||||||
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact) {
|
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
|
||||||
|
{
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
AppAccountsSelectorView(routerPath: routerPath)
|
AppAccountsSelectorView(routerPath: routerPath)
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -24,7 +24,7 @@ extension NotificationService {
|
||||||
var _plaintext: Data?
|
var _plaintext: Data?
|
||||||
do {
|
do {
|
||||||
_plaintext = try AES.GCM.open(sealedBox, using: key)
|
_plaintext = try AES.GCM.open(sealedBox, using: key)
|
||||||
} catch { }
|
} catch {}
|
||||||
guard let plaintext = _plaintext else {
|
guard let plaintext = _plaintext else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@ import Account
|
||||||
import AppAccount
|
import AppAccount
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import StatusKit
|
import StatusKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import Models
|
|
||||||
|
|
||||||
class ShareViewController: UIViewController {
|
class ShareViewController: UIViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
|
|
@ -31,7 +31,7 @@ let package = Package(
|
||||||
.product(name: "Models", package: "Models"),
|
.product(name: "Models", package: "Models"),
|
||||||
.product(name: "StatusKit", package: "StatusKit"),
|
.product(name: "StatusKit", package: "StatusKit"),
|
||||||
.product(name: "Env", package: "Env"),
|
.product(name: "Env", package: "Env"),
|
||||||
.product(name: "ButtonKit", package: "ButtonKit")
|
.product(name: "ButtonKit", package: "ButtonKit"),
|
||||||
],
|
],
|
||||||
swiftSettings: [
|
swiftSettings: [
|
||||||
.enableExperimentalFeature("StrictConcurrency"),
|
.enableExperimentalFeature("StrictConcurrency"),
|
||||||
|
|
|
@ -28,16 +28,16 @@ public struct AccountDetailContextMenu: View {
|
||||||
Label("account.action.message", systemImage: "tray.full")
|
Label("account.action.message", systemImage: "tray.full")
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
#if !targetEnvironment(macCatalyst)
|
||||||
Divider()
|
Divider()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if viewModel.relationship?.blocking == true {
|
if viewModel.relationship?.blocking == true {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
viewModel.relationship = try await client.post(endpoint: Accounts.unblock(id: account.id))
|
viewModel.relationship = try await client.post(endpoint: Accounts.unblock(id: account.id))
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.unblock", systemImage: "person.crop.circle.badge.exclamationmark")
|
Label("account.action.unblock", systemImage: "person.crop.circle.badge.exclamationmark")
|
||||||
|
@ -55,7 +55,7 @@ public struct AccountDetailContextMenu: View {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
viewModel.relationship = try await client.post(endpoint: Accounts.unmute(id: account.id))
|
viewModel.relationship = try await client.post(endpoint: Accounts.unmute(id: account.id))
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.unmute", systemImage: "speaker")
|
Label("account.action.unmute", systemImage: "speaker")
|
||||||
|
@ -67,7 +67,7 @@ public struct AccountDetailContextMenu: View {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
viewModel.relationship = try await client.post(endpoint: Accounts.mute(id: account.id, json: MuteData(duration: duration.rawValue)))
|
viewModel.relationship = try await client.post(endpoint: Accounts.mute(id: account.id, json: MuteData(duration: duration.rawValue)))
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,7 @@ public struct AccountDetailContextMenu: View {
|
||||||
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
||||||
notify: false,
|
notify: false,
|
||||||
reblogs: relationship.showingReblogs))
|
reblogs: relationship.showingReblogs))
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.notify-disable", systemImage: "bell.fill")
|
Label("account.action.notify-disable", systemImage: "bell.fill")
|
||||||
|
@ -98,7 +98,7 @@ public struct AccountDetailContextMenu: View {
|
||||||
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
||||||
notify: true,
|
notify: true,
|
||||||
reblogs: relationship.showingReblogs))
|
reblogs: relationship.showingReblogs))
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.notify-enable", systemImage: "bell")
|
Label("account.action.notify-enable", systemImage: "bell")
|
||||||
|
@ -111,7 +111,7 @@ public struct AccountDetailContextMenu: View {
|
||||||
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
||||||
notify: relationship.notifying,
|
notify: relationship.notifying,
|
||||||
reblogs: false))
|
reblogs: false))
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.reboosts-hide", image: "Rocket.Fill")
|
Label("account.action.reboosts-hide", image: "Rocket.Fill")
|
||||||
|
@ -123,7 +123,7 @@ public struct AccountDetailContextMenu: View {
|
||||||
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id,
|
||||||
notify: relationship.notifying,
|
notify: relationship.notifying,
|
||||||
reblogs: true))
|
reblogs: true))
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.reboosts-show", image: "Rocket")
|
Label("account.action.reboosts-show", image: "Rocket")
|
||||||
|
@ -131,9 +131,9 @@ public struct AccountDetailContextMenu: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
#if !targetEnvironment(macCatalyst)
|
||||||
Divider()
|
Divider()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier {
|
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier {
|
||||||
|
@ -164,7 +164,7 @@ public struct AccountDetailContextMenu: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
#if !targetEnvironment(macCatalyst)
|
||||||
Divider()
|
Divider()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -207,6 +207,7 @@ struct AccountDetailHeaderView: View {
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
.accessibilityRespondsToUserInteraction(false)
|
.accessibilityRespondsToUserInteraction(false)
|
||||||
|
movedToView
|
||||||
joinedAtView
|
joinedAtView
|
||||||
}
|
}
|
||||||
.accessibilityElement(children: .contain)
|
.accessibilityElement(children: .contain)
|
||||||
|
@ -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
|
@ViewBuilder
|
||||||
private func makeNoteView(_ note: String) -> some View {
|
private func makeNoteView(_ note: String) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
@ -372,15 +384,15 @@ struct AccountDetailHeaderView: View {
|
||||||
.accessibilityElement(children: .contain)
|
.accessibilityElement(children: .contain)
|
||||||
.accessibilityLabel("accessibility.tabs.profile.fields.container.label")
|
.accessibilityLabel("accessibility.tabs.profile.fields.container.label")
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
.background(Material.thick)
|
.background(Material.thick)
|
||||||
#else
|
#else
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 4)
|
RoundedRectangle(cornerRadius: 4)
|
||||||
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,8 @@ public struct AccountDetailView: View {
|
||||||
@State private var viewModel: AccountDetailViewModel
|
@State private var viewModel: AccountDetailViewModel
|
||||||
@State private var isCurrentUser: Bool = false
|
@State private var isCurrentUser: Bool = false
|
||||||
@State private var showBlockConfirmation: 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 isEditingRelationshipNote: Bool = false
|
||||||
|
|
||||||
@State private var displayTitle: Bool = false
|
@State private var displayTitle: Bool = false
|
||||||
|
|
||||||
@Binding var scrollToTopSignal: Int
|
@Binding var scrollToTopSignal: Int
|
||||||
|
@ -88,14 +85,14 @@ public struct AccountDetailView: View {
|
||||||
.environment(\.defaultMinListRowHeight, 1)
|
.environment(\.defaultMinListRowHeight, 1)
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.primaryBackgroundColor)
|
.background(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.onChange(of: scrollToTopSignal) {
|
.onChange(of: scrollToTopSignal) {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
guard reasons != .placeholder else { return }
|
guard reasons != .placeholder else { return }
|
||||||
|
@ -136,20 +133,14 @@ public struct AccountDetailView: View {
|
||||||
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
|
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: isEditingAccount) { _, newValue in
|
.onChange(of: routerPath.presentedSheet) { oldValue, newValue in
|
||||||
if !newValue {
|
if oldValue == .accountEditInfo || newValue == .accountEditInfo {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.fetchAccount()
|
await viewModel.fetchAccount()
|
||||||
await preferences.refreshServerPreferences()
|
await preferences.refreshServerPreferences()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isEditingAccount, content: {
|
|
||||||
EditAccountView()
|
|
||||||
})
|
|
||||||
.sheet(isPresented: $isEditingFilters, content: {
|
|
||||||
FiltersListView()
|
|
||||||
})
|
|
||||||
.sheet(isPresented: $isEditingRelationshipNote, content: {
|
.sheet(isPresented: $isEditingRelationshipNote, content: {
|
||||||
EditRelationshipNoteView(accountDetailViewModel: viewModel)
|
EditRelationshipNoteView(accountDetailViewModel: viewModel)
|
||||||
})
|
})
|
||||||
|
@ -220,7 +211,6 @@ public struct AccountDetailView: View {
|
||||||
AvatarView(account.avatar, config: .badge)
|
AvatarView(account.avatar, config: .badge)
|
||||||
.padding(.leading, -4)
|
.padding(.leading, -4)
|
||||||
.accessibilityLabel(account.safeDisplayName)
|
.accessibilityLabel(account.safeDisplayName)
|
||||||
|
|
||||||
}
|
}
|
||||||
.accessibilityAddTraits(.isImage)
|
.accessibilityAddTraits(.isImage)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
@ -247,18 +237,18 @@ public struct AccountDetailView: View {
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
trailing: .layoutPadding))
|
trailing: .layoutPadding))
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
ForEach(viewModel.pinned) { status in
|
ForEach(viewModel.pinned) { status in
|
||||||
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
|
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
|
||||||
}
|
}
|
||||||
Rectangle()
|
Rectangle()
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
.fill(Color.clear)
|
.fill(Color.clear)
|
||||||
#else
|
#else
|
||||||
.fill(theme.secondaryBackgroundColor)
|
.fill(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.frame(height: 12)
|
.frame(height: 12)
|
||||||
.listRowInsets(.init())
|
.listRowInsets(.init())
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
@ -288,7 +278,6 @@ public struct AccountDetailView: View {
|
||||||
routerPath.presentedSheet = .mentionStatusEditor(account: account,
|
routerPath.presentedSheet = .mentionStatusEditor(account: account,
|
||||||
visibility: preferences.postVisibility)
|
visibility: preferences.postVisibility)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "arrowshape.turn.up.left")
|
Image(systemName: "arrowshape.turn.up.left")
|
||||||
|
@ -308,7 +297,7 @@ public struct AccountDetailView: View {
|
||||||
|
|
||||||
if isCurrentUser {
|
if isCurrentUser {
|
||||||
Button {
|
Button {
|
||||||
isEditingAccount = true
|
routerPath.presentedSheet = .accountEditInfo
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.edit-info", systemImage: "pencil")
|
Label("account.action.edit-info", systemImage: "pencil")
|
||||||
}
|
}
|
||||||
|
@ -323,7 +312,7 @@ public struct AccountDetailView: View {
|
||||||
|
|
||||||
if currentInstance.isFiltersSupported {
|
if currentInstance.isFiltersSupported {
|
||||||
Button {
|
Button {
|
||||||
isEditingFilters = true
|
routerPath.presentedSheet = .accountFiltersList
|
||||||
} label: {
|
} label: {
|
||||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||||
}
|
}
|
||||||
|
@ -370,7 +359,7 @@ public struct AccountDetailView: View {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
viewModel.relationship = try await client.post(endpoint: Accounts.block(id: account.id))
|
viewModel.relationship = try await client.post(endpoint: Accounts.block(id: account.id))
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -382,12 +371,13 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
@MainActor
|
||||||
func applyAccountDetailsRowStyle(theme: Theme) -> some View {
|
func applyAccountDetailsRowStyle(theme: Theme) -> some View {
|
||||||
listRowInsets(.init())
|
listRowInsets(.init())
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,7 @@ import SwiftUI
|
||||||
private var tabTask: Task<Void, Never>?
|
private var tabTask: Task<Void, Never>?
|
||||||
|
|
||||||
private(set) var statuses: [Status] = []
|
private(set) var statuses: [Status] = []
|
||||||
|
|
||||||
var boosts: [Status] = []
|
var boosts: [Status] = []
|
||||||
|
|
||||||
/// When coming from a URL like a mention tap in a status.
|
/// When coming from a URL like a mention tap in a status.
|
||||||
|
@ -151,7 +151,7 @@ import SwiftUI
|
||||||
self.familiarFollowers = familiarFollowers?.first?.accounts ?? []
|
self.familiarFollowers = familiarFollowers?.first?.accounts ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNewestStatuses(pullToRefresh: Bool) async {
|
func fetchNewestStatuses(pullToRefresh _: Bool) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
statusesState = .loading
|
statusesState = .loading
|
||||||
|
@ -166,7 +166,7 @@ import SwiftUI
|
||||||
pinned: nil))
|
pinned: nil))
|
||||||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||||
if selectedTab == .boosts {
|
if selectedTab == .boosts {
|
||||||
boosts = statuses.filter{ $0.reblog != nil }
|
boosts = statuses.filter { $0.reblog != nil }
|
||||||
}
|
}
|
||||||
if selectedTab == .statuses {
|
if selectedTab == .statuses {
|
||||||
pinned =
|
pinned =
|
||||||
|
@ -197,17 +197,17 @@ import SwiftUI
|
||||||
case .statuses, .replies, .boosts, .media:
|
case .statuses, .replies, .boosts, .media:
|
||||||
guard let lastId = statuses.last?.id else { return }
|
guard let lastId = statuses.last?.id else { return }
|
||||||
let newStatuses: [Status] =
|
let newStatuses: [Status] =
|
||||||
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
||||||
sinceId: lastId,
|
sinceId: lastId,
|
||||||
tag: nil,
|
tag: nil,
|
||||||
onlyMedia: selectedTab == .media,
|
onlyMedia: selectedTab == .media,
|
||||||
excludeReplies: selectedTab != .replies,
|
excludeReplies: selectedTab != .replies,
|
||||||
excludeReblogs: selectedTab != .boosts,
|
excludeReblogs: selectedTab != .boosts,
|
||||||
pinned: nil))
|
pinned: nil))
|
||||||
statuses.append(contentsOf: newStatuses)
|
statuses.append(contentsOf: newStatuses)
|
||||||
if selectedTab == .boosts {
|
if selectedTab == .boosts {
|
||||||
let newBoosts = statuses.filter{ $0.reblog != nil }
|
let newBoosts = statuses.filter { $0.reblog != nil }
|
||||||
self.boosts.append(contentsOf: newBoosts)
|
boosts.append(contentsOf: newBoosts)
|
||||||
}
|
}
|
||||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||||
if selectedTab == .boosts {
|
if selectedTab == .boosts {
|
||||||
|
@ -253,7 +253,8 @@ import SwiftUI
|
||||||
if let event = event as? StreamEventUpdate {
|
if let event = event as? StreamEventUpdate {
|
||||||
if event.status.account.id == currentAccount.account?.id {
|
if event.status.account.id == currentAccount.account?.id {
|
||||||
if (event.status.inReplyToId == nil && selectedTab == .statuses) ||
|
if (event.status.inReplyToId == nil && selectedTab == .statuses) ||
|
||||||
(event.status.inReplyToId != nil && selectedTab == .replies) {
|
(event.status.inReplyToId != nil && selectedTab == .replies)
|
||||||
|
{
|
||||||
statuses.insert(event.status, at: 0)
|
statuses.insert(event.status, at: 0)
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ public struct AccountsListView: View {
|
||||||
await viewModel.fetch()
|
await viewModel.fetch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var listView: some View {
|
private var listView: some View {
|
||||||
if currentAccount.account?.id == viewModel.accountId {
|
if currentAccount.account?.id == viewModel.accountId {
|
||||||
|
@ -54,7 +54,7 @@ public struct AccountsListView: View {
|
||||||
standardList
|
standardList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var searchableList: some View {
|
private var searchableList: some View {
|
||||||
List {
|
List {
|
||||||
listContent
|
listContent
|
||||||
|
@ -74,13 +74,13 @@ public struct AccountsListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var standardList: some View {
|
private var standardList: some View {
|
||||||
List {
|
List {
|
||||||
listContent
|
listContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var listContent: some View {
|
private var listContent: some View {
|
||||||
switch viewModel.state {
|
switch viewModel.state {
|
||||||
|
@ -89,9 +89,9 @@ public struct AccountsListView: View {
|
||||||
AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder()))
|
AccountsListRow(viewModel: .init(account: .placeholder(), relationShip: .placeholder()))
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
case let .display(accounts, relationships, nextPageState):
|
case let .display(accounts, relationships, nextPageState):
|
||||||
if case .followers = viewModel.mode,
|
if case .followers = viewModel.mode,
|
||||||
|
@ -125,9 +125,9 @@ public struct AccountsListView: View {
|
||||||
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
||||||
AccountsListRow(viewModel: .init(account: account,
|
AccountsListRow(viewModel: .init(account: account,
|
||||||
relationShip: relationship))
|
relationShip: relationship))
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,16 +140,26 @@ public struct AccountsListView: View {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
case .none:
|
case .none:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .error(error):
|
case let .error(error):
|
||||||
Text(error.localizedDescription)
|
Text(error.localizedDescription)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
List {
|
||||||
|
AccountsListRow(viewModel: .init(account: .placeholder(),
|
||||||
|
relationShip: .placeholder()))
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.withPreviewsEnv()
|
||||||
|
.environment(Theme.shared)
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
|
||||||
import OSLog
|
import OSLog
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public enum AccountsListMode {
|
public enum AccountsListMode {
|
||||||
case following(accountId: String), followers(accountId: String)
|
case following(accountId: String), followers(accountId: String)
|
||||||
|
@ -49,7 +49,7 @@ public enum AccountsListMode {
|
||||||
var state = State.loading
|
var state = State.loading
|
||||||
var totalCount: Int?
|
var totalCount: Int?
|
||||||
var accountId: String?
|
var accountId: String?
|
||||||
|
|
||||||
var searchQuery: String = ""
|
var searchQuery: String = ""
|
||||||
|
|
||||||
private var nextPageId: String?
|
private var nextPageId: String?
|
||||||
|
@ -125,7 +125,7 @@ public enum AccountsListMode {
|
||||||
relationships: relationships,
|
relationships: relationships,
|
||||||
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
|
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
|
||||||
}
|
}
|
||||||
|
|
||||||
func search() async {
|
func search() async {
|
||||||
guard let client, !searchQuery.isEmpty else { return }
|
guard let client, !searchQuery.isEmpty else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -144,8 +144,6 @@ public enum AccountsListMode {
|
||||||
relationships: relationships,
|
relationships: relationships,
|
||||||
nextPageState: .none)
|
nextPageState: .none)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
|
||||||
import NukeUI
|
import NukeUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct EditAccountView: View {
|
public struct EditAccountView: View {
|
||||||
|
@ -13,8 +13,8 @@ public struct EditAccountView: View {
|
||||||
@Environment(UserPreferences.self) private var userPrefs
|
@Environment(UserPreferences.self) private var userPrefs
|
||||||
|
|
||||||
@State private var viewModel = EditAccountViewModel()
|
@State private var viewModel = EditAccountViewModel()
|
||||||
|
|
||||||
public init() { }
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
|
@ -31,24 +31,24 @@ public struct EditAccountView: View {
|
||||||
}
|
}
|
||||||
.environment(\.editMode, .constant(.active))
|
.environment(\.editMode, .constant(.active))
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
#endif
|
#endif
|
||||||
.navigationTitle("account.edit.navigation-title")
|
.navigationTitle("account.edit.navigation-title")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
toolbarContent
|
toolbarContent
|
||||||
}
|
}
|
||||||
.alert("account.edit.error.save.title",
|
.alert("account.edit.error.save.title",
|
||||||
isPresented: $viewModel.saveError,
|
isPresented: $viewModel.saveError,
|
||||||
actions: {
|
actions: {
|
||||||
Button("alert.button.ok", action: {})
|
Button("alert.button.ok", action: {})
|
||||||
}, message: { Text("account.edit.error.save.message") })
|
}, message: { Text("account.edit.error.save.message") })
|
||||||
.task {
|
.task {
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
await viewModel.fetchAccount()
|
await viewModel.fetchAccount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ public struct EditAccountView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var imagesSection: some View {
|
private var imagesSection: some View {
|
||||||
Section {
|
Section {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import StatusKit
|
import StatusKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable class EditAccountViewModel {
|
@Observable class EditAccountViewModel {
|
||||||
|
@ -30,22 +30,23 @@ import StatusKit
|
||||||
var fields: [FieldEditViewModel] = []
|
var fields: [FieldEditViewModel] = []
|
||||||
var avatar: URL?
|
var avatar: URL?
|
||||||
var header: URL?
|
var header: URL?
|
||||||
|
|
||||||
var isPhotoPickerPresented: Bool = false {
|
var isPhotoPickerPresented: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
if !isPhotoPickerPresented && mediaPickers.isEmpty {
|
if !isPhotoPickerPresented, mediaPickers.isEmpty {
|
||||||
isChangingAvatar = false
|
isChangingAvatar = false
|
||||||
isChangingHeader = false
|
isChangingHeader = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var isChangingAvatar: Bool = false
|
var isChangingAvatar: Bool = false
|
||||||
var isChangingHeader: Bool = false
|
var isChangingHeader: Bool = false
|
||||||
|
|
||||||
var isLoading: Bool = true
|
var isLoading: Bool = true
|
||||||
var isSaving: Bool = false
|
var isSaving: Bool = false
|
||||||
var saveError: Bool = false
|
var saveError: Bool = false
|
||||||
|
|
||||||
var mediaPickers: [PhotosPickerItem] = [] {
|
var mediaPickers: [PhotosPickerItem] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
if let item = mediaPickers.first {
|
if let item = mediaPickers.first {
|
||||||
|
@ -108,47 +109,47 @@ import StatusKit
|
||||||
saveError = true
|
saveError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func uploadHeader(data: Data) async -> Bool {
|
private func uploadHeader(data: Data) async -> Bool {
|
||||||
guard let client else { return false }
|
guard let client else { return false }
|
||||||
do {
|
do {
|
||||||
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
|
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
|
||||||
version: .v1,
|
version: .v1,
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
mimeType: "image/jpeg",
|
mimeType: "image/jpeg",
|
||||||
filename: "header",
|
filename: "header",
|
||||||
data: data)
|
data: data)
|
||||||
return response?.statusCode == 200
|
return response?.statusCode == 200
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func uploadAvatar(data: Data) async -> Bool {
|
private func uploadAvatar(data: Data) async -> Bool {
|
||||||
guard let client else { return false }
|
guard let client else { return false }
|
||||||
do {
|
do {
|
||||||
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
|
let response = try await client.mediaUpload(endpoint: Accounts.updateCredentialsMedia,
|
||||||
version: .v1,
|
version: .v1,
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
mimeType: "image/jpeg",
|
mimeType: "image/jpeg",
|
||||||
filename: "avatar",
|
filename: "avatar",
|
||||||
data: data)
|
data: data)
|
||||||
return response?.statusCode == 200
|
return response?.statusCode == 200
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getItemImageData(item: PhotosPickerItem) async -> Data? {
|
private func getItemImageData(item: PhotosPickerItem) async -> Data? {
|
||||||
guard let imageFile = try? await item.loadTransferable(type: StatusEditor.ImageFileTranseferable.self) else { return nil }
|
guard let imageFile = try? await item.loadTransferable(type: StatusEditor.ImageFileTranseferable.self) else { return nil }
|
||||||
|
|
||||||
let compressor = StatusEditor.Compressor()
|
let compressor = StatusEditor.Compressor()
|
||||||
|
|
||||||
guard let compressedData = await compressor.compressImageFrom(url: imageFile.url),
|
guard let compressedData = await compressor.compressImageFrom(url: imageFile.url),
|
||||||
let image = UIImage(data: compressedData),
|
let image = UIImage(data: compressedData),
|
||||||
let uploadData = try? await compressor.compressImageForUpload(image)
|
let uploadData = try? await compressor.compressImageForUpload(image)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
return uploadData
|
return uploadData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,20 +71,20 @@ struct EditFilterView: View {
|
||||||
.navigationTitle(filter?.title ?? NSLocalizedString("filter.new", comment: ""))
|
.navigationTitle(filter?.title ?? NSLocalizedString("filter.new", comment: ""))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.scrollDismissesKeyboard(.interactively)
|
.scrollDismissesKeyboard(.interactively)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if filter == nil {
|
if filter == nil {
|
||||||
focusedField = .title
|
focusedField = .title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.toolbar {
|
||||||
.toolbar {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
saveButton
|
||||||
saveButton
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var expirySection: some View {
|
private var expirySection: some View {
|
||||||
|
|
|
@ -74,18 +74,18 @@ public struct FiltersListView: View {
|
||||||
.navigationTitle("filter.filters")
|
.navigationTitle("filter.filters")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.task {
|
.task {
|
||||||
do {
|
do {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2)
|
filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
} catch {
|
} catch {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
|
||||||
import OSLog
|
import OSLog
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable public class FollowButtonViewModel {
|
@Observable public class FollowButtonViewModel {
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Env
|
|
||||||
|
|
||||||
public struct ListsListView: View {
|
public struct ListsListView: View {
|
||||||
@Environment(CurrentAccount.self) private var currentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List {
|
List {
|
||||||
ForEach(currentAccount.lists) { list in
|
ForEach(currentAccount.lists) { list in
|
||||||
|
@ -43,4 +43,3 @@ public struct ListsListView: View {
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,50 +1,50 @@
|
||||||
import StatusKit
|
import DesignSystem
|
||||||
import Network
|
|
||||||
import SwiftUI
|
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import DesignSystem
|
import Network
|
||||||
|
import StatusKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct AccountStatusesListView: View {
|
public struct AccountStatusesListView: View {
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(Client.self) private var client
|
@Environment(Client.self) private var client
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
|
||||||
@State private var viewModel: AccountStatusesListViewModel
|
@State private var viewModel: AccountStatusesListViewModel
|
||||||
@State private var isLoaded = false
|
@State private var isLoaded = false
|
||||||
|
|
||||||
public init(mode: AccountStatusesListViewModel.Mode) {
|
public init(mode: AccountStatusesListViewModel.Mode) {
|
||||||
_viewModel = .init(initialValue: .init(mode: mode))
|
_viewModel = .init(initialValue: .init(mode: mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List {
|
List {
|
||||||
StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath)
|
StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath)
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.primaryBackgroundColor)
|
.background(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.navigationTitle(viewModel.mode.title)
|
.navigationTitle(viewModel.mode.title)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.refreshable {
|
.refreshable {
|
||||||
await viewModel.fetchNewestStatuses(pullToRefresh: true)
|
await viewModel.fetchNewestStatuses(pullToRefresh: true)
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
guard !isLoaded else { return }
|
guard !isLoaded else { return }
|
||||||
viewModel.client = client
|
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)
|
await viewModel.fetchNewestStatuses(pullToRefresh: false)
|
||||||
isLoaded = true
|
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 Env
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import StatusKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
public class AccountStatusesListViewModel: StatusesFetcher {
|
public class AccountStatusesListViewModel: StatusesFetcher {
|
||||||
public enum Mode {
|
public enum Mode {
|
||||||
case bookmarks, favorites
|
case bookmarks, favorites
|
||||||
|
|
||||||
var title: LocalizedStringKey {
|
var title: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
|
@ -18,7 +18,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
||||||
"accessibility.tabs.profile.picker.favorites"
|
"accessibility.tabs.profile.picker.favorites"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func endpoint(sinceId: String?) -> Endpoint {
|
func endpoint(sinceId: String?) -> Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
|
@ -28,19 +28,19 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mode: Mode
|
let mode: Mode
|
||||||
public var statusesState: StatusesState = .loading
|
public var statusesState: StatusesState = .loading
|
||||||
var statuses: [Status] = []
|
var statuses: [Status] = []
|
||||||
var nextPage: LinkHandler?
|
var nextPage: LinkHandler?
|
||||||
|
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
init(mode: Mode) {
|
init(mode: Mode) {
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchNewestStatuses(pullToRefresh: Bool) async {
|
public func fetchNewestStatuses(pullToRefresh _: Bool) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
statusesState = .loading
|
statusesState = .loading
|
||||||
do {
|
do {
|
||||||
|
@ -52,7 +52,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
||||||
statusesState = .error(error: error)
|
statusesState = .error(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchNextPage() async throws {
|
public func fetchNextPage() async throws {
|
||||||
guard let client, let nextId = nextPage?.maxId else { return }
|
guard let client, let nextId = nextPage?.maxId else { return }
|
||||||
var newStatuses: [Status] = []
|
var newStatuses: [Status] = []
|
||||||
|
@ -62,12 +62,8 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
||||||
statusesState = .display(statuses: statuses,
|
statusesState = .display(statuses: statuses,
|
||||||
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
|
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func statusDidAppear(status: Status) {
|
public func statusDidAppear(status _: Status) {}
|
||||||
|
|
||||||
}
|
public func statusDidDisappear(status _: Status) {}
|
||||||
|
|
||||||
public func statusDidDisappear(status: Status) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Env
|
|
||||||
|
|
||||||
public struct FollowedTagsListView: View {
|
public struct FollowedTagsListView: View {
|
||||||
@Environment(CurrentAccount.self) private var currentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List(currentAccount.tags) { tag in
|
List(currentAccount.tags) { tag in
|
||||||
TagRowView(tag: tag)
|
TagRowView(tag: tag)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
|
@ -32,4 +32,3 @@ public struct FollowedTagsListView: View {
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ public struct AppAccountView: View {
|
||||||
@State var viewModel: AppAccountViewModel
|
@State var viewModel: AppAccountViewModel
|
||||||
|
|
||||||
@Binding var isParentPresented: Bool
|
@Binding var isParentPresented: Bool
|
||||||
|
|
||||||
public init(viewModel: AppAccountViewModel, isParentPresented: Binding<Bool>) {
|
public init(viewModel: AppAccountViewModel, isParentPresented: Binding<Bool>) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
_isParentPresented = isParentPresented
|
_isParentPresented = isParentPresented
|
||||||
|
|
|
@ -33,11 +33,11 @@ public struct AppAccountsSelectorView: View {
|
||||||
|
|
||||||
public init(routerPath: RouterPath,
|
public init(routerPath: RouterPath,
|
||||||
accountCreationEnabled: Bool = true,
|
accountCreationEnabled: Bool = true,
|
||||||
avatarConfig: AvatarView.FrameConfig = .badge)
|
avatarConfig: AvatarView.FrameConfig? = nil)
|
||||||
{
|
{
|
||||||
self.routerPath = routerPath
|
self.routerPath = routerPath
|
||||||
self.accountCreationEnabled = accountCreationEnabled
|
self.accountCreationEnabled = accountCreationEnabled
|
||||||
self.avatarConfig = avatarConfig
|
self.avatarConfig = avatarConfig ?? .badge
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
|
@ -97,11 +97,11 @@ public struct AppAccountsSelectorView: View {
|
||||||
}
|
}
|
||||||
addAccountButton
|
addAccountButton
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
.foregroundStyle(theme.labelColor)
|
.foregroundStyle(theme.labelColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if accountCreationEnabled {
|
if accountCreationEnabled {
|
||||||
|
@ -134,7 +134,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
.environment(routerPath)
|
.environment(routerPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addAccountButton: some View {
|
private var addAccountButton: some View {
|
||||||
Button {
|
Button {
|
||||||
isPresented = false
|
isPresented = false
|
||||||
|
@ -158,7 +158,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
Label("tab.settings", systemImage: "gear")
|
Label("tab.settings", systemImage: "gear")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var supportButton: some View {
|
private var supportButton: some View {
|
||||||
Button {
|
Button {
|
||||||
isPresented = false
|
isPresented = false
|
||||||
|
@ -170,7 +170,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
Label("settings.app.support", systemImage: "wand.and.stars")
|
Label("settings.app.support", systemImage: "wand.and.stars")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var aboutButton: some View {
|
private var aboutButton: some View {
|
||||||
Button {
|
Button {
|
||||||
isPresented = false
|
isPresented = false
|
||||||
|
|
|
@ -71,35 +71,35 @@ public struct ConversationDetailView: View {
|
||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.primaryBackgroundColor)
|
.background(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
if viewModel.conversation.accounts.count == 1,
|
if viewModel.conversation.accounts.count == 1,
|
||||||
let account = viewModel.conversation.accounts.first
|
let account = viewModel.conversation.accounts.first
|
||||||
{
|
{
|
||||||
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
||||||
.font(.scaledHeadline)
|
.font(.scaledHeadline)
|
||||||
.foregroundColor(theme.labelColor)
|
.foregroundColor(theme.labelColor)
|
||||||
.emojiText.size(Font.scaledHeadlineFont.emojiSize)
|
.emojiText.size(Font.scaledHeadlineFont.emojiSize)
|
||||||
.emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
|
.emojiText.baselineOffset(Font.scaledHeadlineFont.emojiBaselineOffset)
|
||||||
} else {
|
} else {
|
||||||
Text("Direct message with \(viewModel.conversation.accounts.count) people")
|
Text("Direct message with \(viewModel.conversation.accounts.count) people")
|
||||||
.font(.scaledHeadline)
|
.font(.scaledHeadline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: watcher.latestEvent?.id) {
|
.onChange(of: watcher.latestEvent?.id) {
|
||||||
if let latestEvent = watcher.latestEvent {
|
if let latestEvent = watcher.latestEvent {
|
||||||
viewModel.handleEvent(event: latestEvent)
|
viewModel.handleEvent(event: latestEvent)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom)
|
scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingView: some View {
|
private var loadingView: some View {
|
||||||
|
|
|
@ -205,12 +205,12 @@ struct ConversationMessageView: View {
|
||||||
.frame(height: 200)
|
.frame(height: 200)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
#if targetEnvironment(macCatalyst) || os(visionOS)
|
#if targetEnvironment(macCatalyst) || os(visionOS)
|
||||||
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement],
|
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement],
|
||||||
selectedAttachment: attachement))
|
selectedAttachment: attachement))
|
||||||
#else
|
#else
|
||||||
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
|
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import SwiftUI
|
||||||
@MainActor
|
@MainActor
|
||||||
struct ConversationsListRow: View {
|
struct ConversationsListRow: View {
|
||||||
@Environment(\.openWindow) private var openWindow
|
@Environment(\.openWindow) private var openWindow
|
||||||
|
|
||||||
@Environment(Client.self) private var client
|
@Environment(Client.self) private var client
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
|
|
@ -48,9 +48,9 @@ public struct ConversationsListView: View {
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
} else if conversations.isEmpty, !viewModel.isLoadingFirstPage, !viewModel.isError {
|
} else if conversations.isEmpty, !viewModel.isLoadingFirstPage, !viewModel.isError {
|
||||||
EmptyView(iconName: "tray",
|
PlaceholderView(iconName: "tray",
|
||||||
title: "conversations.empty.title",
|
title: "conversations.empty.title",
|
||||||
message: "conversations.empty.message")
|
message: "conversations.empty.message")
|
||||||
} else if viewModel.isError {
|
} else if viewModel.isError {
|
||||||
ErrorView(title: "conversations.error.title",
|
ErrorView(title: "conversations.error.title",
|
||||||
message: "conversations.error.message",
|
message: "conversations.error.message",
|
||||||
|
|
|
@ -19,7 +19,7 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(name: "Models", path: "../Models"),
|
.package(name: "Models", path: "../Models"),
|
||||||
.package(name: "Env", path: "../Env"),
|
.package(name: "Env", path: "../Env"),
|
||||||
.package(url: "https://github.com/kean/Nuke", from: "12.0.0"),
|
.package(url: "https://github.com/kean/Nuke", from: "12.4.0"),
|
||||||
.package(url: "https://github.com/divadretlaw/EmojiText", from: "4.0.0"),
|
.package(url: "https://github.com/divadretlaw/EmojiText", from: "4.0.0"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
|
|
@ -201,4 +201,3 @@ public struct ThreadsLight: ColorSet {
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
|
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
|
||||||
if condition {
|
if condition {
|
||||||
transform(self)
|
transform(self)
|
||||||
} else {
|
} else {
|
||||||
self
|
self
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,14 @@ import Combine
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
|
@MainActor public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
|
||||||
public var window: UIWindow?
|
public var window: UIWindow?
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
public private(set) var windowWidth: CGFloat = 0
|
public private(set) var windowWidth: CGFloat = 0
|
||||||
public private(set) var windowHeight: CGFloat = 0
|
public private(set) var windowHeight: CGFloat = 0
|
||||||
#else
|
#else
|
||||||
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
|
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
|
||||||
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
|
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public func scene(_ scene: UIScene,
|
public func scene(_ scene: UIScene,
|
||||||
|
@ -30,11 +30,11 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
|
||||||
override public init() {
|
override public init() {
|
||||||
super.init()
|
super.init()
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
windowWidth = window?.bounds.size.width ?? 0
|
windowWidth = window?.bounds.size.width ?? 0
|
||||||
windowHeight = window?.bounds.size.height ?? 0
|
windowHeight = window?.bounds.size.height ?? 0
|
||||||
#else
|
#else
|
||||||
windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width
|
windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width
|
||||||
windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height
|
windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height
|
||||||
#endif
|
#endif
|
||||||
Self.observedSceneDelegate.insert(self)
|
Self.observedSceneDelegate.insert(self)
|
||||||
_ = Self.observer // just for activating the lazy static property
|
_ = Self.observer // just for activating the lazy static property
|
||||||
|
@ -47,30 +47,29 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var observedSceneDelegate: Set<SceneDelegate> = []
|
private static var observedSceneDelegate: Set<SceneDelegate> = []
|
||||||
private static let observer = Task {
|
private static let observer = Task { @MainActor in
|
||||||
while true {
|
while true {
|
||||||
try? await Task.sleep(for: .seconds(0.1))
|
try? await Task.sleep(for: .seconds(0.1))
|
||||||
for delegate in observedSceneDelegate {
|
for delegate in observedSceneDelegate {
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
let newWidth = delegate.window?.bounds.size.width ?? 0
|
let newWidth = delegate.window?.bounds.size.width ?? 0
|
||||||
if delegate.windowWidth != newWidth {
|
if delegate.windowWidth != newWidth {
|
||||||
delegate.windowWidth = newWidth
|
delegate.windowWidth = newWidth
|
||||||
}
|
}
|
||||||
let newHeight = delegate.window?.bounds.size.height ?? 0
|
let newHeight = delegate.window?.bounds.size.height ?? 0
|
||||||
if delegate.windowHeight != newHeight {
|
if delegate.windowHeight != newHeight {
|
||||||
delegate.windowHeight = newHeight
|
delegate.windowHeight = newHeight
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
let newWidth = delegate.window?.bounds.size.width ?? UIScreen.main.bounds.size.width
|
let newWidth = delegate.window?.bounds.size.width ?? UIScreen.main.bounds.size.width
|
||||||
if delegate.windowWidth != newWidth {
|
if delegate.windowWidth != newWidth {
|
||||||
delegate.windowWidth = newWidth
|
delegate.windowWidth = newWidth
|
||||||
}
|
}
|
||||||
let newHeight = delegate.window?.bounds.size.height ?? UIScreen.main.bounds.size.height
|
let newHeight = delegate.window?.bounds.size.height ?? UIScreen.main.bounds.size.height
|
||||||
if delegate.windowHeight != newHeight {
|
if delegate.windowHeight != newHeight {
|
||||||
delegate.windowHeight = newHeight
|
delegate.windowHeight = newHeight
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import Combine
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@Observable public class Theme {
|
@MainActor
|
||||||
class ThemeStorage {
|
@Observable
|
||||||
|
public final class Theme {
|
||||||
|
final class ThemeStorage {
|
||||||
enum ThemeKey: String {
|
enum ThemeKey: String {
|
||||||
case colorScheme, tint, label, primaryBackground, secondaryBackground
|
case colorScheme, tint, label, primaryBackground, secondaryBackground
|
||||||
case avatarPosition2, avatarShape2, statusActionsDisplay, statusDisplayStyle
|
case avatarPosition2, avatarShape2, statusActionsDisplay, statusDisplayStyle
|
||||||
|
@ -69,10 +71,10 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum StatusActionSecondary: String, CaseIterable {
|
public enum StatusActionSecondary: String, CaseIterable {
|
||||||
case share, bookmark
|
case share, bookmark
|
||||||
|
|
||||||
public var description: LocalizedStringKey {
|
public var description: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .share:
|
case .share:
|
||||||
|
@ -167,12 +169,14 @@ import SwiftUI
|
||||||
public var tintColor: Color {
|
public var tintColor: Color {
|
||||||
didSet {
|
didSet {
|
||||||
themeStorage.tintColor = tintColor
|
themeStorage.tintColor = tintColor
|
||||||
|
computeContrastingTintColor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var primaryBackgroundColor: Color {
|
public var primaryBackgroundColor: Color {
|
||||||
didSet {
|
didSet {
|
||||||
themeStorage.primaryBackgroundColor = primaryBackgroundColor
|
themeStorage.primaryBackgroundColor = primaryBackgroundColor
|
||||||
|
computeContrastingTintColor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +189,31 @@ import SwiftUI
|
||||||
public var labelColor: Color {
|
public var labelColor: Color {
|
||||||
didSet {
|
didSet {
|
||||||
themeStorage.labelColor = labelColor
|
themeStorage.labelColor = labelColor
|
||||||
|
computeContrastingTintColor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public private(set) var contrastingTintColor: Color
|
||||||
|
|
||||||
|
// set contrastingTintColor to either labelColor or primaryBackgroundColor, whichever contrasts
|
||||||
|
// better against the tintColor
|
||||||
|
private func computeContrastingTintColor() {
|
||||||
|
func luminance(_ color: Color.Resolved) -> Float {
|
||||||
|
return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedTintColor = tintColor.resolve(in: .init())
|
||||||
|
let resolvedLabelColor = labelColor.resolve(in: .init())
|
||||||
|
let resolvedPrimaryBackgroundColor = primaryBackgroundColor.resolve(in: .init())
|
||||||
|
|
||||||
|
let tintLuminance = luminance(resolvedTintColor)
|
||||||
|
let labelLuminance = luminance(resolvedLabelColor)
|
||||||
|
let primaryBackgroundLuminance = luminance(resolvedPrimaryBackgroundColor)
|
||||||
|
|
||||||
|
if abs(tintLuminance - labelLuminance) > abs(tintLuminance - primaryBackgroundLuminance) {
|
||||||
|
contrastingTintColor = labelColor
|
||||||
|
} else {
|
||||||
|
contrastingTintColor = primaryBackgroundColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,7 +246,7 @@ import SwiftUI
|
||||||
themeStorage.statusDisplayStyle = statusDisplayStyle
|
themeStorage.statusDisplayStyle = statusDisplayStyle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var statusActionSecondary: StatusActionSecondary {
|
public var statusActionSecondary: StatusActionSecondary {
|
||||||
didSet {
|
didSet {
|
||||||
themeStorage.statusActionSecondary = statusActionSecondary
|
themeStorage.statusActionSecondary = statusActionSecondary
|
||||||
|
@ -273,7 +302,7 @@ import SwiftUI
|
||||||
chosenFontData = nil
|
chosenFontData = nil
|
||||||
statusActionSecondary = .share
|
statusActionSecondary = .share
|
||||||
}
|
}
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
isThemePreviouslySet = themeStorage.isThemePreviouslySet
|
isThemePreviouslySet = themeStorage.isThemePreviouslySet
|
||||||
selectedScheme = themeStorage.selectedScheme
|
selectedScheme = themeStorage.selectedScheme
|
||||||
|
@ -281,6 +310,7 @@ import SwiftUI
|
||||||
primaryBackgroundColor = themeStorage.primaryBackgroundColor
|
primaryBackgroundColor = themeStorage.primaryBackgroundColor
|
||||||
secondaryBackgroundColor = themeStorage.secondaryBackgroundColor
|
secondaryBackgroundColor = themeStorage.secondaryBackgroundColor
|
||||||
labelColor = themeStorage.labelColor
|
labelColor = themeStorage.labelColor
|
||||||
|
contrastingTintColor = .red // real work done in computeContrastingTintColor()
|
||||||
avatarPosition = themeStorage.avatarPosition
|
avatarPosition = themeStorage.avatarPosition
|
||||||
avatarShape = themeStorage.avatarShape
|
avatarShape = themeStorage.avatarShape
|
||||||
storedSet = themeStorage.storedSet
|
storedSet = themeStorage.storedSet
|
||||||
|
@ -293,6 +323,8 @@ import SwiftUI
|
||||||
chosenFontData = themeStorage.chosenFontData
|
chosenFontData = themeStorage.chosenFontData
|
||||||
statusActionSecondary = themeStorage.statusActionSecondary
|
statusActionSecondary = themeStorage.statusActionSecondary
|
||||||
selectedSet = storedSet
|
selectedSet = storedSet
|
||||||
|
|
||||||
|
computeContrastingTintColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var allColorSet: [ColorSet] {
|
public static var allColorSet: [ColorSet] {
|
||||||
|
@ -310,7 +342,7 @@ import SwiftUI
|
||||||
ConstellationLight(),
|
ConstellationLight(),
|
||||||
ConstellationDark(),
|
ConstellationDark(),
|
||||||
ThreadsLight(),
|
ThreadsLight(),
|
||||||
ThreadsDark()
|
ThreadsDark(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import SwiftUI
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
func applyTheme(_ theme: Theme) -> some View {
|
@MainActor func applyTheme(_ theme: Theme) -> some View {
|
||||||
modifier(ThemeApplier(theme: theme))
|
modifier(ThemeApplier(theme: theme))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,17 +77,15 @@ struct ThemeApplier: ViewModifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setWindowUserInterfaceStyle(_ userInterfaceStyle: UIUserInterfaceStyle) {
|
private func setWindowUserInterfaceStyle(_ userInterfaceStyle: UIUserInterfaceStyle) {
|
||||||
allWindows()
|
for window in allWindows() {
|
||||||
.forEach {
|
window.overrideUserInterfaceStyle = userInterfaceStyle
|
||||||
$0.overrideUserInterfaceStyle = userInterfaceStyle
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setWindowTint(_ color: Color) {
|
private func setWindowTint(_ color: Color) {
|
||||||
allWindows()
|
for window in allWindows() {
|
||||||
.forEach {
|
window.tintColor = UIColor(color)
|
||||||
$0.tintColor = UIColor(color)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setBarsColor(_ color: Color) {
|
private func setBarsColor(_ color: Color) {
|
||||||
|
|
|
@ -2,9 +2,9 @@ import SwiftUI
|
||||||
|
|
||||||
public struct CancelToolbarItem: ToolbarContent {
|
public struct CancelToolbarItem: ToolbarContent {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
public init() { }
|
public init() {}
|
||||||
|
|
||||||
public var body: some ToolbarContent {
|
public var body: some ToolbarContent {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("action.cancel", role: .cancel, action: { dismiss() })
|
Button("action.cancel", role: .cancel, action: { dismiss() })
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Nuke
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct AccountPopoverView: View {
|
struct AccountPopoverView: View {
|
||||||
let account: Account
|
let account: Account
|
||||||
let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview
|
let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview
|
||||||
|
|
|
@ -33,7 +33,8 @@ public struct AvatarView: View {
|
||||||
self.config = config
|
self.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct FrameConfig: Equatable {
|
@MainActor
|
||||||
|
public struct FrameConfig: Equatable, Sendable {
|
||||||
public let size: CGSize
|
public let size: CGSize
|
||||||
public var width: CGFloat { size.width }
|
public var width: CGFloat { size.width }
|
||||||
public var height: CGFloat { size.height }
|
public var height: CGFloat { size.height }
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
public struct EmptyView: View {
|
|
||||||
public let iconName: String
|
|
||||||
public let title: LocalizedStringKey
|
|
||||||
public let message: LocalizedStringKey
|
|
||||||
|
|
||||||
public init(iconName: String, title: LocalizedStringKey, message: LocalizedStringKey) {
|
|
||||||
self.iconName = iconName
|
|
||||||
self.title = title
|
|
||||||
self.message = message
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
VStack {
|
|
||||||
Image(systemName: iconName)
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(maxHeight: 50)
|
|
||||||
Text(title)
|
|
||||||
.font(.scaledTitle)
|
|
||||||
.padding(.top, 16)
|
|
||||||
Text(message)
|
|
||||||
.font(.scaledSubheadline)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.top, 100)
|
|
||||||
.padding(.layoutPadding)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -42,3 +42,9 @@ public struct ErrorView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ErrorView(title: "Error",
|
||||||
|
message: "Error loading. Please try again",
|
||||||
|
buttonTitle: "Retry") {}
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public struct NextPageView: View {
|
public struct NextPageView: View {
|
||||||
@State private var isLoadingNextPage: Bool = false
|
@State private var isLoadingNextPage: Bool = false
|
||||||
@State private var showRetry: Bool = false
|
@State private var showRetry: Bool = false
|
||||||
|
|
||||||
let loadNextPage: (() async throws -> Void)
|
let loadNextPage: () async throws -> Void
|
||||||
|
|
||||||
public init(loadNextPage: @escaping (() async throws -> Void)) {
|
public init(loadNextPage: @escaping (() async throws -> Void)) {
|
||||||
self.loadNextPage = loadNextPage
|
self.loadNextPage = loadNextPage
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
if showRetry {
|
if showRetry {
|
||||||
|
@ -35,7 +36,7 @@ public struct NextPageView: View {
|
||||||
}
|
}
|
||||||
.listRowSeparator(.hidden, edges: .all)
|
.listRowSeparator(.hidden, edges: .all)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func executeTask() async {
|
private func executeTask() async {
|
||||||
showRetry = false
|
showRetry = false
|
||||||
defer {
|
defer {
|
||||||
|
@ -50,3 +51,11 @@ public struct NextPageView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
List {
|
||||||
|
Text("Item 1")
|
||||||
|
NextPageView {}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct PlaceholderView: View {
|
||||||
|
public let iconName: String
|
||||||
|
public let title: LocalizedStringKey
|
||||||
|
public let message: LocalizedStringKey
|
||||||
|
|
||||||
|
public init(iconName: String, title: LocalizedStringKey, message: LocalizedStringKey) {
|
||||||
|
self.iconName = iconName
|
||||||
|
self.title = title
|
||||||
|
self.message = message
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ContentUnavailableView(title,
|
||||||
|
systemImage: iconName,
|
||||||
|
description: Text(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PlaceholderView(iconName: "square.and.arrow.up.trianglebadge.exclamationmark",
|
||||||
|
title: "Nothing to see",
|
||||||
|
message: "This is a preview. Please try again.")
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ public struct ScrollToView: View {
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
HStack { SwiftUI.EmptyView() }
|
HStack { EmptyView() }
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
.listRowInsets(.init())
|
.listRowInsets(.init())
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
/*! @copyright 2021 Medium */
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// Source: https://www.fivestars.blog/articles/scrollview-offset/
|
|
||||||
|
|
||||||
public struct ScrollViewOffsetReader<Content: View>: View {
|
|
||||||
let onOffsetChange: (CGFloat) -> Void
|
|
||||||
let content: () -> Content
|
|
||||||
|
|
||||||
public init(
|
|
||||||
onOffsetChange: @escaping (CGFloat) -> Void,
|
|
||||||
@ViewBuilder content: @escaping () -> Content
|
|
||||||
) {
|
|
||||||
self.onOffsetChange = onOffsetChange
|
|
||||||
self.content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
ScrollView {
|
|
||||||
offsetReader
|
|
||||||
content()
|
|
||||||
.padding(.top, -8)
|
|
||||||
}
|
|
||||||
.coordinateSpace(name: "frameLayer")
|
|
||||||
.onPreferenceChange(OffsetPreferenceKey.self, perform: onOffsetChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
var offsetReader: some View {
|
|
||||||
GeometryReader { proxy in
|
|
||||||
Color.clear
|
|
||||||
.preference(
|
|
||||||
key: OffsetPreferenceKey.self,
|
|
||||||
value: proxy.frame(in: .named("frameLayer")).minY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.frame(height: 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct OffsetPreferenceKey: PreferenceKey {
|
|
||||||
static var defaultValue: CGFloat = .zero
|
|
||||||
static func reduce(value _: inout CGFloat, nextValue _: () -> CGFloat) {}
|
|
||||||
}
|
|
|
@ -56,12 +56,12 @@ public extension EnvironmentValues {
|
||||||
get { self[IsCompact.self] }
|
get { self[IsCompact.self] }
|
||||||
set { self[IsCompact.self] = newValue }
|
set { self[IsCompact.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var isMediaCompact: Bool {
|
var isMediaCompact: Bool {
|
||||||
get { self[IsMediaCompact.self] }
|
get { self[IsMediaCompact.self] }
|
||||||
set { self[IsMediaCompact.self] = newValue }
|
set { self[IsMediaCompact.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var isModal: Bool {
|
var isModal: Bool {
|
||||||
get { self[IsModal.self] }
|
get { self[IsModal.self] }
|
||||||
set { self[IsModal.self] = newValue }
|
set { self[IsModal.self] = newValue }
|
||||||
|
|
|
@ -6,65 +6,65 @@ public class HapticManager {
|
||||||
public static let shared: HapticManager = .init()
|
public static let shared: HapticManager = .init()
|
||||||
|
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
public enum FeedbackType: Int {
|
public enum FeedbackType: Int {
|
||||||
case success, warning, error
|
case success, warning, error
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public enum HapticType {
|
public enum HapticType {
|
||||||
case buttonPress
|
case buttonPress
|
||||||
case dataRefresh(intensity: CGFloat)
|
case dataRefresh(intensity: CGFloat)
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
case notification(_ type: FeedbackType)
|
case notification(_ type: FeedbackType)
|
||||||
#else
|
#else
|
||||||
case notification(_ type: UINotificationFeedbackGenerator.FeedbackType)
|
case notification(_ type: UINotificationFeedbackGenerator.FeedbackType)
|
||||||
#endif
|
#endif
|
||||||
case tabSelection
|
case tabSelection
|
||||||
case timeline
|
case timeline
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
private let selectionGenerator = UISelectionFeedbackGenerator()
|
private let selectionGenerator = UISelectionFeedbackGenerator()
|
||||||
private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
||||||
private let notificationGenerator = UINotificationFeedbackGenerator()
|
private let notificationGenerator = UINotificationFeedbackGenerator()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private let userPreferences = UserPreferences.shared
|
private let userPreferences = UserPreferences.shared
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
selectionGenerator.prepare()
|
selectionGenerator.prepare()
|
||||||
impactGenerator.prepare()
|
impactGenerator.prepare()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public func fireHaptic(_ type: HapticType) {
|
public func fireHaptic(_ type: HapticType) {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
guard supportsHaptics else { return }
|
guard supportsHaptics else { return }
|
||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
case .buttonPress:
|
case .buttonPress:
|
||||||
if userPreferences.hapticButtonPressEnabled {
|
if userPreferences.hapticButtonPressEnabled {
|
||||||
impactGenerator.impactOccurred()
|
impactGenerator.impactOccurred()
|
||||||
|
}
|
||||||
|
case let .dataRefresh(intensity):
|
||||||
|
if userPreferences.hapticTimelineEnabled {
|
||||||
|
impactGenerator.impactOccurred(intensity: intensity)
|
||||||
|
}
|
||||||
|
case let .notification(type):
|
||||||
|
if userPreferences.hapticButtonPressEnabled {
|
||||||
|
notificationGenerator.notificationOccurred(type)
|
||||||
|
}
|
||||||
|
case .tabSelection:
|
||||||
|
if userPreferences.hapticTabSelectionEnabled {
|
||||||
|
selectionGenerator.selectionChanged()
|
||||||
|
}
|
||||||
|
case .timeline:
|
||||||
|
if userPreferences.hapticTimelineEnabled {
|
||||||
|
selectionGenerator.selectionChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case let .dataRefresh(intensity):
|
|
||||||
if userPreferences.hapticTimelineEnabled {
|
|
||||||
impactGenerator.impactOccurred(intensity: intensity)
|
|
||||||
}
|
|
||||||
case let .notification(type):
|
|
||||||
if userPreferences.hapticButtonPressEnabled {
|
|
||||||
notificationGenerator.notificationOccurred(type)
|
|
||||||
}
|
|
||||||
case .tabSelection:
|
|
||||||
if userPreferences.hapticTabSelectionEnabled {
|
|
||||||
selectionGenerator.selectionChanged()
|
|
||||||
}
|
|
||||||
case .timeline:
|
|
||||||
if userPreferences.hapticTimelineEnabled {
|
|
||||||
selectionGenerator.selectionChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
15
Packages/Env/Sources/Env/PreviewEnv.swift
Normal file
15
Packages/Env/Sources/Env/PreviewEnv.swift
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public extension View {
|
||||||
|
func withPreviewsEnv() -> some View {
|
||||||
|
environment(RouterPath())
|
||||||
|
.environment(Client(server: ""))
|
||||||
|
.environment(CurrentAccount.shared)
|
||||||
|
.environment(UserPreferences.shared)
|
||||||
|
.environment(CurrentInstance.shared)
|
||||||
|
.environment(PushNotificationsService.shared)
|
||||||
|
.environment(QuickLook.shared)
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,15 @@ public enum WindowDestinationMedia: Hashable, Codable {
|
||||||
case mediaViewer(attachments: [MediaAttachment], selectedAttachment: MediaAttachment)
|
case mediaViewer(attachments: [MediaAttachment], selectedAttachment: MediaAttachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SheetDestination: Identifiable {
|
public enum SheetDestination: Identifiable, Hashable {
|
||||||
|
public static func == (lhs: SheetDestination, rhs: SheetDestination) -> Bool {
|
||||||
|
lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
|
||||||
case newStatusEditor(visibility: Models.Visibility)
|
case newStatusEditor(visibility: Models.Visibility)
|
||||||
case editStatusEditor(status: Status)
|
case editStatusEditor(status: Status)
|
||||||
case replyToStatusEditor(status: Status)
|
case replyToStatusEditor(status: Status)
|
||||||
|
@ -60,11 +68,13 @@ public enum SheetDestination: Identifiable {
|
||||||
case shareImage(image: UIImage, status: Status)
|
case shareImage(image: UIImage, status: Status)
|
||||||
case editTagGroup(tagGroup: TagGroup, onSaved: ((TagGroup) -> Void)?)
|
case editTagGroup(tagGroup: TagGroup, onSaved: ((TagGroup) -> Void)?)
|
||||||
case timelineContentFilter
|
case timelineContentFilter
|
||||||
|
case accountEditInfo
|
||||||
|
case accountFiltersList
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
|
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
|
||||||
.mentionStatusEditor, .quoteLinkStatusEditor:
|
.mentionStatusEditor, .quoteLinkStatusEditor:
|
||||||
"statusEditor"
|
"statusEditor"
|
||||||
case .listCreate:
|
case .listCreate:
|
||||||
"listCreate"
|
"listCreate"
|
||||||
|
@ -90,6 +100,10 @@ public enum SheetDestination: Identifiable {
|
||||||
"settings"
|
"settings"
|
||||||
case .timelineContentFilter:
|
case .timelineContentFilter:
|
||||||
"timelineContentFilter"
|
"timelineContentFilter"
|
||||||
|
case .accountEditInfo:
|
||||||
|
"accountEditInfo"
|
||||||
|
case .accountFiltersList:
|
||||||
|
"accountFiltersList"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,8 +161,9 @@ public enum SheetDestination: Identifiable {
|
||||||
navigate(to: .hashTag(tag: tag, account: nil))
|
navigate(to: .hashTag(tag: tag, account: nil))
|
||||||
return .handled
|
return .handled
|
||||||
} else if url.lastPathComponent.first == "@",
|
} else if url.lastPathComponent.first == "@",
|
||||||
let host = url.host,
|
let host = url.host,
|
||||||
!host.hasPrefix("www") {
|
!host.hasPrefix("www")
|
||||||
|
{
|
||||||
let acct = "\(url.lastPathComponent)@\(host)"
|
let acct = "\(url.lastPathComponent)@\(host)"
|
||||||
Task {
|
Task {
|
||||||
await navigateToAccountFrom(acct: acct, url: url)
|
await navigateToAccountFrom(acct: acct, url: url)
|
||||||
|
|
|
@ -27,11 +27,11 @@ import OSLog
|
||||||
public var events: [any StreamEvent] = []
|
public var events: [any StreamEvent] = []
|
||||||
public var unreadNotificationsCount: Int = 0
|
public var unreadNotificationsCount: Int = 0
|
||||||
public var latestEvent: (any StreamEvent)?
|
public var latestEvent: (any StreamEvent)?
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.icecubesapp", category: "stream")
|
private let logger = Logger(subsystem: "com.icecubesapp", category: "stream")
|
||||||
|
|
||||||
public static let shared = StreamWatcher()
|
public static let shared = StreamWatcher()
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ import OSLog
|
||||||
connect()
|
connect()
|
||||||
}
|
}
|
||||||
watchedStreams = streams
|
watchedStreams = streams
|
||||||
streams.forEach { stream in
|
for stream in streams {
|
||||||
sendMessage(message: StreamMessage(type: "subscribe", stream: stream.rawValue))
|
sendMessage(message: StreamMessage(type: "subscribe", stream: stream.rawValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,22 +156,22 @@ import OSLog
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func emmitDeleteEvent(for status: String) {
|
public func emmitDeleteEvent(for status: String) {
|
||||||
let event = StreamEventDelete(status: status)
|
let event = StreamEventDelete(status: status)
|
||||||
self.events.append(event)
|
events.append(event)
|
||||||
self.latestEvent = event
|
latestEvent = event
|
||||||
}
|
}
|
||||||
|
|
||||||
public func emmitEditEvent(for status: Status) {
|
public func emmitEditEvent(for status: Status) {
|
||||||
let event = StreamEventStatusUpdate(status: status)
|
let event = StreamEventStatusUpdate(status: status)
|
||||||
self.events.append(event)
|
events.append(event)
|
||||||
self.latestEvent = event
|
latestEvent = event
|
||||||
}
|
}
|
||||||
|
|
||||||
public func emmitPostEvent(for status: Status) {
|
public func emmitPostEvent(for status: Status) {
|
||||||
let event = StreamEventUpdate(status: status)
|
let event = StreamEventUpdate(status: status)
|
||||||
self.events.append(event)
|
events.append(event)
|
||||||
self.latestEvent = event
|
latestEvent = event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ import SwiftUI
|
||||||
@AppStorage("collapse-long-posts") public var collapseLongPosts = true
|
@AppStorage("collapse-long-posts") public var collapseLongPosts = true
|
||||||
|
|
||||||
@AppStorage("share-button-behavior") public var shareButtonBehavior: PreferredShareButtonBehavior = .linkOnly
|
@AppStorage("share-button-behavior") public var shareButtonBehavior: PreferredShareButtonBehavior = .linkOnly
|
||||||
|
|
||||||
@AppStorage("fast_refresh") public var fastRefreshEnabled: Bool = false
|
@AppStorage("fast_refresh") public var fastRefreshEnabled: Bool = false
|
||||||
|
|
||||||
@AppStorage("max_reply_indentation") public var maxReplyIndentation: UInt = 7
|
@AppStorage("max_reply_indentation") public var maxReplyIndentation: UInt = 7
|
||||||
|
@ -164,7 +164,7 @@ import SwiftUI
|
||||||
storage.appDefaultPostsSensitive = appDefaultPostsSensitive
|
storage.appDefaultPostsSensitive = appDefaultPostsSensitive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var appRequireAltText: Bool {
|
public var appRequireAltText: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
storage.appRequireAltText = appRequireAltText
|
storage.appRequireAltText = appRequireAltText
|
||||||
|
@ -183,7 +183,6 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public var alwaysUseDeepl: Bool {
|
public var alwaysUseDeepl: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
storage.alwaysUseDeepl = alwaysUseDeepl
|
storage.alwaysUseDeepl = alwaysUseDeepl
|
||||||
|
@ -303,7 +302,7 @@ import SwiftUI
|
||||||
storage.shareButtonBehavior = shareButtonBehavior
|
storage.shareButtonBehavior = shareButtonBehavior
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var fastRefreshEnabled: Bool {
|
public var fastRefreshEnabled: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
storage.fastRefreshEnabled = fastRefreshEnabled
|
storage.fastRefreshEnabled = fastRefreshEnabled
|
||||||
|
@ -415,7 +414,7 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
|
|
||||||
public var totalNotificationsCount: Int {
|
public var totalNotificationsCount: Int {
|
||||||
notificationsCount.compactMap{ $0.value }.reduce(0, +)
|
notificationsCount.compactMap { $0.value }.reduce(0, +)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reloadNotificationsCount(tokens: [OauthToken]) {
|
public func reloadNotificationsCount(tokens: [OauthToken]) {
|
||||||
|
@ -509,7 +508,7 @@ extension UInt: RawRepresentable {
|
||||||
public var rawValue: Int {
|
public var rawValue: Int {
|
||||||
Int(self)
|
Int(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init?(rawValue: Int) {
|
public init?(rawValue: Int) {
|
||||||
if rawValue >= 0 {
|
if rawValue >= 0 {
|
||||||
self.init(rawValue)
|
self.init(rawValue)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@testable import Env
|
@testable import Env
|
||||||
import XCTest
|
|
||||||
import SwiftUI
|
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
import XCTest
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class RouterTests: XCTestCase {
|
final class RouterTests: XCTestCase {
|
||||||
|
@ -11,14 +11,14 @@ final class RouterTests: XCTestCase {
|
||||||
_ = router.handle(url: url)
|
_ = router.handle(url: url)
|
||||||
XCTAssertTrue(router.path.isEmpty)
|
XCTAssertTrue(router.path.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRouterTagsURL() {
|
func testRouterTagsURL() {
|
||||||
let router = RouterPath()
|
let router = RouterPath()
|
||||||
let url = URL(string: "https://mastodon.social/tags/test")!
|
let url = URL(string: "https://mastodon.social/tags/test")!
|
||||||
_ = router.handle(url: url)
|
_ = router.handle(url: url)
|
||||||
XCTAssertTrue(router.path.first == .hashTag(tag: "test", account: nil))
|
XCTAssertTrue(router.path.first == .hashTag(tag: "test", account: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRouterLocalStatusURL() {
|
func testRouterLocalStatusURL() {
|
||||||
let router = RouterPath()
|
let router = RouterPath()
|
||||||
let client = Client(server: "mastodon.social",
|
let client = Client(server: "mastodon.social",
|
||||||
|
@ -29,7 +29,7 @@ final class RouterTests: XCTestCase {
|
||||||
_ = router.handle(url: url)
|
_ = router.handle(url: url)
|
||||||
XCTAssertTrue(router.path.first == .statusDetail(id: "1010384"))
|
XCTAssertTrue(router.path.first == .statusDetail(id: "1010384"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRouterRemoteStatusURL() {
|
func testRouterRemoteStatusURL() {
|
||||||
let router = RouterPath()
|
let router = RouterPath()
|
||||||
let client = Client(server: "mastodon.social",
|
let client = Client(server: "mastodon.social",
|
||||||
|
@ -40,7 +40,7 @@ final class RouterTests: XCTestCase {
|
||||||
_ = router.handle(url: url)
|
_ = router.handle(url: url)
|
||||||
XCTAssertTrue(router.path.first == .remoteStatusDetail(url: url))
|
XCTAssertTrue(router.path.first == .remoteStatusDetail(url: url))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRouteRandomURL() {
|
func testRouteRandomURL() {
|
||||||
let router = RouterPath()
|
let router = RouterPath()
|
||||||
let url = URL(string: "https://theweb.com/test/test/one")!
|
let url = URL(string: "https://theweb.com/test/test/one")!
|
||||||
|
|
|
@ -32,9 +32,9 @@ public struct ExploreView: View {
|
||||||
} else if !viewModel.searchQuery.isEmpty {
|
} else if !viewModel.searchQuery.isEmpty {
|
||||||
if let results = viewModel.results[viewModel.searchQuery] {
|
if let results = viewModel.results[viewModel.searchQuery] {
|
||||||
if results.isEmpty, !viewModel.isSearching {
|
if results.isEmpty, !viewModel.isSearching {
|
||||||
EmptyView(iconName: "magnifyingglass",
|
PlaceholderView(iconName: "magnifyingglass",
|
||||||
title: "explore.search.empty.title",
|
title: "explore.search.empty.title",
|
||||||
message: "explore.search.empty.message")
|
message: "explore.search.empty.message")
|
||||||
.listRowBackground(theme.secondaryBackgroundColor)
|
.listRowBackground(theme.secondaryBackgroundColor)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
} else {
|
} else {
|
||||||
|
@ -53,12 +53,12 @@ public struct ExploreView: View {
|
||||||
.id(UUID())
|
.id(UUID())
|
||||||
}
|
}
|
||||||
} else if viewModel.allSectionsEmpty {
|
} else if viewModel.allSectionsEmpty {
|
||||||
EmptyView(iconName: "magnifyingglass",
|
PlaceholderView(iconName: "magnifyingglass",
|
||||||
title: "explore.search.title",
|
title: "explore.search.title",
|
||||||
message: "explore.search.message-\(client.server)")
|
message: "explore.search.message-\(client.server)")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.secondaryBackgroundColor)
|
.listRowBackground(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
} else {
|
} else {
|
||||||
quickAccessView
|
quickAccessView
|
||||||
|
@ -94,32 +94,32 @@ public struct ExploreView: View {
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.navigationTitle("explore.navigation-title")
|
.navigationTitle("explore.navigation-title")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.searchable(text: $viewModel.searchQuery,
|
.searchable(text: $viewModel.searchQuery,
|
||||||
isPresented: $viewModel.isSearchPresented,
|
isPresented: $viewModel.isSearchPresented,
|
||||||
placement: .navigationBarDrawer(displayMode: .always),
|
placement: .navigationBarDrawer(displayMode: .always),
|
||||||
prompt: Text("explore.search.prompt"))
|
prompt: Text("explore.search.prompt"))
|
||||||
.searchScopes($viewModel.searchScope) {
|
.searchScopes($viewModel.searchScope) {
|
||||||
ForEach(ExploreViewModel.SearchScope.allCases, id: \.self) { scope in
|
ForEach(ExploreViewModel.SearchScope.allCases, id: \.self) { scope in
|
||||||
Text(scope.localizedString)
|
Text(scope.localizedString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task(id: viewModel.searchQuery) {
|
.task(id: viewModel.searchQuery) {
|
||||||
await viewModel.search()
|
await viewModel.search()
|
||||||
}
|
}
|
||||||
.onChange(of: scrollToTopSignal) {
|
.onChange(of: scrollToTopSignal) {
|
||||||
if viewModel.scrollToTopVisible {
|
if viewModel.scrollToTopVisible {
|
||||||
viewModel.isSearchPresented.toggle()
|
viewModel.isSearchPresented.toggle()
|
||||||
} else {
|
} else {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,9 +148,9 @@ public struct ExploreView: View {
|
||||||
.scrollIndicators(.never)
|
.scrollIndicators(.never)
|
||||||
.listRowInsets(EdgeInsets())
|
.listRowInsets(EdgeInsets())
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.secondaryBackgroundColor)
|
.listRowBackground(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingView: some View {
|
private var loadingView: some View {
|
||||||
|
@ -159,9 +159,9 @@ public struct ExploreView: View {
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,13 +172,13 @@ public struct ExploreView: View {
|
||||||
ForEach(results.accounts) { account in
|
ForEach(results.accounts) { account in
|
||||||
if let relationship = results.relationships.first(where: { $0.id == account.id }) {
|
if let relationship = results.relationships.first(where: { $0.id == account.id }) {
|
||||||
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,13 +187,13 @@ public struct ExploreView: View {
|
||||||
Section("explore.section.tags") {
|
Section("explore.section.tags") {
|
||||||
ForEach(results.hashtags) { tag in
|
ForEach(results.hashtags) { tag in
|
||||||
TagRowView(tag: tag)
|
TagRowView(tag: tag)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,13 +202,13 @@ public struct ExploreView: View {
|
||||||
Section("explore.section.posts") {
|
Section("explore.section.posts") {
|
||||||
ForEach(results.statuses) { status in
|
ForEach(results.statuses) { status in
|
||||||
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
|
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -222,13 +222,13 @@ public struct ExploreView: View {
|
||||||
{ account in
|
{ account in
|
||||||
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
||||||
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) {
|
NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) {
|
||||||
|
@ -239,7 +239,7 @@ public struct ExploreView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -251,13 +251,13 @@ public struct ExploreView: View {
|
||||||
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count))
|
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count))
|
||||||
{ tag in
|
{ tag in
|
||||||
TagRowView(tag: tag)
|
TagRowView(tag: tag)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) {
|
NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) {
|
||||||
|
@ -268,7 +268,7 @@ public struct ExploreView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -280,13 +280,13 @@ public struct ExploreView: View {
|
||||||
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count))
|
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count))
|
||||||
{ status in
|
{ status in
|
||||||
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
|
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,7 +298,7 @@ public struct ExploreView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -311,13 +311,13 @@ public struct ExploreView: View {
|
||||||
{ card in
|
{ card in
|
||||||
StatusRowCardView(card: card)
|
StatusRowCardView(card: card)
|
||||||
.environment(\.isCompact, true)
|
.environment(\.isCompact, true)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ public struct ExploreView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
.listRowBackground(RoundedRectangle(cornerRadius: 8)
|
||||||
.foregroundStyle(.background).hoverEffect())
|
.foregroundStyle(.background).hoverEffect())
|
||||||
.listRowHoverEffectDisabled()
|
.listRowHoverEffectDisabled()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,9 @@ public struct TagsListView: View {
|
||||||
List {
|
List {
|
||||||
ForEach(tags) { tag in
|
ForEach(tags) { tag in
|
||||||
TagRowView(tag: tag)
|
TagRowView(tag: tag)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Models
|
import Models
|
||||||
|
import Network
|
||||||
import StatusKit
|
import StatusKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Network
|
|
||||||
|
|
||||||
public struct TrendingLinksListView: View {
|
public struct TrendingLinksListView: View {
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
@ -19,9 +19,9 @@ public struct TrendingLinksListView: View {
|
||||||
ForEach(links) { card in
|
ForEach(links) { card in
|
||||||
StatusRowCardView(card: card)
|
StatusRowCardView(card: card)
|
||||||
.environment(\.isCompact, true)
|
.environment(\.isCompact, true)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
NextPageView {
|
NextPageView {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
enum DisplayType {
|
enum DisplayType {
|
||||||
case image
|
case image
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import AVKit
|
import AVKit
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
import Models
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Models
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable public class MediaUIAttachmentVideoViewModel {
|
@Observable public class MediaUIAttachmentVideoViewModel {
|
||||||
|
@ -21,9 +21,9 @@ import Models
|
||||||
player = .init(url: url)
|
player = .init(url: url)
|
||||||
player?.audiovisualBackgroundPlaybackPolicy = .pauses
|
player?.audiovisualBackgroundPlaybackPolicy = .pauses
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
player?.preventsDisplaySleepDuringVideoPlayback = false
|
player?.preventsDisplaySleepDuringVideoPlayback = false
|
||||||
#endif
|
#endif
|
||||||
if (autoPlay || forceAutoPlay) && !isCompact {
|
if autoPlay || forceAutoPlay, !isCompact {
|
||||||
player?.play()
|
player?.play()
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
} else {
|
} else {
|
||||||
|
@ -41,7 +41,7 @@ import Models
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mute(_ mute: Bool) {
|
func mute(_ mute: Bool) {
|
||||||
player?.isMuted = mute
|
player?.isMuted = mute
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ import Models
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
player?.pause()
|
player?.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
player?.pause()
|
player?.pause()
|
||||||
|
@ -62,15 +62,15 @@ import Models
|
||||||
player?.seek(to: CMTime.zero)
|
player?.seek(to: CMTime.zero)
|
||||||
player?.play()
|
player?.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
func resume() {
|
func resume() {
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
player?.play()
|
player?.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
func preventSleep(_ preventSleep: Bool) {
|
func preventSleep(_ preventSleep: Bool) {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
player?.preventsDisplaySleepDuringVideoPlayback = preventSleep
|
player?.preventsDisplaySleepDuringVideoPlayback = preventSleep
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,90 +96,91 @@ public struct MediaUIAttachmentVideoView: View {
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
videoView
|
videoView
|
||||||
.onAppear {
|
|
||||||
viewModel.preparePlayer(autoPlay: isFullScreen ? true : preferences.autoPlayVideo,
|
|
||||||
isCompact: isCompact)
|
|
||||||
viewModel.mute(preferences.muteVideo)
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
viewModel.stop()
|
|
||||||
}
|
|
||||||
.onTapGesture {
|
|
||||||
if !preferences.autoPlayVideo && !viewModel.isPlaying {
|
|
||||||
viewModel.play()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
viewModel.pause()
|
|
||||||
let attachement = MediaAttachment.videoWith(url: viewModel.url)
|
|
||||||
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement], selectedAttachment: attachement))
|
|
||||||
#else
|
|
||||||
isFullScreen = true
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
.fullScreenCover(isPresented: $isFullScreen) {
|
|
||||||
NavigationStack {
|
|
||||||
videoView
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .topBarLeading) {
|
|
||||||
Button { isFullScreen.toggle() } label: {
|
|
||||||
Image(systemName: "xmark.circle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QuickLookToolbarItem(itemUrl: viewModel.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
DispatchQueue.global().async {
|
viewModel.preparePlayer(autoPlay: isFullScreen ? true : preferences.autoPlayVideo,
|
||||||
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
isCompact: isCompact)
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .duckOthers)
|
viewModel.mute(preferences.muteVideo)
|
||||||
try? AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
}
|
|
||||||
viewModel.preventSleep(true)
|
|
||||||
viewModel.mute(false)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
||||||
if isCompact || !preferences.autoPlayVideo {
|
|
||||||
viewModel.play()
|
|
||||||
} else {
|
|
||||||
viewModel.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
if isCompact || !preferences.autoPlayVideo {
|
viewModel.stop()
|
||||||
viewModel.pause()
|
|
||||||
}
|
|
||||||
viewModel.preventSleep(false)
|
|
||||||
viewModel.mute(preferences.muteVideo)
|
|
||||||
DispatchQueue.global().async {
|
|
||||||
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
|
||||||
try? AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
.onTapGesture {
|
||||||
.cornerRadius(4)
|
if !preferences.autoPlayVideo && !viewModel.isPlaying {
|
||||||
.onChange(of: scenePhase) { _, newValue in
|
|
||||||
switch newValue {
|
|
||||||
case .background, .inactive:
|
|
||||||
viewModel.pause()
|
|
||||||
case .active:
|
|
||||||
if (preferences.autoPlayVideo || viewModel.forceAutoPlay || isFullScreen) && !isCompact {
|
|
||||||
viewModel.play()
|
viewModel.play()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
viewModel.pause()
|
||||||
|
let attachement = MediaAttachment.videoWith(url: viewModel.url)
|
||||||
|
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement], selectedAttachment: attachement))
|
||||||
|
#else
|
||||||
|
isFullScreen = true
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.fullScreenCover(isPresented: $isFullScreen) {
|
||||||
|
NavigationStack {
|
||||||
|
videoView
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
|
Button { isFullScreen.toggle() } label: {
|
||||||
|
Image(systemName: "xmark.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QuickLookToolbarItem(itemUrl: viewModel.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .duckOthers)
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
}
|
||||||
|
viewModel.preventSleep(true)
|
||||||
|
viewModel.mute(false)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
|
if isCompact || !preferences.autoPlayVideo {
|
||||||
|
viewModel.play()
|
||||||
|
} else {
|
||||||
|
viewModel.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
if isCompact || !preferences.autoPlayVideo {
|
||||||
|
viewModel.pause()
|
||||||
|
}
|
||||||
|
viewModel.preventSleep(false)
|
||||||
|
viewModel.mute(preferences.muteVideo)
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cornerRadius(4)
|
||||||
|
.onChange(of: scenePhase) { _, newValue in
|
||||||
|
switch newValue {
|
||||||
|
case .background, .inactive:
|
||||||
|
viewModel.pause()
|
||||||
|
case .active:
|
||||||
|
if (preferences.autoPlayVideo || viewModel.forceAutoPlay || isFullScreen) && !isCompact {
|
||||||
|
viewModel.play()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var videoView: some View {
|
private var videoView: some View {
|
||||||
VideoPlayer(player: viewModel.player, videoOverlay: {
|
VideoPlayer(player: viewModel.player, videoOverlay: {
|
||||||
if !preferences.autoPlayVideo,
|
if !preferences.autoPlayVideo,
|
||||||
!viewModel.forceAutoPlay,
|
!viewModel.forceAutoPlay,
|
||||||
!isFullScreen,
|
!isFullScreen,
|
||||||
!viewModel.isPlaying,
|
!viewModel.isPlaying,
|
||||||
!isCompact {
|
!isCompact
|
||||||
|
{
|
||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.play()
|
viewModel.play()
|
||||||
}, label: {
|
}, label: {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import AVFoundation
|
||||||
import Models
|
import Models
|
||||||
import Nuke
|
import Nuke
|
||||||
import QuickLook
|
import QuickLook
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
public struct MediaUIView: View, @unchecked Sendable {
|
public struct MediaUIView: View, @unchecked Sendable {
|
||||||
private let data: [DisplayData]
|
private let data: [DisplayData]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import SwiftUI
|
|
||||||
import NukeUI
|
|
||||||
import Nuke
|
import Nuke
|
||||||
|
import NukeUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct QuickLookToolbarItem: ToolbarContent, @unchecked Sendable {
|
struct QuickLookToolbarItem: ToolbarContent, @unchecked Sendable {
|
||||||
let itemUrl: URL
|
let itemUrl: URL
|
||||||
|
|
|
@ -61,6 +61,7 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
|
||||||
public let source: Source?
|
public let source: Source?
|
||||||
public let bot: Bool
|
public let bot: Bool
|
||||||
public let discoverable: Bool?
|
public let discoverable: Bool?
|
||||||
|
public let moved: Account?
|
||||||
|
|
||||||
public var haveAvatar: Bool {
|
public var haveAvatar: Bool {
|
||||||
avatar.lastPathComponent != "missing.png"
|
avatar.lastPathComponent != "missing.png"
|
||||||
|
@ -70,7 +71,7 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
|
||||||
header.lastPathComponent != "missing.png"
|
header.lastPathComponent != "missing.png"
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil) {
|
public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil, moved: Account? = nil) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.username = username
|
self.username = username
|
||||||
self.displayName = displayName
|
self.displayName = displayName
|
||||||
|
@ -90,14 +91,15 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
|
||||||
self.source = source
|
self.source = source
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.discoverable = discoverable
|
self.discoverable = discoverable
|
||||||
|
self.moved = moved
|
||||||
|
|
||||||
if let displayName, !displayName.isEmpty {
|
if let displayName, !displayName.isEmpty {
|
||||||
self.cachedDisplayName = .init(stringValue: displayName)
|
cachedDisplayName = .init(stringValue: displayName)
|
||||||
} else {
|
} else {
|
||||||
self.cachedDisplayName = .init(stringValue: "@\(username)")
|
cachedDisplayName = .init(stringValue: "@\(username)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CodingKeys: CodingKey {
|
public enum CodingKeys: CodingKey {
|
||||||
case id
|
case id
|
||||||
case username
|
case username
|
||||||
|
@ -118,33 +120,36 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
|
||||||
case source
|
case source
|
||||||
case bot
|
case bot
|
||||||
case discoverable
|
case discoverable
|
||||||
|
case moved
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
id = try container.decode(String.self, forKey: .id)
|
||||||
self.username = try container.decode(String.self, forKey: .username)
|
username = try container.decode(String.self, forKey: .username)
|
||||||
self.displayName = try container.decodeIfPresent(String.self, forKey: .displayName)
|
displayName = try container.decodeIfPresent(String.self, forKey: .displayName)
|
||||||
self.avatar = try container.decode(URL.self, forKey: .avatar)
|
avatar = try container.decode(URL.self, forKey: .avatar)
|
||||||
self.header = try container.decode(URL.self, forKey: .header)
|
header = try container.decode(URL.self, forKey: .header)
|
||||||
self.acct = try container.decode(String.self, forKey: .acct)
|
acct = try container.decode(String.self, forKey: .acct)
|
||||||
self.note = try container.decode(HTMLString.self, forKey: .note)
|
note = try container.decode(HTMLString.self, forKey: .note)
|
||||||
self.createdAt = try container.decode(ServerDate.self, forKey: .createdAt)
|
createdAt = try container.decode(ServerDate.self, forKey: .createdAt)
|
||||||
self.followersCount = try container.decodeIfPresent(Int.self, forKey: .followersCount)
|
followersCount = try container.decodeIfPresent(Int.self, forKey: .followersCount)
|
||||||
self.followingCount = try container.decodeIfPresent(Int.self, forKey: .followingCount)
|
followingCount = try container.decodeIfPresent(Int.self, forKey: .followingCount)
|
||||||
self.statusesCount = try container.decodeIfPresent(Int.self, forKey: .statusesCount)
|
statusesCount = try container.decodeIfPresent(Int.self, forKey: .statusesCount)
|
||||||
self.lastStatusAt = try container.decodeIfPresent(String.self, forKey: .lastStatusAt)
|
lastStatusAt = try container.decodeIfPresent(String.self, forKey: .lastStatusAt)
|
||||||
self.fields = try container.decode([Account.Field].self, forKey: .fields)
|
fields = try container.decode([Account.Field].self, forKey: .fields)
|
||||||
self.locked = try container.decode(Bool.self, forKey: .locked)
|
locked = try container.decode(Bool.self, forKey: .locked)
|
||||||
self.emojis = try container.decode([Emoji].self, forKey: .emojis)
|
emojis = try container.decode([Emoji].self, forKey: .emojis)
|
||||||
self.url = try container.decodeIfPresent(URL.self, forKey: .url)
|
url = try container.decodeIfPresent(URL.self, forKey: .url)
|
||||||
self.source = try container.decodeIfPresent(Account.Source.self, forKey: .source)
|
source = try container.decodeIfPresent(Account.Source.self, forKey: .source)
|
||||||
self.bot = try container.decode(Bool.self, forKey: .bot)
|
bot = try container.decode(Bool.self, forKey: .bot)
|
||||||
self.discoverable = try container.decodeIfPresent(Bool.self, forKey: .discoverable)
|
discoverable = try container.decodeIfPresent(Bool.self, forKey: .discoverable)
|
||||||
|
moved = try container.decodeIfPresent(Account.self, forKey: .moved)
|
||||||
|
|
||||||
if let displayName, !displayName.isEmpty {
|
if let displayName, !displayName.isEmpty {
|
||||||
self.cachedDisplayName = .init(stringValue: displayName)
|
cachedDisplayName = .init(stringValue: displayName)
|
||||||
} else {
|
} else {
|
||||||
self.cachedDisplayName = .init(stringValue: "@\(username)")
|
cachedDisplayName = .init(stringValue: "@\(username)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,14 @@ public struct ServerDate: Codable, Hashable, Equatable, Sendable {
|
||||||
relativeTo: Date())
|
relativeTo: Date())
|
||||||
} else {
|
} else {
|
||||||
return Duration.seconds(-date.timeIntervalSinceNow).formatted(.units(width: .narrow,
|
return Duration.seconds(-date.timeIntervalSinceNow).formatted(.units(width: .narrow,
|
||||||
maximumUnitCount: 1))
|
maximumUnitCount: 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var shortDateFormatted: String {
|
public var shortDateFormatted: String {
|
||||||
DateFormatterCache.shared.createdAtShortDateFormatted.string(from: asDate)
|
DateFormatterCache.shared.createdAtShortDateFormatted.string(from: asDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let calendar = Calendar(identifier: .gregorian)
|
private static let calendar = Calendar(identifier: .gregorian)
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
|
@ -41,7 +41,7 @@ public struct ServerDate: Codable, Hashable, Equatable, Sendable {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
asDate = try container.decode(Date.self, forKey: .asDate)
|
asDate = try container.decode(Date.self, forKey: .asDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
let aDay: TimeInterval = 60 * 60 * 24
|
let aDay: TimeInterval = 60 * 60 * 24
|
||||||
isOlderThanADay = Date().timeIntervalSince(asDate) >= aDay
|
isOlderThanADay = Date().timeIntervalSince(asDate) >= aDay
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,8 @@ public struct ConsolidatedNotification: Identifiable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func placeholders() -> [ConsolidatedNotification] {
|
public static func placeholders() -> [ConsolidatedNotification] {
|
||||||
[.placeholder(), .placeholder(), .placeholder(),
|
[.placeholder(), .placeholder(), .placeholder(),
|
||||||
.placeholder(), .placeholder(), .placeholder(),
|
.placeholder(), .placeholder(), .placeholder(),
|
||||||
.placeholder(), .placeholder(), .placeholder(),
|
.placeholder(), .placeholder(), .placeholder(),
|
||||||
.placeholder(), .placeholder(), .placeholder()]
|
.placeholder(), .placeholder(), .placeholder()]
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ public struct List: Codable, Identifiable, Equatable, Hashable {
|
||||||
|
|
||||||
case followed, list, none
|
case followed, list, none
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(id: String, title: String, repliesPolicy: RepliesPolicy? = nil, exclusive: Bool? = nil) {
|
public init(id: String, title: String, repliesPolicy: RepliesPolicy? = nil, exclusive: Bool? = nil) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
|
|
|
@ -6,7 +6,7 @@ public struct Marker: Codable, Sendable {
|
||||||
public let version: Int
|
public let version: Int
|
||||||
public let updatedAt: ServerDate
|
public let updatedAt: ServerDate
|
||||||
}
|
}
|
||||||
|
|
||||||
public let notifications: Content?
|
public let notifications: Content?
|
||||||
public let home: Content?
|
public let home: Content?
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable {
|
||||||
description: nil,
|
description: nil,
|
||||||
meta: nil)
|
meta: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func videoWith(url: URL) -> MediaAttachment {
|
public static func videoWith(url: URL) -> MediaAttachment {
|
||||||
.init(id: UUID().uuidString,
|
.init(id: UUID().uuidString,
|
||||||
type: "video",
|
type: "video",
|
||||||
|
|
|
@ -5,7 +5,7 @@ public struct OauthToken: Codable, Hashable, Sendable {
|
||||||
public let tokenType: String
|
public let tokenType: String
|
||||||
public let scope: String
|
public let scope: String
|
||||||
public let createdAt: Double
|
public let createdAt: Double
|
||||||
|
|
||||||
public init(accessToken: String, tokenType: String, scope: String, createdAt: Double) {
|
public init(accessToken: String, tokenType: String, scope: String, createdAt: Double) {
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
self.tokenType = tokenType
|
self.tokenType = tokenType
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum PostError: Error {
|
public enum PostError: Error {
|
||||||
// Throw when any attached media is missing media description (alt text)
|
// Throw when any attached media is missing media description (alt text)
|
||||||
case missingAltText
|
case missingAltText
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PostError: CustomStringConvertible {
|
extension PostError: CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .missingAltText:
|
case .missingAltText:
|
||||||
return NSLocalizedString("status.error.no-alt-text", comment: "media does not have media description")
|
return NSLocalizedString("status.error.no-alt-text", comment: "media does not have media description")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
|
||||||
public let filtered: [Filtered]?
|
public let filtered: [Filtered]?
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
public let language: String?
|
public let language: String?
|
||||||
|
|
||||||
public var isHidden: Bool {
|
public var isHidden: Bool {
|
||||||
filtered?.first?.filter.filterAction == .hide
|
filtered?.first?.filter.filterAction == .hide
|
||||||
}
|
}
|
||||||
|
@ -214,7 +214,7 @@ public final class ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Ha
|
||||||
public let filtered: [Filtered]?
|
public let filtered: [Filtered]?
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
public let language: String?
|
public let language: String?
|
||||||
|
|
||||||
public var isHidden: Bool {
|
public var isHidden: Bool {
|
||||||
filtered?.first?.filter.filterAction == .hide
|
filtered?.first?.filter.filterAction == .hide
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ public struct StreamEventUpdate: StreamEvent {
|
||||||
|
|
||||||
public struct StreamEventStatusUpdate: StreamEvent {
|
public struct StreamEventStatusUpdate: StreamEvent {
|
||||||
public let date = Date()
|
public let date = Date()
|
||||||
public var id: String { status.id + (status.editedAt?.asDate.description ?? "")}
|
public var id: String { status.id + (status.editedAt?.asDate.description ?? "") }
|
||||||
public let status: Status
|
public let status: Status
|
||||||
public init(status: Status) {
|
public init(status: Status) {
|
||||||
self.status = status
|
self.status = status
|
||||||
|
|
|
@ -7,7 +7,7 @@ public struct Tag: Codable, Identifiable, Equatable, Hashable {
|
||||||
|
|
||||||
public static func == (lhs: Tag, rhs: Tag) -> Bool {
|
public static func == (lhs: Tag, rhs: Tag) -> Bool {
|
||||||
lhs.name == rhs.name &&
|
lhs.name == rhs.name &&
|
||||||
lhs.following == rhs.following
|
lhs.following == rhs.following
|
||||||
}
|
}
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
|
|
|
@ -3,8 +3,8 @@ import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Observation
|
import Observation
|
||||||
import os
|
import os
|
||||||
import SwiftUI
|
|
||||||
import OSLog
|
import OSLog
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@Observable public final class Client: Equatable, Identifiable, Hashable, @unchecked Sendable {
|
@Observable public final class Client: Equatable, Identifiable, Hashable, @unchecked Sendable {
|
||||||
public static func == (lhs: Client, rhs: Client) -> Bool {
|
public static func == (lhs: Client, rhs: Client) -> Bool {
|
||||||
|
@ -44,7 +44,7 @@ import OSLog
|
||||||
public let version: Version
|
public let version: Version
|
||||||
private let urlSession: URLSession
|
private let urlSession: URLSession
|
||||||
private let decoder = JSONDecoder()
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.icecubesapp", category: "networking")
|
private let logger = Logger(subsystem: "com.icecubesapp", category: "networking")
|
||||||
|
|
||||||
// Putting all mutable state inside an `OSAllocatedUnfairLock` makes `Client`
|
// Putting all mutable state inside an `OSAllocatedUnfairLock` makes `Client`
|
||||||
|
@ -263,7 +263,7 @@ import OSLog
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func mediaUpload(endpoint: Endpoint,
|
public func mediaUpload(endpoint: Endpoint,
|
||||||
version: Version,
|
version: Version,
|
||||||
method: String,
|
method: String,
|
||||||
|
@ -280,13 +280,14 @@ import OSLog
|
||||||
let (_, httpResponse) = try await urlSession.data(for: request)
|
let (_, httpResponse) = try await urlSession.data(for: request)
|
||||||
return httpResponse as? HTTPURLResponse
|
return httpResponse as? HTTPURLResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeFormDataRequest(endpoint: Endpoint,
|
private func makeFormDataRequest(endpoint: Endpoint,
|
||||||
version: Version,
|
version: Version,
|
||||||
method: String,
|
method: String,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
filename: String,
|
filename: String,
|
||||||
data: Data) throws -> URLRequest {
|
data: Data) throws -> URLRequest
|
||||||
|
{
|
||||||
let url = try makeURL(endpoint: endpoint, forceVersion: version)
|
let url = try makeURL(endpoint: endpoint, forceVersion: version)
|
||||||
var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method)
|
var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method)
|
||||||
let boundary = UUID().uuidString
|
let boundary = UUID().uuidString
|
||||||
|
|
|
@ -98,11 +98,11 @@ public enum Accounts: Endpoint {
|
||||||
if let sinceId {
|
if let sinceId {
|
||||||
params.append(.init(name: "max_id", value: sinceId))
|
params.append(.init(name: "max_id", value: sinceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
params.append(.init(name: "only_media", value: onlyMedia ? "true" : "false"))
|
params.append(.init(name: "only_media", value: onlyMedia ? "true" : "false"))
|
||||||
params.append(.init(name: "exclude_replies", value: excludeReplies ? "true" : "false"))
|
params.append(.init(name: "exclude_replies", value: excludeReplies ? "true" : "false"))
|
||||||
params.append(.init(name: "exclude_reblogs", value: excludeReblogs ? "true" : "false"))
|
params.append(.init(name: "exclude_reblogs", value: excludeReblogs ? "true" : "false"))
|
||||||
|
|
||||||
if let pinned {
|
if let pinned {
|
||||||
params.append(.init(name: "pinned", value: pinned ? "true" : "false"))
|
params.append(.init(name: "pinned", value: pinned ? "true" : "false"))
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue