Reorganize appearance prefs, add mock status preview

This commit is contained in:
Shadowfacts 2024-04-14 14:11:43 -04:00
parent 39251b9aa2
commit c7a56a9f61
11 changed files with 511 additions and 194 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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 = "<group>"; };
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
@ -714,6 +714,8 @@
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AppListStyle.swift"; sourceTree = "<group>"; };
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineJumpButton.swift; sourceTree = "<group>"; };
D6C4532A2BCAD7F900E26A0E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
D6C4532E2BCB873400E26A0E /* MockStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStatusView.swift; sourceTree = "<group>"; };
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
@ -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 = "<group>";
};
D6C4532B2BCB86A100E26A0E /* Appearance */ = {
isa = PBXGroup;
children = (
D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */,
D6C4532E2BCB873400E26A0E /* MockStatusView.swift */,
);
path = Appearance;
sourceTree = "<group>";
};
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 */,

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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<Void, Never> {
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<Bool> = 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

View File

@ -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

View File

@ -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

View File

@ -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<UITouch>, with event: UIEvent?) {
hStack.backgroundColor = activeBackgroundColor
hStack.backgroundColor = StatusCardView.activeBackgroundColor
setNeedsDisplay()
}
@ -227,7 +231,7 @@ class StatusCardView: UIView {
}
override func touchesEnded(_ touches: Set<UITouch>, 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<UITouch>, with event: UIEvent?) {
hStack.backgroundColor = inactiveBackgroundColor
hStack.backgroundColor = StatusCardView.inactiveBackgroundColor
setNeedsDisplay()
}

View File

@ -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