diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 02f2a1d9..62000268 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -169,7 +169,6 @@ D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; }; D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; }; D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; }; - D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; }; D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; }; D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; }; D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; }; @@ -592,7 +591,6 @@ D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = ""; }; D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = ""; }; D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = ""; }; - D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = ""; }; D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = ""; }; D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = ""; }; D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = ""; }; @@ -1172,12 +1170,9 @@ D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */, 04586B4022B2FFB10021BD04 /* PreferencesView.swift */, D64B96802BC3279D002C8990 /* PrefsAccountView.swift */, - D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */, - D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */, 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */, D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */, D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */, - D68015412401A74600D6103B /* MediaPrefsView.swift */, D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */, 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */, D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */, @@ -1492,6 +1487,8 @@ children = ( D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */, D6C4532E2BCB873400E26A0E /* MockStatusView.swift */, + D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */, + D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */, ); path = Appearance; sourceTree = ""; @@ -2348,7 +2345,6 @@ D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */, D6CF5B892AC9BA6E00F15D83 /* MastodonSearchController.swift in Sources */, D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */, - D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */, D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */, D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */, D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */, diff --git a/Tusker/Screens/Preferences/Appearance/AppearancePrefsView.swift b/Tusker/Screens/Preferences/Appearance/AppearancePrefsView.swift index 36813107..3643de28 100644 --- a/Tusker/Screens/Preferences/Appearance/AppearancePrefsView.swift +++ b/Tusker/Screens/Preferences/Appearance/AppearancePrefsView.swift @@ -7,18 +7,49 @@ // import SwiftUI +import Combine import TuskerPreferences struct AppearancePrefsView: View { @ObservedObject private var preferences = Preferences.shared + @Environment(\.colorScheme) private var colorScheme + + private var appearanceChangePublisher: some Publisher { + preferences.$theme + .map { _ in () } + .merge(with: preferences.$pureBlackDarkMode.map { _ in () }, + preferences.$accentColor.map { _ in () } + ) + // the prefrence publishers are all willSet, but want to notify after the change, so wait one runloop iteration + .receive(on: DispatchQueue.main) + } + + private static let accentColorsAndImages: [(AccentColor, UIImage?)] = AccentColor.allCases.map { color in + var image: UIImage? + if let color = color.color { + if #available(iOS 16.0, *) { + image = UIImage(systemName: "circle.fill")!.withTintColor(color, renderingMode: .alwaysTemplate).withRenderingMode(.alwaysOriginal) + } else { + image = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 20)).image { context in + color.setFill() + context.cgContext.fillEllipse(in: CGRect(x: 0, y: 0, width: 20, height: 20)) + } + } + } + return (color, image) + } var body: some View { List { - Section { + themeSection + interfaceSection + + Section("Post Preview") { MockStatusView() .padding(.top, 8) + .padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 8 : 4) } - .appGroupedListRowBackground() + .listRowBackground(preferences.pureBlackDarkMode ? colorScheme == .dark ? Color.black : Color.white : Color.appBackground) accountsSection postsSection @@ -29,6 +60,53 @@ struct AppearancePrefsView: View { .navigationTitle("Appearance") } + private var themeSection: some View { + Section { + #if !os(visionOS) + Picker(selection: $preferences.theme, label: Text("Theme")) { + Text("Use System Theme").tag(Theme.unspecified) + Text("Light").tag(Theme.light) + Text("Dark").tag(Theme.dark) + } + + // macOS system dark mode isn't pure black, so this isn't necessary + if !ProcessInfo.processInfo.isMacCatalystApp && !ProcessInfo.processInfo.isiOSAppOnMac { + Toggle(isOn: $preferences.pureBlackDarkMode) { + Text("Pure Black Dark Mode") + } + } + #endif + + Picker(selection: $preferences.accentColor, label: Text("Accent Color")) { + ForEach(Self.accentColorsAndImages, id: \.0.rawValue) { (color, image) in + HStack { + Text(color.name) + if let image { + Spacer() + Image(uiImage: image) + } + } + .tag(color) + } + } + } + .onReceive(appearanceChangePublisher) { _ in + NotificationCenter.default.post(name: .themePreferenceChanged, object: nil) + } + .appGroupedListRowBackground() + } + + @ViewBuilder + private var interfaceSection: some View { + let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) + if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) { + Section(header: Text("Interface")) { + WidescreenNavigationPrefsView() + } + .appGroupedListRowBackground() + } + } + private var accountsSection: some View { Section("Accounts") { Toggle(isOn: Binding(get: { @@ -65,15 +143,16 @@ struct AppearancePrefsView: View { Toggle(isOn: $preferences.underlineTextLinks) { Text("Underline Links") } -// NavigationLink("Leading Swipe Actions") { -// SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions) -// .edgesIgnoringSafeArea(.all) -// .navigationTitle("Leading Swipe Actions") -// } -// NavigationLink("Trailing Swipe Actions") { -// SwipeActionsPrefsView(selection: $preferences.trailingStatusSwipeActions) -// .edgesIgnoringSafeArea(.all) -// .navigationTitle("Trailing Swipe Actions") + NavigationLink("Leading Swipe Actions") { + SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions) + .edgesIgnoringSafeArea(.all) + .navigationTitle("Leading Swipe Actions") + } + NavigationLink("Trailing Swipe Actions") { + SwipeActionsPrefsView(selection: $preferences.trailingStatusSwipeActions) + .edgesIgnoringSafeArea(.all) + .navigationTitle("Trailing Swipe Actions") + } } .appGroupedListRowBackground() } diff --git a/Tusker/Screens/Preferences/Appearance/MockStatusView.swift b/Tusker/Screens/Preferences/Appearance/MockStatusView.swift index 4d3fe045..5c313ae1 100644 --- a/Tusker/Screens/Preferences/Appearance/MockStatusView.swift +++ b/Tusker/Screens/Preferences/Appearance/MockStatusView.swift @@ -161,7 +161,7 @@ private actor MockAttachmentsGenerator { return attachmentURLs } - let size = CGSize(width: 100, height: 100) + let size = CGSize(width: 200, height: 200) let bounds = CGRect(origin: .zero, size: size) let format = UIGraphicsImageRendererFormat() format.scale = displayScale @@ -171,24 +171,24 @@ private actor MockAttachmentsGenerator { UIColor(red: 0x56 / 255, green: 0x03 / 255, blue: 0xad / 255, alpha: 1).setFill() ctx.fill(bounds) ctx.cgContext.concatenate(CGAffineTransform(1, 0, -0.5, 1, 0, 0)) - for minX in stride(from: 0, through: 100, by: 30) { + for x in 0..<9 { UIColor(red: 0x83 / 255, green: 0x67 / 255, blue: 0xc7 / 255, alpha: 1).setFill() - ctx.fill(CGRect(x: minX + 20, y: 0, width: 15, height: 100)) + ctx.fill(CGRect(x: CGFloat(x) * 30 + 20, y: 0, width: 15, height: bounds.height)) } } let secondImage = renderer.image { ctx in UIColor(red: 0x00 / 255, green: 0x43 / 255, blue: 0x85 / 255, alpha: 1).setFill() ctx.fill(bounds) UIColor(red: 0x05 / 255, green: 0xb2 / 255, blue: 0xdc / 255, alpha: 1).setFill() - for y in 0..<2 { - for x in 0..<4 { + for y in 0..<4 { + for x in 0..<5 { let rect = CGRect(x: x * 45 - 5, y: y * 50 + 15, width: 20, height: 20) ctx.cgContext.fillEllipse(in: rect) } } UIColor(red: 0x08 / 255, green: 0x7c / 255, blue: 0xa7 / 255, alpha: 1).setFill() - for y in 0..<3 { - for x in 0..<2 { + for y in 0..<5 { + for x in 0..<4 { let rect = CGRect(x: CGFloat(x) * 45 + 22.5, y: CGFloat(y) * 50 - 5, width: 10, height: 10) ctx.cgContext.fillEllipse(in: rect) } diff --git a/Tusker/Screens/Preferences/SwipeActionsPrefsView.swift b/Tusker/Screens/Preferences/Appearance/SwipeActionsPrefsView.swift similarity index 100% rename from Tusker/Screens/Preferences/SwipeActionsPrefsView.swift rename to Tusker/Screens/Preferences/Appearance/SwipeActionsPrefsView.swift diff --git a/Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift b/Tusker/Screens/Preferences/Appearance/WidescreenNavigationPrefsView.swift similarity index 91% rename from Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift rename to Tusker/Screens/Preferences/Appearance/WidescreenNavigationPrefsView.swift index 6f765838..917910f0 100644 --- a/Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift +++ b/Tusker/Screens/Preferences/Appearance/WidescreenNavigationPrefsView.swift @@ -12,26 +12,32 @@ import TuskerPreferences struct WidescreenNavigationPrefsView: View { @ObservedObject private var preferences = Preferences.shared - @State private var startAnimation = PassthroughSubject() + @State private var startAnimation = CurrentValueSubject(false) + + private var startAnimationSignal: some Publisher { + startAnimation.filter { $0 }.removeDuplicates().map { _ in () } + } var body: some View { HStack { Spacer() - OptionView( + OptionView( + content: StackNavigationPreview.self, value: .stack, selection: $preferences.widescreenNavigationMode, - startAnimation: startAnimation + startAnimation: startAnimationSignal ) { Text("Stack") } Spacer(minLength: 32) - OptionView( + OptionView( + content: SplitNavigationPreview.self, value: .splitScreen, selection: $preferences.widescreenNavigationMode, - startAnimation: startAnimation + startAnimation: startAnimationSignal ) { Text("Split Screen") } @@ -39,10 +45,11 @@ struct WidescreenNavigationPrefsView: View { if preferences.hasFeatureFlag(.iPadMultiColumn) { Spacer(minLength: 32) - OptionView( + OptionView( + content: MultiColumnNavigationPreview.self, value: .multiColumn, selection: $preferences.widescreenNavigationMode, - startAnimation: startAnimation + startAnimation: startAnimationSignal ) { Text("Multi-Column") } @@ -53,19 +60,26 @@ struct WidescreenNavigationPrefsView: View { .frame(height: 100) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { - startAnimation.send() + startAnimation.send(true) } } } } -private struct OptionView: View { +private struct OptionView>: View { let value: WidescreenNavigationMode @Binding var selection: WidescreenNavigationMode - let startAnimation: PassthroughSubject - @ViewBuilder let label: Text + let startAnimation: P + let label: Text @Environment(\.colorScheme) private var colorScheme + init(content _: Content.Type, value: WidescreenNavigationMode, selection: Binding, startAnimation: P, @ViewBuilder label: () -> Text) { + self.value = value + self._selection = selection + self.startAnimation = startAnimation + self.label = label() + } + private var selected: Bool { selection == value } @@ -84,7 +98,7 @@ private struct OptionView: View { } private var preview: some View { - NavigationModeRepresentable(startAnimation: startAnimation) + NavigationModeRepresentable(content: Content.self, startAnimation: startAnimation) .clipShape(RoundedRectangle(cornerRadius: 12.5, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 12.5, style: .continuous) @@ -106,11 +120,15 @@ private struct WideCapsule: Shape { @MainActor private protocol NavigationModePreview: UIView { - init(startAnimation: PassthroughSubject) + init(startAnimation: some Publisher) } -private struct NavigationModeRepresentable: UIViewRepresentable { - let startAnimation: PassthroughSubject +private struct NavigationModeRepresentable>: UIViewRepresentable { + let startAnimation: P + + init(content _: UIViewType.Type, startAnimation: P) { + self.startAnimation = startAnimation + } func makeUIView(context: Context) -> UIViewType { UIViewType(startAnimation: startAnimation) @@ -128,7 +146,7 @@ private final class StackNavigationPreview: UIView, NavigationModePreview { private let destinationView = UIView() private var cancellable: AnyCancellable? - init(startAnimation: PassthroughSubject) { + init(startAnimation: some Publisher) { super.init(frame: .zero) backgroundColor = .appBackground @@ -203,7 +221,7 @@ private final class SplitNavigationPreview: UIView, NavigationModePreview { private var cellStackTrailingConstraint: NSLayoutConstraint! private var cancellable: AnyCancellable? - init(startAnimation: PassthroughSubject) { + init(startAnimation: some Publisher) { super.init(frame: .zero) backgroundColor = .appBackground @@ -297,7 +315,7 @@ private final class MultiColumnNavigationPreview: UIView, NavigationModePreview private var startedAnimation = false - init(startAnimation: PassthroughSubject) { + init(startAnimation: some Publisher) { super.init(frame: .zero) backgroundColor = .appSecondaryBackground diff --git a/Tusker/Screens/Preferences/MediaPrefsView.swift b/Tusker/Screens/Preferences/MediaPrefsView.swift deleted file mode 100644 index 270acd98..00000000 --- a/Tusker/Screens/Preferences/MediaPrefsView.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// MediaPrefsView.swift -// Tusker -// -// Created by Shadowfacts on 2/22/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import SwiftUI -import TuskerPreferences - -struct MediaPrefsView: View { - @ObservedObject var preferences = Preferences.shared - - var body: some View { - List { - viewingSection - } - .listStyle(.insetGrouped) - .appGroupedListBackground(container: PreferencesNavigationController.self) - .navigationBarTitle("Media") - } - - var viewingSection: some View { - Section(header: Text("Viewing")) { - Picker(selection: $preferences.attachmentBlurMode) { - ForEach(AttachmentBlurMode.allCases, id: \.self) { mode in - Text(mode.displayName).tag(mode) - } - } label: { - Text("Blur Media") - } - - Toggle(isOn: $preferences.blurMediaBehindContentWarning) { - Text("Blur Media Behind Content Warning") - } - .disabled(preferences.attachmentBlurMode != .useStatusSetting) - - Toggle(isOn: $preferences.automaticallyPlayGifs) { - Text("Automatically Play GIFs") - } - - Toggle(isOn: $preferences.showUncroppedMediaInline) { - Text("Show Uncropped Media Inline") - } - - Toggle(isOn: $preferences.showAttachmentBadges) { - Text("Show GIF/\(Text("Alt").font(.body.lowercaseSmallCaps())) Badges") - } - - Toggle(isOn: $preferences.attachmentAltBadgeInverted) { - Text("Show Badge when Missing \(Text("Alt").font(.body.lowercaseSmallCaps()))") - } - .disabled(!preferences.showAttachmentBadges) - } - .appGroupedListRowBackground() - } -} - -struct MediaPrefsView_Previews: PreviewProvider { - static var previews: some View { - MediaPrefsView() - } -} diff --git a/Tusker/Screens/Preferences/PreferencesView.swift b/Tusker/Screens/Preferences/PreferencesView.swift index 4e5ad84b..eb44df48 100644 --- a/Tusker/Screens/Preferences/PreferencesView.swift +++ b/Tusker/Screens/Preferences/PreferencesView.swift @@ -23,7 +23,6 @@ struct PreferencesView: View { var body: some View { List { accountsSection - notificationsSection preferencesSection aboutSection } @@ -92,31 +91,22 @@ struct PreferencesView: View { .appGroupedListRowBackground() } - private var notificationsSection: some View { - Section { - NavigationLink(isActive: $navigationState.showNotificationPreferences) { - NotificationsPrefsView() - } label: { - Text("Notifications") - } - } - .appGroupedListRowBackground() - } - private var preferencesSection: some View { Section { NavigationLink(destination: AppearancePrefsView()) { Text("Appearance") } - NavigationLink(destination: ComposingPrefsView()) { - Text("Composing") - } - NavigationLink(destination: MediaPrefsView()) { - Text("Media") - } NavigationLink(destination: BehaviorPrefsView()) { Text("Behavior") } + NavigationLink(isActive: $navigationState.showNotificationPreferences) { + NotificationsPrefsView() + } label: { + Text("Notifications") + } + NavigationLink(destination: ComposingPrefsView()) { + Text("Composing") + } NavigationLink(destination: WellnessPrefsView()) { Text("Digital Wellness") }