Reorganize appearance prefs, add mock status preview
This commit is contained in:
parent
39251b9aa2
commit
c7a56a9f61
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -72,21 +72,35 @@ 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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue