From c7a56a9f6173ed284fe7849a34c0ecc8d90a256f Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 14 Apr 2024 14:11:43 -0400 Subject: [PATCH] Reorganize appearance prefs, add mock status preview --- .../Sources/Pachyderm/Model/Attachment.swift | 11 + .../Sources/Pachyderm/Model/Card.swift | 32 +++ Tusker.xcodeproj/project.pbxproj | 20 +- .../Screens/Preferences/About/AboutView.swift | 18 +- .../Appearance/AppearancePrefsView.swift | 123 ++++++++ .../Appearance/MockStatusView.swift | 270 ++++++++++++++++++ .../Preferences/AppearancePrefsView.swift | 158 ---------- Tusker/Views/Attachments/AttachmentView.swift | 6 + .../AttachmentsContainerView.swift | 26 +- Tusker/Views/Status/StatusCardView.swift | 18 +- .../Status/StatusMetaIndicatorsView.swift | 23 +- 11 files changed, 511 insertions(+), 194 deletions(-) create mode 100644 Tusker/Screens/Preferences/Appearance/AppearancePrefsView.swift create mode 100644 Tusker/Screens/Preferences/Appearance/MockStatusView.swift delete mode 100644 Tusker/Screens/Preferences/AppearancePrefsView.swift diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Attachment.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Attachment.swift index 4de49d66..a682a3ad 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Attachment.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Attachment.swift @@ -25,6 +25,17 @@ public struct Attachment: Codable, Sendable { ], nil)) } + public init(id: String, kind: Attachment.Kind, url: URL, remoteURL: URL? = nil, previewURL: URL? = nil, meta: Attachment.Metadata? = nil, description: String? = nil, blurHash: String? = nil) { + self.id = id + self.kind = kind + self.url = url + self.remoteURL = remoteURL + self.previewURL = previewURL + self.meta = meta + self.description = description + self.blurHash = blurHash + } + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(String.self, forKey: .id) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift index f7396268..f3dbd14e 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift @@ -26,6 +26,38 @@ public struct Card: Codable, Sendable { /// Only present when returned from the trending links endpoint public let history: [History]? + public init( + url: WebURL, + title: String, + description: String, + image: WebURL? = nil, + kind: Card.Kind, + authorName: String? = nil, + authorURL: WebURL? = nil, + providerName: String? = nil, + providerURL: WebURL? = nil, + html: String? = nil, + width: Int? = nil, + height: Int? = nil, + blurhash: String? = nil, + history: [History]? = nil + ) { + self.url = url + self.title = title + self.description = description + self.image = image + self.kind = kind + self.authorName = authorName + self.authorURL = authorURL + self.providerName = providerName + self.providerURL = providerURL + self.html = html + self.width = width + self.height = height + self.blurhash = blurhash + self.history = history + } + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 4c4472a9..02f2a1d9 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; }; 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; }; 04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; }; - 04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; }; 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; @@ -291,6 +290,8 @@ D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; }; D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */; }; D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */; }; + D6C4532D2BCB86AC00E26A0E /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */; }; + D6C4532F2BCB873400E26A0E /* MockStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C4532E2BCB873400E26A0E /* MockStatusView.swift */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; }; D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; }; D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; }; @@ -435,7 +436,6 @@ 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = ""; }; 0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = ""; }; 04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; - 04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = ""; }; 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = ""; }; @@ -714,6 +714,8 @@ D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AppListStyle.swift"; sourceTree = ""; }; D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineJumpButton.swift; sourceTree = ""; }; D6C4532A2BCAD7F900E26A0E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = ""; }; + D6C4532E2BCB873400E26A0E /* MockStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStatusView.swift; sourceTree = ""; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = ""; }; D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = ""; }; @@ -1170,7 +1172,6 @@ D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */, 04586B4022B2FFB10021BD04 /* PreferencesView.swift */, D64B96802BC3279D002C8990 /* PrefsAccountView.swift */, - 04586B4222B301470021BD04 /* AppearancePrefsView.swift */, D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */, D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */, 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */, @@ -1181,6 +1182,7 @@ 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */, D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */, D68A76ED295369C7001DA1B3 /* AcknowledgementsView.swift */, + D6C4532B2BCB86A100E26A0E /* Appearance */, D64B96822BC3892B002C8990 /* Notifications */, D60089172981FEA4005B4D00 /* Tip Jar */, D68A76EF2953910A001DA1B3 /* About */, @@ -1485,6 +1487,15 @@ path = Views; sourceTree = ""; }; + D6C4532B2BCB86A100E26A0E /* Appearance */ = { + isa = PBXGroup; + children = ( + D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */, + D6C4532E2BCB873400E26A0E /* MockStatusView.swift */, + ); + path = Appearance; + sourceTree = ""; + }; D6C693FA2162FE5D007D6A6D /* Utilities */ = { isa = PBXGroup; children = ( @@ -2102,6 +2113,7 @@ D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */, D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */, D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */, + D6C4532D2BCB86AC00E26A0E /* AppearancePrefsView.swift in Sources */, D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */, D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */, 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */, @@ -2290,7 +2302,6 @@ D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */, D61F759929384D4D00C0B37F /* CustomizeTimelinesView.swift in Sources */, D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */, - 04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */, D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */, D646DCD42A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift in Sources */, D69261272BB3BA610023152C /* Box.swift in Sources */, @@ -2359,6 +2370,7 @@ D65B4B562971F98300DABDFB /* ReportView.swift in Sources */, D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */, D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */, + D6C4532F2BCB873400E26A0E /* MockStatusView.swift in Sources */, D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */, D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */, D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */, diff --git a/Tusker/Screens/Preferences/About/AboutView.swift b/Tusker/Screens/Preferences/About/AboutView.swift index 071fa2c8..8f87b236 100644 --- a/Tusker/Screens/Preferences/About/AboutView.swift +++ b/Tusker/Screens/Preferences/About/AboutView.swift @@ -104,7 +104,9 @@ struct AboutView: View { private var appIcon: some View { VStack { - AppIconView() + Image("AboutIcon") + .resizable() + .clipShape(RoundedRectangle(cornerRadius: 256 / 6.4)) .shadow(radius: 6, y: 3) .frame(width: 256, height: 256) @@ -125,20 +127,6 @@ struct AboutView: View { } } -private struct AppIconView: UIViewRepresentable { - func makeUIView(context: Context) -> UIImageView { - let view = UIImageView(image: UIImage(named: "AboutIcon")) - view.contentMode = .scaleAspectFit - view.layer.cornerRadius = 256 / 6.4 - view.layer.cornerCurve = .continuous - view.layer.masksToBounds = true - return view - } - - func updateUIView(_ uiView: UIImageView, context: Context) { - } -} - private struct MailSheet: UIViewControllerRepresentable { typealias UIViewControllerType = MFMailComposeViewController diff --git a/Tusker/Screens/Preferences/Appearance/AppearancePrefsView.swift b/Tusker/Screens/Preferences/Appearance/AppearancePrefsView.swift new file mode 100644 index 00000000..36813107 --- /dev/null +++ b/Tusker/Screens/Preferences/Appearance/AppearancePrefsView.swift @@ -0,0 +1,123 @@ +// +// AppearancePrefsView.swift +// Tusker +// +// Created by Shadowfacts on 6/13/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import SwiftUI +import TuskerPreferences + +struct AppearancePrefsView: View { + @ObservedObject private var preferences = Preferences.shared + + var body: some View { + List { + Section { + MockStatusView() + .padding(.top, 8) + } + .appGroupedListRowBackground() + + accountsSection + postsSection + mediaSection + } + .listStyle(.insetGrouped) + .appGroupedListBackground(container: PreferencesNavigationController.self) + .navigationTitle("Appearance") + } + + private var accountsSection: some View { + Section("Accounts") { + Toggle(isOn: Binding(get: { + preferences.avatarStyle == .circle + }, set: { + preferences.avatarStyle = $0 ? .circle : .roundRect + })) { + Text("Use Circular Avatars") + } + Toggle(isOn: $preferences.hideCustomEmojiInUsernames) { + Text("Hide Custom Emoji in Usernames") + } + } + .appGroupedListRowBackground() + } + + private var postsSection: some View { + Section("Posts") { + Toggle(isOn: $preferences.showIsStatusReplyIcon) { + Text("Show Status Reply Icons") + } + Toggle(isOn: $preferences.alwaysShowStatusVisibilityIcon) { + Text("Always Show Status Visibility Icons") + } + Toggle(isOn: $preferences.showLinkPreviews) { + Text("Show Link Previews") + } + Toggle(isOn: $preferences.showAttachmentsInTimeline) { + Text("Show Attachments on Timeline") + } + Toggle(isOn: $preferences.hideActionsInTimeline) { + Text("Hide Actions on Timeline") + } + 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") + } + .appGroupedListRowBackground() + } + + private var mediaSection: some View { + Section("Media") { + 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() + } +} + +#if DEBUG +struct AppearancePrefsView_Previews : PreviewProvider { + static var previews: some View { + AppearancePrefsView() + } +} +#endif diff --git a/Tusker/Screens/Preferences/Appearance/MockStatusView.swift b/Tusker/Screens/Preferences/Appearance/MockStatusView.swift new file mode 100644 index 00000000..4d3fe045 --- /dev/null +++ b/Tusker/Screens/Preferences/Appearance/MockStatusView.swift @@ -0,0 +1,270 @@ +// +// MockStatusView.swift +// Tusker +// +// Created by Shadowfacts on 4/13/24. +// Copyright © 2024 Shadowfacts. All rights reserved. +// + +import SwiftUI +import Pachyderm +import WebURL + +struct MockStatusView: View { + @ObservedObject private var preferences = Preferences.shared + @ScaledMetric(relativeTo: .body) private var attachmentsLabelHeight = 17 + + var body: some View { + HStack(alignment: .top, spacing: 8) { + VStack(spacing: 4) { + Image("AboutIcon") + .resizable() + .clipShape(RoundedRectangle(cornerRadius: preferences.avatarStyle.cornerRadiusFraction * 50)) + .frame(width: 50, height: 50) + + MockMetaIndicatorsView() + + Spacer() + } + + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 4) { + MockDisplayNameLabel() + Text(verbatim: "@tusker@example.com") + .foregroundStyle(.secondary) + .font(.body.weight(.light)) + .lineLimit(1) + .truncationMode(.tail) + .layoutPriority(-100) + Spacer() + Text("1h") + .foregroundStyle(.secondary) + .font(.body.weight(.light)) + } + + MockStatusContentView() + + if preferences.showLinkPreviews { + MockStatusCardView() + .frame(height: StatusContentContainer.cardViewHeight) + } + + MockAttachmentsContainerView() + .aspectRatio(preferences.showAttachmentsInTimeline ? 16/9 : nil, contentMode: .fill) + .frame(height: preferences.showAttachmentsInTimeline ? nil : attachmentsLabelHeight) + .padding(.bottom, preferences.showAttachmentsInTimeline && preferences.hideActionsInTimeline ? 8 : 0) + + if !preferences.hideActionsInTimeline { + MockStatusActionButtons() + } + } + .layoutPriority(100) + } + } +} + +private struct MockMetaIndicatorsView: UIViewRepresentable { + @ObservedObject private var preferences = Preferences.shared + + func makeUIView(context: Context) -> StatusMetaIndicatorsView { + let view = StatusMetaIndicatorsView() + view.primaryAxis = .vertical + view.secondaryAxisAlignment = .trailing + return view + } + + func updateUIView(_ uiView: StatusMetaIndicatorsView, context: Context) { + var indicators: StatusMetaIndicatorsView.Indicator = [] + if preferences.showIsStatusReplyIcon { + indicators.insert(.reply) + } + if preferences.alwaysShowStatusVisibilityIcon { + indicators.insert(.visibility) + } + uiView.setIndicators(indicators, visibility: .public) + } +} + +private struct MockDisplayNameLabel: View { + @ObservedObject private var preferences = Preferences.shared + @ScaledMetric(relativeTo: .body) private var emojiSize = 17 + @State var textWithImage = Text("Tusker") + + var body: some View { + displayName + .font(.body.weight(.semibold)) + // don't let the height change depending on whether emojis are present or not + .frame(height: emojiSize) + .task(id: emojiSize) { + let size = CGSize(width: emojiSize, height: emojiSize) + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { ctx in + let bounds = CGRect(origin: .zero, size: size) + UIBezierPath(roundedRect: bounds, cornerRadius: 2).addClip() + UIImage(named: "AboutIcon")!.draw(in: bounds) + } + textWithImage = Text("Tusker \(Image(uiImage: image))") + } + } + + private var displayName: Text { + if preferences.hideCustomEmojiInUsernames { + Text("Tusker") + } else { + textWithImage + } + } +} + +private struct MockStatusContentView: View { + @ObservedObject private var preferences = Preferences.shared + + var body: some View { + Text("This is an example post so you can check out how things look.\n\nThanks for using \(link)!") + .lineLimit(nil) + } + + private var link: Text { + Text("Tusker") + .foregroundColor(.accentColor) + .underline(preferences.underlineTextLinks) + } +} + +private struct MockStatusCardView: UIViewRepresentable { + func makeUIView(context: Context) -> StatusCardView { + let view = StatusCardView() + view.isUserInteractionEnabled = false + let card = Card( + url: WebURL("https://vaccor.space/tusker")!, + title: "Tusker", + description: "Tusker is an iOS app for Mastodon", + image: WebURL("https://vaccor.space/tusker/img/icon.png")!, + kind: .link + ) + view.updateUI(card: card, sensitive: false) + return view + } + + func updateUIView(_ uiView: StatusCardView, context: Context) { + } +} + +private actor MockAttachmentsGenerator { + static let shared = MockAttachmentsGenerator() + + private var attachmentURLs: [URL]? + + func getAttachmentURLs(displayScale: CGFloat) -> [URL] { + if let attachmentURLs, + attachmentURLs.allSatisfy({ FileManager.default.fileExists(atPath: $0.path) }) { + return attachmentURLs + } + + let size = CGSize(width: 100, height: 100) + let bounds = CGRect(origin: .zero, size: size) + let format = UIGraphicsImageRendererFormat() + format.scale = displayScale + let renderer = UIGraphicsImageRenderer(size: size, format: format) + + let firstImage = renderer.image { ctx in + 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) { + 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)) + } + } + 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 { + 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 { + let rect = CGRect(x: CGFloat(x) * 45 + 22.5, y: CGFloat(y) * 50 - 5, width: 10, height: 10) + ctx.cgContext.fillEllipse(in: rect) + } + } + } + + let tempDirectory = FileManager.default.temporaryDirectory + let firstURL = tempDirectory.appendingPathComponent("\(UUID().description)", conformingTo: .png) + let secondURL = tempDirectory.appendingPathComponent("\(UUID().description)", conformingTo: .png) + + do { + try firstImage.pngData()!.write(to: firstURL) + try secondImage.pngData()!.write(to: secondURL) + attachmentURLs = [firstURL, secondURL] + return [firstURL, secondURL] + } catch { + return [] + } + } +} + +private struct MockAttachmentsContainerView: View { + @State private var attachments: [Attachment] = [] + @Environment(\.displayScale) private var displayScale + + var body: some View { + MockAttachmentsContainerRepresentable(attachments: attachments) + .task { + let attachmentURLs = await MockAttachmentsGenerator.shared.getAttachmentURLs(displayScale: displayScale) + self.attachments = [ + .init(id: "1", kind: .image, url: attachmentURLs[0], description: "test"), + .init(id: "2", kind: .image, url: attachmentURLs[1], description: nil), + ] + } + } +} + +private struct MockAttachmentsContainerRepresentable: UIViewRepresentable { + let attachments: [Attachment] + @ObservedObject private var preferences = Preferences.shared + + func makeUIView(context: Context) -> AttachmentsContainerView { + let view = AttachmentsContainerView() + view.isUserInteractionEnabled = false + return view + } + + func updateUIView(_ uiView: AttachmentsContainerView, context: Context) { + uiView.updateUI(attachments: attachments, labelOnly: !preferences.showAttachmentsInTimeline) + uiView.contentHidden = preferences.attachmentBlurMode == .always + for attachmentView in uiView.attachmentViews.allObjects { + attachmentView.updateBadges() + } + } +} + +private struct MockStatusActionButtons: View { + var body: some View { + HStack(spacing: 0) { + Image(systemName: "arrowshape.turn.up.left.fill") + .foregroundStyle(.tint) + Spacer() + Image(systemName: "star.fill") + .foregroundStyle(.tint) + Spacer() + Image(systemName: "repeat") + .foregroundStyle(.yellow) + Spacer() + Image(systemName: "ellipsis") + .foregroundStyle(.tint) + Spacer() + } + } +} + +#Preview { + MockStatusView() + .frame(height: 300) +} diff --git a/Tusker/Screens/Preferences/AppearancePrefsView.swift b/Tusker/Screens/Preferences/AppearancePrefsView.swift deleted file mode 100644 index 9ee53759..00000000 --- a/Tusker/Screens/Preferences/AppearancePrefsView.swift +++ /dev/null @@ -1,158 +0,0 @@ -// AppearancePrefsView.swift -// Tusker -// -// Created by Shadowfacts on 6/13/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import SwiftUI -import Combine -import TuskerPreferences - -struct AppearancePrefsView : View { - @ObservedObject var preferences = Preferences.shared - - 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 var useCircularAvatars: Binding = Binding(get: { - Preferences.shared.avatarStyle == .circle - }) { - Preferences.shared.avatarStyle = $0 ? .circle : .roundRect - } - - private 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 { - themeSection - interfaceSection - accountsSection - postsSection - } - .listStyle(.insetGrouped) - .appGroupedListBackground(container: PreferencesNavigationController.self) - .navigationBarTitle(Text("Appearance")) - } - - private var themeSection: some View { - Section { - #if !os(visionOS) - Picker(selection: $preferences.theme, label: Text("Theme")) { - Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified) - Text("Light").tag(UIUserInterfaceStyle.light) - Text("Dark").tag(UIUserInterfaceStyle.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(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(header: Text("Accounts")) { - Toggle(isOn: useCircularAvatars) { - Text("Use Circular Avatars") - } - Toggle(isOn: $preferences.hideCustomEmojiInUsernames) { - Text("Hide Custom Emoji in Usernames") - } - } - .appGroupedListRowBackground() - } - - private var postsSection: some View { - Section(header: Text("Posts")) { - Toggle(isOn: $preferences.showIsStatusReplyIcon) { - Text("Show Status Reply Icons") - } - Toggle(isOn: $preferences.alwaysShowStatusVisibilityIcon) { - Text("Always Show Status Visibility Icons") - } - Toggle(isOn: $preferences.showLinkPreviews) { - Text("Show Link Previews") - } - Toggle(isOn: $preferences.showAttachmentsInTimeline) { - Text("Show Attachments on Timeline") - } - Toggle(isOn: $preferences.hideActionsInTimeline) { - Text("Hide Actions on Timeline") - } - 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") - } - } - .appGroupedListRowBackground() - } -} - -#if DEBUG -struct AppearancePrefsView_Previews : PreviewProvider { - static var previews: some View { - AppearancePrefsView() - } -} -#endif diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index 99b7ff3d..42edf2ad 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -111,6 +111,10 @@ class AttachmentView: GIFImageView { } } + func updateBadges() { + createBadgesView(getBadges()) + } + @objc private func gifPlaybackModeChanged() { // NSProcessInfoPowerStateDidChange is sometimes fired on a background thread DispatchQueue.main.async { @@ -370,6 +374,8 @@ class AttachmentView: GIFImageView { return } + self.badgeContainer?.removeFromSuperview() + let stack = UIStackView() self.badgeContainer = stack stack.axis = .horizontal diff --git a/Tusker/Views/Attachments/AttachmentsContainerView.swift b/Tusker/Views/Attachments/AttachmentsContainerView.swift index a72596cd..eb60dce2 100644 --- a/Tusker/Views/Attachments/AttachmentsContainerView.swift +++ b/Tusker/Views/Attachments/AttachmentsContainerView.swift @@ -72,20 +72,34 @@ class AttachmentsContainerView: UIView { func updateUI(attachments: [Attachment], labelOnly: Bool = false) { let newTokens = attachments.map { AttachmentToken(attachment: $0) } - guard !labelOnly else { + guard labelOnly != (label != nil) || self.attachmentTokens != newTokens else { self.attachments = attachments self.attachmentTokens = newTokens - updateLabel(attachments: attachments) - return - } - - guard self.attachmentTokens != newTokens else { self.isHidden = attachments.isEmpty + if labelOnly && !attachments.isEmpty { + updateLabel(attachments: attachments) + } else { + label?.removeFromSuperview() + label = nil + } return } self.attachments = attachments self.attachmentTokens = newTokens + + if labelOnly { + if !attachments.isEmpty { + updateLabel(attachments: attachments) + } else { + label?.removeFromSuperview() + label = nil + } + return + } else { + label?.removeFromSuperview() + label = nil + } removeAttachmentViews() hideButtonView?.isHidden = false diff --git a/Tusker/Views/Status/StatusCardView.swift b/Tusker/Views/Status/StatusCardView.swift index 51613381..b8f7d6bb 100644 --- a/Tusker/Views/Status/StatusCardView.swift +++ b/Tusker/Views/Status/StatusCardView.swift @@ -20,8 +20,8 @@ class StatusCardView: UIView { private var statusID: String? private(set) var card: Card? - private let activeBackgroundColor = UIColor.secondarySystemFill - private let inactiveBackgroundColor = UIColor.secondarySystemBackground + private static let activeBackgroundColor = UIColor.secondarySystemFill + private static let inactiveBackgroundColor = UIColor.secondarySystemBackground private var isGrayscale = false @@ -107,7 +107,7 @@ class StatusCardView: UIView { hStack.clipsToBounds = true hStack.layer.borderWidth = 0.5 hStack.layer.cornerCurve = .continuous - hStack.backgroundColor = inactiveBackgroundColor + hStack.backgroundColor = StatusCardView.inactiveBackgroundColor updateBorderColor() addSubview(hStack) @@ -173,8 +173,12 @@ class StatusCardView: UIView { return } + updateUI(card: card, sensitive: status.sensitive) + } + + func updateUI(card: Card, sensitive: Bool) { if let image = card.image { - if status.sensitive { + if sensitive { if let blurhash = card.blurhash { imageView.blurImage = false imageView.showOnlyBlurHash(blurhash, for: URL(image)!) @@ -219,7 +223,7 @@ class StatusCardView: UIView { } override func touchesBegan(_ touches: Set, with event: UIEvent?) { - hStack.backgroundColor = activeBackgroundColor + hStack.backgroundColor = StatusCardView.activeBackgroundColor setNeedsDisplay() } @@ -227,7 +231,7 @@ class StatusCardView: UIView { } override func touchesEnded(_ touches: Set, with event: UIEvent?) { - hStack.backgroundColor = inactiveBackgroundColor + hStack.backgroundColor = StatusCardView.inactiveBackgroundColor setNeedsDisplay() if let card = card, let delegate = navigationDelegate { @@ -236,7 +240,7 @@ class StatusCardView: UIView { } override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - hStack.backgroundColor = inactiveBackgroundColor + hStack.backgroundColor = StatusCardView.inactiveBackgroundColor setNeedsDisplay() } diff --git a/Tusker/Views/Status/StatusMetaIndicatorsView.swift b/Tusker/Views/Status/StatusMetaIndicatorsView.swift index a97c5699..830c32fb 100644 --- a/Tusker/Views/Status/StatusMetaIndicatorsView.swift +++ b/Tusker/Views/Status/StatusMetaIndicatorsView.swift @@ -80,20 +80,35 @@ class StatusMetaIndicatorsView: UIView { } statusID = status.id - var images: [UIImage] = [] + var indicators: Indicator = [] if allowedIndicators.contains(.reply) && Preferences.shared.showIsStatusReplyIcon && status.inReplyToID != nil { - images.append(UIImage(systemName: "bubble.left.and.bubble.right")!) + indicators.insert(.reply) } if allowedIndicators.contains(.visibility) && Preferences.shared.alwaysShowStatusVisibilityIcon { - images.append(UIImage(systemName: status.visibility.unfilledImageName)!) + indicators.insert(.visibility) } if allowedIndicators.contains(.localOnly) && status.localOnly { - images.append(UIImage(named: "link.broken")!) + indicators.insert(.localOnly) } + setIndicators(indicators, visibility: status.visibility) + } + + // Used by MockStatusView + func setIndicators(_ indicators: Indicator, visibility: Visibility) { + var images: [UIImage] = [] + if indicators.contains(.reply) { + images.append(UIImage(systemName: "bubble.left.and.bubble.right")!) + } + if indicators.contains(.visibility) { + images.append(UIImage(systemName: visibility.unfilledImageName)!) + } + if indicators.contains(.localOnly) { + images.append(UIImage(named: "link.broken")!) + } let views = images.map { let v = UIImageView(image: $0) v.translatesAutoresizingMaskIntoConstraints = false