Compare commits
8 Commits
5f6699749c
...
381f3ee737
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 381f3ee737 | |
Shadowfacts | 5be80d8e68 | |
Shadowfacts | 12bab71b17 | |
Shadowfacts | 02fd724b0b | |
Shadowfacts | 7d47f1f259 | |
Shadowfacts | cad074bcc3 | |
Shadowfacts | 8243e06e95 | |
Shadowfacts | f4b51c06c1 |
|
@ -371,14 +371,13 @@ private struct HTMLCallbacks: HTMLConversionCallbacks {
|
||||||
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
||||||
// serializing the WebURL as a string and then having Foundation parse it again),
|
// serializing the WebURL as a string and then having Foundation parse it again),
|
||||||
// so, if available, use the system parser which doesn't require another round trip.
|
// so, if available, use the system parser which doesn't require another round trip.
|
||||||
if #available(iOS 16.0, macOS 13.0, *),
|
if let url = try? URL.ParseStrategy().parse(string) {
|
||||||
let url = try? URL.ParseStrategy().parse(string) {
|
|
||||||
url
|
url
|
||||||
} else if let web = WebURL(string),
|
} else if let web = WebURL(string),
|
||||||
let url = URL(web) {
|
let url = URL(web) {
|
||||||
url
|
url
|
||||||
} else {
|
} else {
|
||||||
URL(string: string)
|
nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.7
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "ComposeUI",
|
name: "ComposeUI",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
@ -27,9 +27,15 @@ let package = Package(
|
||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "ComposeUI",
|
name: "ComposeUI",
|
||||||
dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation", "TuskerPreferences"]),
|
dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation", "TuskerPreferences"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "ComposeUITests",
|
name: "ComposeUITests",
|
||||||
dependencies: ["ComposeUI"]),
|
dependencies: ["ComposeUI"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -30,20 +30,14 @@ enum ToolbarElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct FocusedComposeInput: FocusedValueKey {
|
private struct FocusedComposeInput: FocusedValueKey {
|
||||||
typealias Value = (any ComposeInput)?
|
typealias Value = any ComposeInput
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FocusedValues {
|
extension FocusedValues {
|
||||||
// This double optional is unfortunate, but avoiding it requires iOS 16 API
|
var composeInput: (any ComposeInput)? {
|
||||||
fileprivate var _composeInput: (any ComposeInput)?? {
|
|
||||||
get { self[FocusedComposeInput.self] }
|
get { self[FocusedComposeInput.self] }
|
||||||
set { self[FocusedComposeInput.self] = newValue }
|
set { self[FocusedComposeInput.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var composeInput: (any ComposeInput)? {
|
|
||||||
get { _composeInput ?? nil }
|
|
||||||
set { _composeInput = newValue }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
|
@ -72,6 +66,6 @@ struct FocusedInputModifier: ViewModifier {
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.environment(\.composeInputBox, box)
|
.environment(\.composeInputBox, box)
|
||||||
.focusedValue(\._composeInput, box.wrappedValue)
|
.focusedValue(\.composeInput, box.wrappedValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ class AttachmentRowController: ViewController {
|
||||||
Button(role: .destructive, action: controller.removeAttachment) {
|
Button(role: .destructive, action: controller.removeAttachment) {
|
||||||
Label("Delete", systemImage: "trash")
|
Label("Delete", systemImage: "trash")
|
||||||
}
|
}
|
||||||
} previewIfAvailable: {
|
} preview: {
|
||||||
ControllerView(controller: { controller.thumbnailController })
|
ControllerView(controller: { controller.thumbnailController })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,16 +221,3 @@ extension AttachmentRowController {
|
||||||
case allowEntry, recognizingText
|
case allowEntry, recognizingText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@available(visionOS 1.0, *)
|
|
||||||
@ViewBuilder
|
|
||||||
func contextMenu<M: View, P: View>(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.contextMenu(menuItems: menuItems, preview: preview)
|
|
||||||
} else {
|
|
||||||
self.contextMenu(menuItems: menuItems)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -181,7 +181,7 @@ extension EnvironmentValues {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct GIFViewWrapper: UIViewRepresentable {
|
struct GIFViewWrapper: UIViewRepresentable {
|
||||||
typealias UIViewType = GIFImageView
|
typealias UIViewType = GIFImageView
|
||||||
|
|
||||||
@State var controller: GIFController
|
@State var controller: GIFController
|
||||||
|
|
|
@ -214,44 +214,6 @@ fileprivate extension View {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func sheetOrPopover(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> some View) -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.modifier(SheetOrPopover(isPresented: isPresented, view: content))
|
|
||||||
} else {
|
|
||||||
self.popover(isPresented: isPresented, content: content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func withSheetDetentsIfAvailable() -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self
|
|
||||||
.presentationDetents([.medium, .large])
|
|
||||||
.presentationDragIndicator(.visible)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
fileprivate struct SheetOrPopover<V: View>: ViewModifier {
|
|
||||||
@Binding var isPresented: Bool
|
|
||||||
@ViewBuilder let view: () -> V
|
|
||||||
|
|
||||||
@Environment(\.horizontalSizeClass) var sizeClass
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
if sizeClass == .compact {
|
|
||||||
content.sheet(isPresented: $isPresented, content: view)
|
|
||||||
} else {
|
|
||||||
content.popover(isPresented: $isPresented, content: view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(visionOS 1.0, *)
|
@available(visionOS 1.0, *)
|
||||||
|
|
|
@ -125,9 +125,7 @@ public final class ComposeController: ViewController {
|
||||||
self.toolbarController = ToolbarController(parent: self)
|
self.toolbarController = ToolbarController(parent: self)
|
||||||
self.attachmentsListController = AttachmentsListController(parent: self)
|
self.attachmentsListController = AttachmentsListController(parent: self)
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
NotificationCenter.default.addObserver(self, selector: #selector(currentInputModeChanged), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(currentInputModeChanged), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
|
|
||||||
}
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: DraftsPersistentContainer.shared.viewContext)
|
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: DraftsPersistentContainer.shared.viewContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,10 +328,6 @@ public final class ComposeController: ViewController {
|
||||||
ControllerView(controller: { controller.toolbarController })
|
ControllerView(controller: { controller.toolbarController })
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#if !os(visionOS)
|
|
||||||
// on iPadOS15, the toolbar ends up below the keyboard's toolbar without this
|
|
||||||
.padding(.bottom, keyboardInset)
|
|
||||||
#endif
|
|
||||||
.transition(.move(edge: .bottom))
|
.transition(.move(edge: .bottom))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -442,7 +436,7 @@ public final class ComposeController: ViewController {
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
.scrollDismissesKeyboard(.interactively)
|
||||||
#endif
|
#endif
|
||||||
.disabled(controller.isPosting)
|
.disabled(controller.isPosting)
|
||||||
}
|
}
|
||||||
|
@ -493,31 +487,6 @@ public final class ComposeController: ViewController {
|
||||||
.keyboardShortcut(.return, modifiers: .command)
|
.keyboardShortcut(.return, modifiers: .command)
|
||||||
.disabled(!controller.postButtonEnabled)
|
.disabled(!controller.postButtonEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private var keyboardInset: CGFloat {
|
|
||||||
if #unavailable(iOS 16.0),
|
|
||||||
UIDevice.current.userInterfaceIdiom == .pad,
|
|
||||||
keyboardReader.isVisible {
|
|
||||||
return ToolbarController.height
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.scrollDismissesKeyboard(.interactively)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,14 +51,11 @@ class FocusedAttachmentController: ViewController {
|
||||||
.onAppear {
|
.onAppear {
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
} else if #available(iOS 16.0, *) {
|
} else {
|
||||||
ZoomableScrollView {
|
ZoomableScrollView {
|
||||||
attachmentView
|
attachmentView
|
||||||
.matchedGeometryDestination(id: attachment.id)
|
.matchedGeometryDestination(id: attachment.id)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
attachmentView
|
|
||||||
.matchedGeometryDestination(id: attachment.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
|
|
|
@ -96,7 +96,7 @@ class PollController: ViewController {
|
||||||
.onMove(perform: controller.moveOptions)
|
.onMove(perform: controller.moveOptions)
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.scrollDisabledIfAvailable(true)
|
.scrollDisabled(true)
|
||||||
.frame(height: 44 * CGFloat(poll.options.count))
|
.frame(height: 44 * CGFloat(poll.options.count))
|
||||||
|
|
||||||
Button(action: controller.addOption) {
|
Button(action: controller.addOption) {
|
||||||
|
|
|
@ -66,7 +66,7 @@ class ToolbarController: ViewController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0)
|
.scrollDisabled(realWidth ?? 0 <= minWidth ?? 0)
|
||||||
.frame(height: ToolbarController.height)
|
.frame(height: ToolbarController.height)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background(.regularMaterial, ignoresSafeAreaEdges: [.bottom, .leading, .trailing])
|
.background(.regularMaterial, ignoresSafeAreaEdges: [.bottom, .leading, .trailing])
|
||||||
|
@ -122,8 +122,7 @@ class ToolbarController: ViewController {
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if #available(iOS 16.0, *),
|
if composeController.mastodonController.instanceFeatures.createStatusWithLanguage {
|
||||||
composeController.mastodonController.instanceFeatures.createStatusWithLanguage {
|
|
||||||
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection)
|
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
//
|
|
||||||
// View+ForwardsCompat.swift
|
|
||||||
// ComposeUI
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 3/25/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
#if os(visionOS)
|
|
||||||
func scrollDisabledIfAvailable(_ disabled: Bool) -> some View {
|
|
||||||
self.scrollDisabled(disabled)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func scrollDisabledIfAvailable(_ disabled: Bool) -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.scrollDisabled(disabled)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
|
@ -6,12 +6,174 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import InstanceFeatures
|
||||||
|
import Vision
|
||||||
|
|
||||||
struct AttachmentRowView: View {
|
struct AttachmentRowView: View {
|
||||||
@ObservedObject var attachment: DraftAttachment
|
@ObservedObject var attachment: DraftAttachment
|
||||||
|
@State private var isRecognizingText = false
|
||||||
|
@State private var textRecognitionError: (any Error)?
|
||||||
|
|
||||||
|
private var thumbnailSize: CGFloat {
|
||||||
|
#if os(visionOS)
|
||||||
|
120
|
||||||
|
#else
|
||||||
|
80
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(attachment.id.uuidString)
|
HStack(alignment: .center, spacing: 4) {
|
||||||
|
thumbnailView
|
||||||
|
|
||||||
|
descriptionView
|
||||||
|
}
|
||||||
|
.alertWithData("Text Recognition Failed", data: $textRecognitionError) { _ in
|
||||||
|
Button("OK") {}
|
||||||
|
} message: { error in
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: attachments missing descriptions feature
|
||||||
|
|
||||||
|
private var thumbnailView: some View {
|
||||||
|
AttachmentThumbnailView(attachment: attachment)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
.frame(width: thumbnailSize, height: thumbnailSize)
|
||||||
|
.contextMenu {
|
||||||
|
EditDrawingButton(attachment: attachment)
|
||||||
|
RecognizeTextButton(attachment: attachment, isRecognizingText: $isRecognizingText, error: $textRecognitionError)
|
||||||
|
DeleteButton(attachment: attachment)
|
||||||
|
} preview: {
|
||||||
|
// TODO: need to fix flash of preview changing size
|
||||||
|
AttachmentThumbnailView(attachment: attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var descriptionView: some View {
|
||||||
|
if isRecognizingText {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
} else {
|
||||||
|
InlineAttachmentDescriptionView(attachment: attachment, minHeight: thumbnailSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct EditDrawingButton: View {
|
||||||
|
@ObservedObject var attachment: DraftAttachment
|
||||||
|
@Environment(\.composeUIConfig.presentDrawing) private var presentDrawing
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if attachment.drawingData != nil {
|
||||||
|
Button(action: editDrawing) {
|
||||||
|
Label("Edit Drawing", systemImage: "hand.draw")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func editDrawing() {
|
||||||
|
if case .drawing(let drawing) = attachment.data {
|
||||||
|
presentDrawing?(drawing) {
|
||||||
|
attachment.drawing = $0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct RecognizeTextButton: View {
|
||||||
|
@ObservedObject var attachment: DraftAttachment
|
||||||
|
@Binding var isRecognizingText: Bool
|
||||||
|
@Binding var error: (any Error)?
|
||||||
|
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if attachment.type == .image {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await recognizeText()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Recognize Text", systemImage: "doc.text.viewfinder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func recognizeText() async {
|
||||||
|
isRecognizingText = true
|
||||||
|
defer { isRecognizingText = false }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let data = try await getAttachmentData()
|
||||||
|
let observations = try await runRecognizeTextRequest(data: data)
|
||||||
|
if let observations {
|
||||||
|
var text = ""
|
||||||
|
for observation in observations {
|
||||||
|
let result = observation.topCandidates(1).first!
|
||||||
|
text.append(result.string)
|
||||||
|
text.append("\n")
|
||||||
|
}
|
||||||
|
self.attachment.attachmentDescription = text
|
||||||
|
}
|
||||||
|
} catch let error as NSError where error.domain == VNErrorDomain && error.code == 1 {
|
||||||
|
// The perform call throws an error with code 1 if the request is cancelled, which we don't want to show an alert for.
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getAttachmentData() async throws -> Data {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
attachment.getData(features: instanceFeatures) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let (data, _)):
|
||||||
|
continuation.resume(returning: data)
|
||||||
|
case .failure(let error):
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runRecognizeTextRequest(data: Data) async throws -> [VNRecognizedTextObservation]? {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
let handler = VNImageRequestHandler(data: data)
|
||||||
|
let request = VNRecognizeTextRequest { request, error in
|
||||||
|
if let error {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
} else {
|
||||||
|
continuation.resume(returning: request.results as? [VNRecognizedTextObservation])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.recognitionLevel = .accurate
|
||||||
|
request.usesLanguageCorrection = true
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
try? handler.perform([request])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DeleteButton: View {
|
||||||
|
let attachment: DraftAttachment
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(role: .destructive, action: removeAttachment) {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeAttachment() {
|
||||||
|
let draft = attachment.draft
|
||||||
|
var array = draft.draftAttachments
|
||||||
|
guard let index = array.firstIndex(of: attachment) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
array.remove(at: index)
|
||||||
|
draft.attachments = NSMutableOrderedSet(array: array)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
//
|
||||||
|
// AttachmentThumbnailView.swift
|
||||||
|
// ComposeUI
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/14/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import TuskerComponents
|
||||||
|
import AVFoundation
|
||||||
|
import Photos
|
||||||
|
|
||||||
|
struct AttachmentThumbnailView: View {
|
||||||
|
let attachment: DraftAttachment
|
||||||
|
let contentMode: ContentMode = .fit
|
||||||
|
@State private var mode: Mode = .empty
|
||||||
|
@EnvironmentObject private var composeController: ComposeController
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch mode {
|
||||||
|
case .empty:
|
||||||
|
Image(systemName: "photo")
|
||||||
|
.task {
|
||||||
|
await loadThumbnail()
|
||||||
|
}
|
||||||
|
case .image(let image):
|
||||||
|
Image(uiImage: image)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: contentMode)
|
||||||
|
case .gifController(let controller):
|
||||||
|
GIFViewWrapper(controller: controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadThumbnail() async {
|
||||||
|
switch attachment.data {
|
||||||
|
case .editing(_, let kind, let url):
|
||||||
|
switch kind {
|
||||||
|
case .image:
|
||||||
|
if let image = await composeController.fetchAttachment(url) {
|
||||||
|
self.mode = .image(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .video, .gifv:
|
||||||
|
await loadVideoThumbnail(url: url)
|
||||||
|
|
||||||
|
case .audio, .unknown:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case .asset(let id):
|
||||||
|
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil).firstObject else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let isGIF = PHAssetResource.assetResources(for: asset).contains {
|
||||||
|
$0.uniformTypeIdentifier == UTType.gif.identifier
|
||||||
|
}
|
||||||
|
if isGIF {
|
||||||
|
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: nil) { data, typeIdentifier, orientation, info in
|
||||||
|
guard let data else { return }
|
||||||
|
if typeIdentifier == UTType.gif.identifier {
|
||||||
|
self.mode = .gifController(GIFController(gifData: data))
|
||||||
|
} else if let image = UIImage(data: data) {
|
||||||
|
self.mode = .image(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let size = CGSize(width: 80, height: 80)
|
||||||
|
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { image, _ in
|
||||||
|
if let image {
|
||||||
|
self.mode = .image(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .drawing(let drawing):
|
||||||
|
self.mode = .image(drawing.imageInLightMode(from: drawing.bounds))
|
||||||
|
|
||||||
|
case .file(let url, let type):
|
||||||
|
if type.conforms(to: .movie) {
|
||||||
|
await loadVideoThumbnail(url: url)
|
||||||
|
} else if let data = try? Data(contentsOf: url) {
|
||||||
|
if type == .gif {
|
||||||
|
self.mode = .gifController(GIFController(gifData: data))
|
||||||
|
} else if type.conforms(to: .image) {
|
||||||
|
if let image = UIImage(data: data),
|
||||||
|
// using prepareThumbnail on images from PHPicker results in extremely high memory usage,
|
||||||
|
// crashing share extension. see FB12186346
|
||||||
|
let prepared = await image.byPreparingForDisplay() {
|
||||||
|
self.mode = .image(prepared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .none:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadVideoThumbnail(url: URL) async {
|
||||||
|
let asset = AVURLAsset(url: url)
|
||||||
|
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
||||||
|
imageGenerator.appliesPreferredTrackTransform = true
|
||||||
|
if let (cgImage, _) = try? await imageGenerator.image(at: .zero) {
|
||||||
|
self.mode = .image(UIImage(cgImage: cgImage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
case empty
|
||||||
|
case image(UIImage)
|
||||||
|
case gifController(GIFController)
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,95 +28,15 @@ struct AttachmentsListView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
WrappedCollectionView(attachments: draft.draftAttachments, hasPoll: draft.poll != nil, callbacks: callbacks, canAddAttachment: canAddAttachment)
|
||||||
WrappedCollectionView(attachments: draft.draftAttachments, hasPoll: draft.poll != nil, callbacks: callbacks, canAddAttachment: canAddAttachment)
|
// Impose a minimum height, because otherwise it defaults to zero which prevents the collection
|
||||||
// Impose a minimum height, because otherwise it defaults to zero which prevents the collection
|
// view from laying out, and leaving the intrinsic content size at zero too.
|
||||||
// view from laying out, and leaving the intrinsic content size at zero too.
|
.frame(minHeight: 50)
|
||||||
.frame(minHeight: 50)
|
.padding(.horizontal, -8)
|
||||||
.padding(.horizontal, -8)
|
.environmentObject(instanceFeatures)
|
||||||
} else {
|
|
||||||
LegacyAttachmentsList(draft: draft, callbacks: callbacks, canAddAttachment: canAddAttachment)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private struct LegacyAttachmentsList: View {
|
|
||||||
@ObservedObject var draft: Draft
|
|
||||||
let callbacks: Callbacks
|
|
||||||
let canAddAttachment: Bool
|
|
||||||
@State private var attachmentHeights = [NSManagedObjectID: CGFloat]()
|
|
||||||
|
|
||||||
private var totalHeight: CGFloat {
|
|
||||||
let buttonsHeight = 3 * (40 + AttachmentsListPaddingModifier.cellPadding)
|
|
||||||
let rowHeights = draft.attachments.compactMap {
|
|
||||||
attachmentHeights[($0 as! NSManagedObject).objectID]
|
|
||||||
}.reduce(0) { partialResult, height in
|
|
||||||
partialResult + height + AttachmentsListPaddingModifier.cellPadding
|
|
||||||
}
|
|
||||||
return buttonsHeight + rowHeights
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
List {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
.listStyle(.plain)
|
|
||||||
.frame(height: totalHeight)
|
|
||||||
.scrollDisabledIfAvailable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var content: some View {
|
|
||||||
ForEach(draft.draftAttachments) { attachment in
|
|
||||||
AttachmentRowView(attachment: attachment)
|
|
||||||
.modifier(AttachmentRowHeightModifier(height: $attachmentHeights[attachment.objectID]))
|
|
||||||
}
|
|
||||||
.onMove(perform: self.moveAttachments)
|
|
||||||
.onDelete(perform: self.removeAttachments)
|
|
||||||
|
|
||||||
AddPhotoButton(canAddAttachment: canAddAttachment, draft: draft, insertAttachments: self.insertAttachments)
|
|
||||||
|
|
||||||
AddDrawingButton(canAddAttachment: canAddAttachment)
|
|
||||||
|
|
||||||
TogglePollButton(poll: draft.poll)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move this to Callbacks
|
|
||||||
private func insertAttachments(at offset: Int, itemProviders: [NSItemProvider]) {
|
|
||||||
for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) {
|
|
||||||
provider.loadObject(ofClass: DraftAttachment.self) { object, error in
|
|
||||||
guard let attachment = object as? DraftAttachment else { return }
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard self.canAddAttachment else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DraftsPersistentContainer.shared.viewContext.insert(attachment)
|
|
||||||
attachment.draft = self.draft
|
|
||||||
self.draft.attachments.add(attachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move this to Callbacks
|
|
||||||
private func moveAttachments(from source: IndexSet, to destination: Int) {
|
|
||||||
// just using moveObjects(at:to:) on the draft.attachments NSMutableOrderedSet
|
|
||||||
// results in the order switching back to the previous order and then to the correct one
|
|
||||||
// on the subsequent 2 view updates. creating a new set with the proper order doesn't have that problem
|
|
||||||
var array = draft.draftAttachments
|
|
||||||
array.move(fromOffsets: source, toOffset: destination)
|
|
||||||
draft.attachments = NSMutableOrderedSet(array: array)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func removeAttachments(at indices: IndexSet) {
|
|
||||||
for index in indices {
|
|
||||||
callbacks.removeAttachment(at: index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct Callbacks {
|
private struct Callbacks {
|
||||||
let draft: Draft
|
let draft: Draft
|
||||||
let presentAssetPicker: ((@MainActor @escaping ([PHPickerResult]) -> Void) -> Void)?
|
let presentAssetPicker: ((@MainActor @escaping ([PHPickerResult]) -> Void) -> Void)?
|
||||||
|
@ -143,6 +63,11 @@ private struct Callbacks {
|
||||||
draft.attachments = NSMutableOrderedSet(array: array)
|
draft.attachments = NSMutableOrderedSet(array: array)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reorderAttachments(with difference: CollectionDifference<DraftAttachment>) {
|
||||||
|
let array = draft.draftAttachments.applying(difference)!
|
||||||
|
draft.attachments = NSMutableOrderedSet(array: array)
|
||||||
|
}
|
||||||
|
|
||||||
func addPhoto() {
|
func addPhoto() {
|
||||||
presentAssetPicker?() {
|
presentAssetPicker?() {
|
||||||
insertAttachments(at: draft.attachments.count, itemProviders: $0.map(\.itemProvider))
|
insertAttachments(at: draft.attachments.count, itemProviders: $0.map(\.itemProvider))
|
||||||
|
@ -157,104 +82,6 @@ private struct Callbacks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct AttachmentRowHeightPreferenceKey: PreferenceKey {
|
|
||||||
static var defaultValue: CGFloat { 0 }
|
|
||||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
||||||
value = nextValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AttachmentRowHeightModifier: ViewModifier {
|
|
||||||
@Binding var height: CGFloat?
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.background {
|
|
||||||
GeometryReader { proxy in
|
|
||||||
Color.clear
|
|
||||||
// do the preference dance because onChange(of:inital:_:) is iOS 17+ :/
|
|
||||||
.preference(key: AttachmentRowHeightPreferenceKey.self, value: proxy.size.height)
|
|
||||||
.onPreferenceChange(AttachmentRowHeightPreferenceKey.self) { newValue in
|
|
||||||
height = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AttachmentsListPaddingModifier: ViewModifier {
|
|
||||||
static let cellPadding: CGFloat = 12
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.listRowInsets(EdgeInsets(top: Self.cellPadding / 2, leading: Self.cellPadding / 2, bottom: Self.cellPadding / 2, trailing: Self.cellPadding / 2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AttachmentsListButton<Label: View>: View {
|
|
||||||
let action: () -> Void
|
|
||||||
@ViewBuilder let label: Label
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
label
|
|
||||||
}
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
.frame(height: 40)
|
|
||||||
.modifier(AttachmentsListPaddingModifier())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AddPhotoButton: View {
|
|
||||||
let canAddAttachment: Bool
|
|
||||||
let draft: Draft
|
|
||||||
let insertAttachments: (Int, [NSItemProvider]) -> Void
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
|
||||||
@Environment(\.composeUIConfig.presentAssetPicker) private var presentAssetPicker
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
AttachmentsListButton {
|
|
||||||
presentAssetPicker?() { results in
|
|
||||||
insertAttachments(draft.attachments.count, results.map(\.itemProvider))
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Label("Add photo or video", systemImage: colorScheme == .dark ? "photo.fill" : "photo")
|
|
||||||
}
|
|
||||||
.disabled(!canAddAttachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AddDrawingButton: View {
|
|
||||||
let canAddAttachment: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
AttachmentsListButton {
|
|
||||||
fatalError("TODO")
|
|
||||||
} label: {
|
|
||||||
Label("Draw something", systemImage: "hand.draw")
|
|
||||||
}
|
|
||||||
.disabled(!canAddAttachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct TogglePollButton: View {
|
|
||||||
let poll: Poll?
|
|
||||||
|
|
||||||
var canAddPoll: Bool {
|
|
||||||
// TODO
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
AttachmentsListButton {
|
|
||||||
fatalError("TODO")
|
|
||||||
} label: {
|
|
||||||
Label(poll == nil ? "Add a poll" : "Remove poll", systemImage: "chart.bar.doc.horizontal")
|
|
||||||
}
|
|
||||||
.disabled(!canAddPoll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
private struct WrappedCollectionView: UIViewRepresentable {
|
private struct WrappedCollectionView: UIViewRepresentable {
|
||||||
let attachments: [DraftAttachment]
|
let attachments: [DraftAttachment]
|
||||||
|
@ -279,6 +106,31 @@ private struct WrappedCollectionView: UIViewRepresentable {
|
||||||
context.coordinator.dataSource = dataSource
|
context.coordinator.dataSource = dataSource
|
||||||
view.delegate = context.coordinator
|
view.delegate = context.coordinator
|
||||||
view.isScrollEnabled = false
|
view.isScrollEnabled = false
|
||||||
|
|
||||||
|
dataSource.reorderingHandlers.canReorderItem = {
|
||||||
|
if case .attachment(_) = $0 {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.reorderingHandlers.didReorder = { transaction in
|
||||||
|
let attachmentsChanges = transaction.difference.map {
|
||||||
|
switch $0 {
|
||||||
|
case .insert(let offset, let element, let associatedWith):
|
||||||
|
guard case .attachment(let attachment) = element else { fatalError() }
|
||||||
|
return CollectionDifference<DraftAttachment>.Change.insert(offset: offset, element: attachment, associatedWith: associatedWith)
|
||||||
|
case .remove(let offset, let element, let associatedWith):
|
||||||
|
guard case .attachment(let attachment) = element else { fatalError() }
|
||||||
|
return CollectionDifference<DraftAttachment>.Change.remove(offset: offset, element: attachment, associatedWith: associatedWith)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let attachmentsDiff = CollectionDifference(attachmentsChanges)!
|
||||||
|
callbacks.reorderAttachments(with: attachmentsDiff)
|
||||||
|
}
|
||||||
|
let longPressRecognizer = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(WrappedCollectionViewCoordinator.reorderingLongPressRecognized))
|
||||||
|
longPressRecognizer.delegate = context.coordinator
|
||||||
|
view.addGestureRecognizer(longPressRecognizer)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +219,7 @@ private final class IntrinsicContentSizeCollectionView: UICollectionView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
private final class WrappedCollectionViewCoordinator: NSObject, UICollectionViewDelegate {
|
private final class WrappedCollectionViewCoordinator: NSObject, UICollectionViewDelegate, UIGestureRecognizerDelegate {
|
||||||
var callbacks: Callbacks
|
var callbacks: Callbacks
|
||||||
var setHeightOfCellBeingDeleted: ((CGFloat) -> Void)?
|
var setHeightOfCellBeingDeleted: ((CGFloat) -> Void)?
|
||||||
|
|
||||||
|
@ -379,7 +231,7 @@ private final class WrappedCollectionViewCoordinator: NSObject, UICollectionView
|
||||||
|
|
||||||
private let attachmentCell = UICollectionView.CellRegistration<UICollectionViewListCell, DraftAttachment> { cell, indexPath, item in
|
private let attachmentCell = UICollectionView.CellRegistration<UICollectionViewListCell, DraftAttachment> { cell, indexPath, item in
|
||||||
cell.contentConfiguration = UIHostingConfiguration {
|
cell.contentConfiguration = UIHostingConfiguration {
|
||||||
Text(item.id.uuidString)
|
AttachmentRowView(attachment: item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -456,6 +308,41 @@ private final class WrappedCollectionViewCoordinator: NSObject, UICollectionView
|
||||||
callbacks.togglePoll()
|
callbacks.togglePoll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveOfItemFromOriginalIndexPath originalIndexPath: IndexPath, atCurrentIndexPath currentIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
|
||||||
|
let snapshot = dataSource.snapshot()
|
||||||
|
let attachmentsSection = snapshot.indexOfSection(.attachments)!
|
||||||
|
if proposedIndexPath.section != attachmentsSection {
|
||||||
|
return IndexPath(item: snapshot.itemIdentifiers(inSection: .attachments).count - 1, section: attachmentsSection)
|
||||||
|
} else {
|
||||||
|
return proposedIndexPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
let collectionView = gestureRecognizer.view as! UICollectionView
|
||||||
|
let location = gestureRecognizer.location(in: collectionView)
|
||||||
|
guard let indexPath = collectionView.indexPathForItem(at: location) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return collectionView.beginInteractiveMovementForItem(at: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func reorderingLongPressRecognized(_ recognizer: UILongPressGestureRecognizer) {
|
||||||
|
let collectionView = recognizer.view as! UICollectionView
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
break
|
||||||
|
case .changed:
|
||||||
|
collectionView.updateInteractiveMovementTargetPosition(recognizer.location(in: collectionView))
|
||||||
|
case .ended:
|
||||||
|
collectionView.endInteractiveMovement()
|
||||||
|
case .cancelled:
|
||||||
|
collectionView.cancelInteractiveMovement()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ struct ComposeToolbarView: View {
|
||||||
|
|
||||||
VisibilityButton(draft: draft, instanceFeatures: mastodonController.instanceFeatures)
|
VisibilityButton(draft: draft, instanceFeatures: mastodonController.instanceFeatures)
|
||||||
|
|
||||||
LocalOnlyButton(enabled: $draft.contentWarningEnabled, mastodonController: mastodonController)
|
LocalOnlyButton(enabled: $draft.localOnly, mastodonController: mastodonController)
|
||||||
|
|
||||||
InsertEmojiButton()
|
InsertEmojiButton()
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ private struct ToolbarScrollView<Content: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0)
|
.scrollDisabled(realWidth ?? 0 <= minWidth ?? 0)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background {
|
.background {
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
|
@ -246,8 +246,7 @@ private struct LangaugeButton: View {
|
||||||
@State private var hasChanged = false
|
@State private var hasChanged = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *),
|
if instanceFeatures.createStatusWithLanguage {
|
||||||
instanceFeatures.createStatusWithLanguage {
|
|
||||||
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $hasChanged)
|
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $hasChanged)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UITextInputMode.currentInputModeDidChangeNotification), perform: currentInputModeChanged)
|
.onReceive(NotificationCenter.default.publisher(for: UITextInputMode.currentInputModeDidChangeNotification), perform: currentInputModeChanged)
|
||||||
.onChange(of: draft.id) { _ in
|
.onChange(of: draft.id) { _ in
|
||||||
|
|
|
@ -15,15 +15,8 @@ struct ComposeView: View {
|
||||||
@EnvironmentObject private var controller: ComposeController
|
@EnvironmentObject private var controller: ComposeController
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
NavigationStack {
|
||||||
NavigationStack {
|
navigationRoot
|
||||||
navigationRoot
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NavigationView {
|
|
||||||
navigationRoot
|
|
||||||
}
|
|
||||||
.navigationViewStyle(.stack)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +25,7 @@ struct ComposeView: View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
scrollContent
|
scrollContent
|
||||||
}
|
}
|
||||||
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
.scrollDismissesKeyboard(.interactively)
|
||||||
#if !os(visionOS) && !targetEnvironment(macCatalyst)
|
#if !os(visionOS) && !targetEnvironment(macCatalyst)
|
||||||
.modifier(ToolbarSafeAreaInsetModifier())
|
.modifier(ToolbarSafeAreaInsetModifier())
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -18,7 +18,6 @@ struct NewMainTextView: View {
|
||||||
NewMainTextViewRepresentable(value: $value, becomeFirstResponder: $becomeFirstResponder)
|
NewMainTextViewRepresentable(value: $value, becomeFirstResponder: $becomeFirstResponder)
|
||||||
.focused($focusedField, equals: .body)
|
.focused($focusedField, equals: .body)
|
||||||
.modifier(FocusedInputModifier())
|
.modifier(FocusedInputModifier())
|
||||||
.modifier(HeightExpandingModifier(minHeight: Self.minHeight))
|
|
||||||
.overlay(alignment: .topLeading) {
|
.overlay(alignment: .topLeading) {
|
||||||
if value.isEmpty {
|
if value.isEmpty {
|
||||||
PlaceholderView()
|
PlaceholderView()
|
||||||
|
@ -37,18 +36,10 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
||||||
@Environment(\.composeUIConfig.useTwitterKeyboard) private var useTwitterKeyboard
|
@Environment(\.composeUIConfig.useTwitterKeyboard) private var useTwitterKeyboard
|
||||||
// TODO: test textSelectionStartsAtBeginning
|
// TODO: test textSelectionStartsAtBeginning
|
||||||
@Environment(\.composeUIConfig.textSelectionStartsAtBeginning) private var textSelectionStartsAtBeginning
|
@Environment(\.composeUIConfig.textSelectionStartsAtBeginning) private var textSelectionStartsAtBeginning
|
||||||
#if !os(visionOS)
|
|
||||||
@Environment(\.textViewContentHeight) @Binding private var textViewContentHeight
|
|
||||||
#endif
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UITextView {
|
func makeUIView(context: Context) -> UITextView {
|
||||||
let view: UITextView
|
|
||||||
// TODO: if we're not doing the pill background, reevaluate whether this version fork is necessary
|
// TODO: if we're not doing the pill background, reevaluate whether this version fork is necessary
|
||||||
if #available(iOS 16.0, *) {
|
let view = WrappedTextView(usingTextLayoutManager: true)
|
||||||
view = WrappedTextView(usingTextLayoutManager: true)
|
|
||||||
} else {
|
|
||||||
view = WrappedTextView()
|
|
||||||
}
|
|
||||||
view.delegate = context.coordinator
|
view.delegate = context.coordinator
|
||||||
view.adjustsFontForContentSizeCategory = true
|
view.adjustsFontForContentSizeCategory = true
|
||||||
view.textContainer.lineBreakMode = .byWordWrapping
|
view.textContainer.lineBreakMode = .byWordWrapping
|
||||||
|
@ -92,16 +83,6 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
||||||
becomeFirstResponder = false
|
becomeFirstResponder = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
if #unavailable(iOS 16.0) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let targetSize = CGSize(width: uiView.bounds.width, height: UIView.layoutFittingCompressedSize.height)
|
|
||||||
let fittingSize = uiView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultHigh)
|
|
||||||
textViewContentHeight = fittingSize.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeCoordinator() -> WrappedTextViewCoordinator {
|
func makeCoordinator() -> WrappedTextViewCoordinator {
|
||||||
|
@ -120,6 +101,9 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// laxer than the CharacterCounter regex, because we want to find mentions that are being typed but aren't yet complete (e.g., "@a@b")
|
||||||
|
private let mentionRegex = try! NSRegularExpression(pattern: "(@[a-z0-9_]+)(?:@[a-z0-9\\-\\.]+)?", options: .caseInsensitive)
|
||||||
|
|
||||||
private final class WrappedTextViewCoordinator: NSObject {
|
private final class WrappedTextViewCoordinator: NSObject {
|
||||||
private static let attachment: NSTextAttachment = {
|
private static let attachment: NSTextAttachment = {
|
||||||
let font = UIFont.systemFont(ofSize: 20)
|
let font = UIFont.systemFont(ofSize: 20)
|
||||||
|
@ -149,13 +133,8 @@ private final class WrappedTextViewCoordinator: NSObject {
|
||||||
let str = NSMutableAttributedString(string: text)
|
let str = NSMutableAttributedString(string: text)
|
||||||
let mentionMatches = CharacterCounter.mention.matches(in: text, range: NSRange(location: 0, length: str.length))
|
let mentionMatches = CharacterCounter.mention.matches(in: text, range: NSRange(location: 0, length: str.length))
|
||||||
for match in mentionMatches.reversed() {
|
for match in mentionMatches.reversed() {
|
||||||
let range: NSRange
|
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
||||||
if #available(iOS 16.0, *) {
|
let range = NSRange(location: match.range.location, length: match.range.length + 1)
|
||||||
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
|
||||||
range = NSRange(location: match.range.location, length: match.range.length + 1)
|
|
||||||
} else {
|
|
||||||
range = match.range
|
|
||||||
}
|
|
||||||
str.addAttributes([
|
str.addAttributes([
|
||||||
.mention: true,
|
.mention: true,
|
||||||
.foregroundColor: UIColor.tintColor,
|
.foregroundColor: UIColor.tintColor,
|
||||||
|
@ -173,6 +152,7 @@ private final class WrappedTextViewCoordinator: NSObject {
|
||||||
private func updateAttributes(in textView: UITextView) {
|
private func updateAttributes(in textView: UITextView) {
|
||||||
let str = NSMutableAttributedString(attributedString: textView.attributedText!)
|
let str = NSMutableAttributedString(attributedString: textView.attributedText!)
|
||||||
var changed = false
|
var changed = false
|
||||||
|
var cursorOffset = 0
|
||||||
|
|
||||||
// remove existing mentions that aren't valid
|
// remove existing mentions that aren't valid
|
||||||
str.enumerateAttribute(.mention, in: NSRange(location: 0, length: str.length), options: .reverse) { value, range, stop in
|
str.enumerateAttribute(.mention, in: NSRange(location: 0, length: str.length), options: .reverse) { value, range, stop in
|
||||||
|
@ -181,44 +161,46 @@ private final class WrappedTextViewCoordinator: NSObject {
|
||||||
if hasTextAttachment {
|
if hasTextAttachment {
|
||||||
substr = String(substr.dropFirst())
|
substr = String(substr.dropFirst())
|
||||||
}
|
}
|
||||||
if CharacterCounter.mention.numberOfMatches(in: substr, range: NSRange(location: 0, length: substr.utf16.count)) == 0 {
|
if mentionRegex.numberOfMatches(in: substr, range: NSRange(location: 0, length: substr.utf16.count)) == 0 {
|
||||||
changed = true
|
changed = true
|
||||||
str.removeAttribute(.mention, range: range)
|
str.removeAttribute(.mention, range: range)
|
||||||
str.removeAttribute(.foregroundColor, range: range)
|
str.removeAttribute(.foregroundColor, range: range)
|
||||||
if hasTextAttachment {
|
if hasTextAttachment {
|
||||||
str.deleteCharacters(in: NSRange(location: range.location, length: 1))
|
str.deleteCharacters(in: NSRange(location: range.location, length: 1))
|
||||||
|
cursorOffset -= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add mentions for those missing
|
// add mentions for those missing
|
||||||
let mentionMatches = CharacterCounter.mention.matches(in: str.string, range: NSRange(location: 0, length: str.length))
|
let mentionMatches = mentionRegex.matches(in: str.string, range: NSRange(location: 0, length: str.length))
|
||||||
for match in mentionMatches.reversed() {
|
for match in mentionMatches.reversed() {
|
||||||
var attributeRange = NSRange()
|
var attributeRange = NSRange()
|
||||||
let attribute = str.attribute(.mention, at: match.range.location, effectiveRange: &attributeRange)
|
let attribute = str.attribute(.mention, at: match.range.location, effectiveRange: &attributeRange)
|
||||||
// the attribute range should always be one greater than the match range, to account for the text attachment
|
// the attribute range should always be one greater than the match range, to account for the text attachment
|
||||||
if attribute == nil || attributeRange.length <= match.range.length {
|
if attribute == nil || attributeRange.length <= match.range.length {
|
||||||
changed = true
|
changed = true
|
||||||
if #available(iOS 16.0, *) {
|
let newAttributeRange: NSRange
|
||||||
let newAttributeRange: NSRange
|
if attribute == nil {
|
||||||
if attribute == nil {
|
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
||||||
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
newAttributeRange = NSRange(location: match.range.location, length: match.range.length + 1)
|
||||||
newAttributeRange = NSRange(location: match.range.location, length: match.range.length + 1)
|
cursorOffset += 1
|
||||||
} else {
|
|
||||||
newAttributeRange = match.range
|
|
||||||
}
|
|
||||||
str.addAttributes([
|
|
||||||
.mention: true,
|
|
||||||
.foregroundColor: UIColor.tintColor,
|
|
||||||
], range: newAttributeRange)
|
|
||||||
} else {
|
} else {
|
||||||
str.addAttribute(.foregroundColor, value: UIColor.tintColor, range: match.range)
|
newAttributeRange = match.range
|
||||||
}
|
}
|
||||||
|
str.addAttributes([
|
||||||
|
.mention: true,
|
||||||
|
.foregroundColor: UIColor.tintColor,
|
||||||
|
], range: newAttributeRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
|
let selection = textView.selectedRange
|
||||||
|
|
||||||
textView.attributedText = str
|
textView.attributedText = str
|
||||||
|
|
||||||
|
textView.selectedRange = NSRange(location: selection.location + cursorOffset, length: selection.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,38 +258,3 @@ private struct PlaceholderView: View {
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private struct HeightExpandingModifier: ViewModifier {
|
|
||||||
let minHeight: CGFloat
|
|
||||||
|
|
||||||
@State private var height: CGFloat?
|
|
||||||
private var effectiveHeight: CGFloat {
|
|
||||||
height.map { max($0, minHeight) } ?? minHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
content
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
.frame(height: effectiveHeight)
|
|
||||||
.environment(\.textViewContentHeight, $height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private struct TextViewContentHeightKey: EnvironmentKey {
|
|
||||||
static var defaultValue: Binding<CGFloat?> { .constant(nil) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private extension EnvironmentValues {
|
|
||||||
var textViewContentHeight: Binding<CGFloat?> {
|
|
||||||
get { self[TextViewContentHeightKey.self] }
|
|
||||||
set { self[TextViewContentHeightKey.self] = newValue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.7
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "Duckable",
|
name: "Duckable",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
@ -23,7 +23,10 @@ let package = Package(
|
||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "Duckable",
|
name: "Duckable",
|
||||||
dependencies: []),
|
dependencies: [],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
// .testTarget(
|
// .testTarget(
|
||||||
// name: "DuckableTests",
|
// name: "DuckableTests",
|
||||||
// dependencies: ["Duckable"]),
|
// dependencies: ["Duckable"]),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.10
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "GalleryVC",
|
name: "GalleryVC",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
|
@ -18,9 +18,15 @@ let package = Package(
|
||||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||||
// Targets can depend on other targets in this package and products from dependencies.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
.target(
|
.target(
|
||||||
name: "GalleryVC"),
|
name: "GalleryVC",
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "GalleryVCTests",
|
name: "GalleryVCTests",
|
||||||
dependencies: ["GalleryVC"]),
|
dependencies: ["GalleryVC"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.7
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "InstanceFeatures",
|
name: "InstanceFeatures",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
@ -23,9 +23,15 @@ let package = Package(
|
||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "InstanceFeatures",
|
name: "InstanceFeatures",
|
||||||
dependencies: ["Pachyderm"]),
|
dependencies: ["Pachyderm"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "InstanceFeaturesTests",
|
name: "InstanceFeaturesTests",
|
||||||
dependencies: ["InstanceFeatures"]),
|
dependencies: ["InstanceFeatures"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.8
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "MatchedGeometryPresentation",
|
name: "MatchedGeometryPresentation",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
|
@ -18,7 +18,10 @@ let package = Package(
|
||||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||||
// Targets can depend on other targets in this package and products from dependencies.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
.target(
|
.target(
|
||||||
name: "MatchedGeometryPresentation"),
|
name: "MatchedGeometryPresentation",
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
// .testTarget(
|
// .testTarget(
|
||||||
// name: "MatchedGeometryPresentationTests",
|
// name: "MatchedGeometryPresentationTests",
|
||||||
// dependencies: ["MatchedGeometryPresentation"]),
|
// dependencies: ["MatchedGeometryPresentation"]),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.6
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "Pachyderm",
|
name: "Pachyderm",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
@ -26,9 +26,15 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "WebURL", package: "swift-url"),
|
.product(name: "WebURL", package: "swift-url"),
|
||||||
.product(name: "WebURLFoundationExtras", package: "swift-url"),
|
.product(name: "WebURLFoundationExtras", package: "swift-url"),
|
||||||
|
],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
]),
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "PachydermTests",
|
name: "PachydermTests",
|
||||||
dependencies: ["Pachyderm"]),
|
dependencies: ["Pachyderm"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.10
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "PushNotifications",
|
name: "PushNotifications",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
|
@ -23,10 +23,17 @@ let package = Package(
|
||||||
// Targets can depend on other targets in this package and products from dependencies.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
.target(
|
.target(
|
||||||
name: "PushNotifications",
|
name: "PushNotifications",
|
||||||
dependencies: ["UserAccounts", "Pachyderm"]
|
dependencies: ["UserAccounts", "Pachyderm"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "PushNotificationsTests",
|
name: "PushNotificationsTests",
|
||||||
dependencies: ["PushNotifications"]),
|
dependencies: ["PushNotifications"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.7
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "TTTKit",
|
name: "TTTKit",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
@ -23,9 +23,15 @@ let package = Package(
|
||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "TTTKit",
|
name: "TTTKit",
|
||||||
dependencies: []),
|
dependencies: [],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "TTTKitTests",
|
name: "TTTKitTests",
|
||||||
dependencies: ["TTTKit"]),
|
dependencies: ["TTTKit"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.7
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "TuskerComponents",
|
name: "TuskerComponents",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
@ -23,7 +23,10 @@ let package = Package(
|
||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "TuskerComponents",
|
name: "TuskerComponents",
|
||||||
dependencies: []),
|
dependencies: [],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
// .testTarget(
|
// .testTarget(
|
||||||
// name: "TuskerComponentsTests",
|
// name: "TuskerComponentsTests",
|
||||||
// dependencies: ["TuskerComponents"]),
|
// dependencies: ["TuskerComponents"]),
|
||||||
|
|
|
@ -9,21 +9,14 @@ import SwiftUI
|
||||||
|
|
||||||
public struct AsyncPicker<V: Hashable, Content: View>: View {
|
public struct AsyncPicker<V: Hashable, Content: View>: View {
|
||||||
let titleKey: LocalizedStringKey
|
let titleKey: LocalizedStringKey
|
||||||
#if !os(visionOS)
|
|
||||||
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
|
||||||
let labelHidden: Bool
|
|
||||||
#endif
|
|
||||||
let alignment: Alignment
|
let alignment: Alignment
|
||||||
@Binding var value: V
|
@Binding var value: V
|
||||||
let onChange: (V) async -> Bool
|
let onChange: (V) async -> Bool
|
||||||
let content: Content
|
let content: Content
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
|
|
||||||
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, alignment: Alignment = .center, value: Binding<V>, onChange: @escaping (V) async -> Bool, @ViewBuilder content: () -> Content) {
|
public init(_ titleKey: LocalizedStringKey, alignment: Alignment = .center, value: Binding<V>, onChange: @escaping (V) async -> Bool, @ViewBuilder content: () -> Content) {
|
||||||
self.titleKey = titleKey
|
self.titleKey = titleKey
|
||||||
#if !os(visionOS)
|
|
||||||
self.labelHidden = labelHidden
|
|
||||||
#endif
|
|
||||||
self.alignment = alignment
|
self.alignment = alignment
|
||||||
self._value = value
|
self._value = value
|
||||||
self.onChange = onChange
|
self.onChange = onChange
|
||||||
|
@ -31,25 +24,9 @@ public struct AsyncPicker<V: Hashable, Content: View>: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
#if os(visionOS)
|
|
||||||
LabeledContent(titleKey) {
|
LabeledContent(titleKey) {
|
||||||
picker
|
picker
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
LabeledContent(titleKey) {
|
|
||||||
picker
|
|
||||||
}
|
|
||||||
} else if labelHidden {
|
|
||||||
picker
|
|
||||||
} else {
|
|
||||||
HStack {
|
|
||||||
Text(titleKey)
|
|
||||||
Spacer()
|
|
||||||
picker
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var picker: some View {
|
private var picker: some View {
|
||||||
|
|
|
@ -10,42 +10,19 @@ import SwiftUI
|
||||||
|
|
||||||
public struct AsyncToggle: View {
|
public struct AsyncToggle: View {
|
||||||
let titleKey: LocalizedStringKey
|
let titleKey: LocalizedStringKey
|
||||||
#if !os(visionOS)
|
|
||||||
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
|
||||||
let labelHidden: Bool
|
|
||||||
#endif
|
|
||||||
@Binding var mode: Mode
|
@Binding var mode: Mode
|
||||||
let onChange: (Bool) async -> Bool
|
let onChange: (Bool) async -> Bool
|
||||||
|
|
||||||
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Bool) {
|
public init(_ titleKey: LocalizedStringKey, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Bool) {
|
||||||
self.titleKey = titleKey
|
self.titleKey = titleKey
|
||||||
#if !os(visionOS)
|
|
||||||
self.labelHidden = labelHidden
|
|
||||||
#endif
|
|
||||||
self._mode = mode
|
self._mode = mode
|
||||||
self.onChange = onChange
|
self.onChange = onChange
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
#if os(visionOS)
|
|
||||||
LabeledContent(titleKey) {
|
LabeledContent(titleKey) {
|
||||||
toggleOrSpinner
|
toggleOrSpinner
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
LabeledContent(titleKey) {
|
|
||||||
toggleOrSpinner
|
|
||||||
}
|
|
||||||
} else if labelHidden {
|
|
||||||
toggleOrSpinner
|
|
||||||
} else {
|
|
||||||
HStack {
|
|
||||||
Text(titleKey)
|
|
||||||
Spacer()
|
|
||||||
toggleOrSpinner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -47,9 +47,7 @@ public struct MenuPicker<Value: Hashable>: UIViewRepresentable {
|
||||||
|
|
||||||
private func makeConfiguration() -> UIButton.Configuration {
|
private func makeConfiguration() -> UIButton.Configuration {
|
||||||
var config = UIButton.Configuration.borderless()
|
var config = UIButton.Configuration.borderless()
|
||||||
if #available(iOS 16.0, *) {
|
config.indicator = .popup
|
||||||
config.indicator = .popup
|
|
||||||
}
|
|
||||||
if buttonStyle.hasIcon {
|
if buttonStyle.hasIcon {
|
||||||
config.image = selectedOption.image
|
config.image = selectedOption.image
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.8
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "TuskerPreferences",
|
name: "TuskerPreferences",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
|
@ -22,11 +22,17 @@ let package = Package(
|
||||||
// Targets can depend on other targets in this package and products from dependencies.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
.target(
|
.target(
|
||||||
name: "TuskerPreferences",
|
name: "TuskerPreferences",
|
||||||
dependencies: ["Pachyderm"]
|
dependencies: ["Pachyderm"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "TuskerPreferencesTests",
|
name: "TuskerPreferencesTests",
|
||||||
dependencies: ["TuskerPreferences"]
|
dependencies: ["TuskerPreferences"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.7
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
@ -6,7 +6,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "UserAccounts",
|
name: "UserAccounts",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
@ -23,7 +23,10 @@ let package = Package(
|
||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "UserAccounts",
|
name: "UserAccounts",
|
||||||
dependencies: ["Pachyderm"]),
|
dependencies: ["Pachyderm"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
// .testTarget(
|
// .testTarget(
|
||||||
// name: "UserAccountsTests",
|
// name: "UserAccountsTests",
|
||||||
// dependencies: ["UserAccounts"]),
|
// dependencies: ["UserAccounts"]),
|
||||||
|
|
|
@ -141,7 +141,6 @@
|
||||||
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96802BC3279D002C8990 /* PrefsAccountView.swift */; };
|
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96802BC3279D002C8990 /* PrefsAccountView.swift */; };
|
||||||
D64B96842BC3893C002C8990 /* PushSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */; };
|
D64B96842BC3893C002C8990 /* PushSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */; };
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
|
||||||
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
|
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
|
||||||
D6531DEE246B81C9000F9538 /* GifvPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvPlayerView.swift */; };
|
D6531DEE246B81C9000F9538 /* GifvPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvPlayerView.swift */; };
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
||||||
|
@ -337,7 +336,7 @@
|
||||||
D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F582A13293200AB2315 /* BackgroundManager.swift */; };
|
D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F582A13293200AB2315 /* BackgroundManager.swift */; };
|
||||||
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
||||||
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
||||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
D6D9498F298EB79400C59229 /* CopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLabel.swift */; };
|
||||||
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
|
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
|
||||||
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; };
|
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; };
|
||||||
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */; };
|
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */; };
|
||||||
|
@ -572,7 +571,6 @@
|
||||||
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsAccountView.swift; sourceTree = "<group>"; };
|
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsAccountView.swift; sourceTree = "<group>"; };
|
||||||
D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSubscriptionView.swift; sourceTree = "<group>"; };
|
D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSubscriptionView.swift; sourceTree = "<group>"; };
|
||||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
|
||||||
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
|
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
|
||||||
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvPlayerView.swift; sourceTree = "<group>"; };
|
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvPlayerView.swift; sourceTree = "<group>"; };
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
||||||
|
@ -778,7 +776,7 @@
|
||||||
D6D79F582A13293200AB2315 /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = "<group>"; };
|
D6D79F582A13293200AB2315 /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = "<group>"; };
|
||||||
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
||||||
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
D6D9498E298EB79400C59229 /* CopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLabel.swift; sourceTree = "<group>"; };
|
||||||
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; };
|
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; };
|
||||||
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; };
|
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; };
|
||||||
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfilesViewController.swift; sourceTree = "<group>"; };
|
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfilesViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1490,7 +1488,7 @@
|
||||||
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
||||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */,
|
D620483523D38075008A63EF /* ContentTextView.swift */,
|
||||||
D6D9498E298EB79400C59229 /* CopyableLable.swift */,
|
D6D9498E298EB79400C59229 /* CopyableLabel.swift */,
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
||||||
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */,
|
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */,
|
||||||
|
@ -1627,7 +1625,6 @@
|
||||||
D6DEBA8C2B6579830008629A /* MainThreadBox.swift */,
|
D6DEBA8C2B6579830008629A /* MainThreadBox.swift */,
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */,
|
D6B81F432560390300F6E31D /* MenuController.swift */,
|
||||||
D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */,
|
D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */,
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
|
||||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||||
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */,
|
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */,
|
||||||
D6895DE828D962C2006341DA /* TimelineLikeController.swift */,
|
D6895DE828D962C2006341DA /* TimelineLikeController.swift */,
|
||||||
|
@ -2306,7 +2303,7 @@
|
||||||
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
|
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
|
||||||
D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */,
|
D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */,
|
||||||
D65B4B5A29720AB000DABDFB /* ReportStatusView.swift in Sources */,
|
D65B4B5A29720AB000DABDFB /* ReportStatusView.swift in Sources */,
|
||||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */,
|
D6D9498F298EB79400C59229 /* CopyableLabel.swift in Sources */,
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||||
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
|
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||||
|
@ -2390,7 +2387,6 @@
|
||||||
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
||||||
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
|
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
|
||||||
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */,
|
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */,
|
||||||
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
|
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
|
||||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||||
|
@ -2547,6 +2543,7 @@
|
||||||
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2579,6 +2576,7 @@
|
||||||
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2610,6 +2608,7 @@
|
||||||
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2700,6 +2699,7 @@
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2743,7 +2743,7 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2766,6 +2766,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2793,6 +2794,7 @@
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2821,6 +2823,7 @@
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2849,6 +2852,7 @@
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -3004,6 +3008,7 @@
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -3036,6 +3041,7 @@
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -3100,7 +3106,7 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -3120,7 +3126,7 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -3143,6 +3149,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -3167,6 +3174,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import os
|
||||||
|
|
||||||
struct DiskCacheTransformer<T> {
|
struct DiskCacheTransformer<T> {
|
||||||
let toData: (T) throws -> Data
|
let toData: (T) throws -> Data
|
||||||
|
@ -21,7 +22,7 @@ class DiskCache<T> {
|
||||||
let defaultExpiry: CacheExpiry
|
let defaultExpiry: CacheExpiry
|
||||||
let transformer: DiskCacheTransformer<T>
|
let transformer: DiskCacheTransformer<T>
|
||||||
|
|
||||||
private var fileStates = MultiThreadDictionary<String, FileState>()
|
private var fileStates = OSAllocatedUnfairLock(initialState: [String: FileState]())
|
||||||
|
|
||||||
init(name: String, defaultExpiry: CacheExpiry, transformer: DiskCacheTransformer<T>, fileManager: FileManager = .default) throws {
|
init(name: String, defaultExpiry: CacheExpiry, transformer: DiskCacheTransformer<T>, fileManager: FileManager = .default) throws {
|
||||||
self.defaultExpiry = defaultExpiry
|
self.defaultExpiry = defaultExpiry
|
||||||
|
@ -59,7 +60,9 @@ class DiskCache<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fileState(forKey key: String) -> FileState {
|
private func fileState(forKey key: String) -> FileState {
|
||||||
return fileStates[key] ?? .unknown
|
return fileStates.withLock {
|
||||||
|
$0[key] ?? .unknown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setObject(_ object: T, forKey key: String) throws {
|
func setObject(_ object: T, forKey key: String) throws {
|
||||||
|
@ -68,13 +71,17 @@ class DiskCache<T> {
|
||||||
guard fileManager.createFile(atPath: path, contents: data, attributes: [.modificationDate: defaultExpiry.date]) else {
|
guard fileManager.createFile(atPath: path, contents: data, attributes: [.modificationDate: defaultExpiry.date]) else {
|
||||||
throw Error.couldNotCreateFile
|
throw Error.couldNotCreateFile
|
||||||
}
|
}
|
||||||
fileStates[key] = .exists
|
fileStates.withLock {
|
||||||
|
$0[key] = .exists
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeObject(forKey key: String) throws {
|
func removeObject(forKey key: String) throws {
|
||||||
let path = makeFilePath(for: key)
|
let path = makeFilePath(for: key)
|
||||||
try fileManager.removeItem(atPath: path)
|
try fileManager.removeItem(atPath: path)
|
||||||
fileStates[key] = .doesNotExist
|
fileStates.withLock {
|
||||||
|
$0[key] = .doesNotExist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func existsObject(forKey key: String) throws -> Bool {
|
func existsObject(forKey key: String) throws -> Bool {
|
||||||
|
@ -105,7 +112,9 @@ class DiskCache<T> {
|
||||||
}
|
}
|
||||||
guard date.timeIntervalSinceNow >= 0 else {
|
guard date.timeIntervalSinceNow >= 0 else {
|
||||||
try fileManager.removeItem(atPath: path)
|
try fileManager.removeItem(atPath: path)
|
||||||
fileStates[key] = .doesNotExist
|
fileStates.withLock {
|
||||||
|
$0[key] = .doesNotExist
|
||||||
|
}
|
||||||
throw Error.expired
|
throw Error.expired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,17 +76,10 @@ func fromTimelineKind(_ kind: String) -> Timeline {
|
||||||
} else if kind == "direct" {
|
} else if kind == "direct" {
|
||||||
return .direct
|
return .direct
|
||||||
} else if kind.starts(with: "hashtag:") {
|
} else if kind.starts(with: "hashtag:") {
|
||||||
return .tag(hashtag: String(trimmingPrefix("hashtag:", of: kind)))
|
return .tag(hashtag: String(kind.trimmingPrefix("hashtag:")))
|
||||||
} else if kind.starts(with: "list:") {
|
} else if kind.starts(with: "list:") {
|
||||||
return .list(id: String(trimmingPrefix("list:", of: kind)))
|
return .list(id: String(kind.trimmingPrefix("list:")))
|
||||||
} else {
|
} else {
|
||||||
fatalError("invalid timeline kind \(kind)")
|
fatalError("invalid timeline kind \(kind)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace with Collection.trimmingPrefix
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@available(visionOS 1.0, *)
|
|
||||||
private func trimmingPrefix(_ prefix: String, of str: String) -> Substring {
|
|
||||||
return str[str.index(str.startIndex, offsetBy: prefix.count)...]
|
|
||||||
}
|
|
||||||
|
|
|
@ -36,19 +36,12 @@ private struct AppGroupedListBackground: ViewModifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if #available(iOS 16.0, *) {
|
if colorScheme == .dark, !pureBlackDarkMode {
|
||||||
if colorScheme == .dark, !pureBlackDarkMode {
|
content
|
||||||
content
|
.scrollContentBackground(.hidden)
|
||||||
.scrollContentBackground(.hidden)
|
.background(Color.appGroupedBackground.edgesIgnoringSafeArea(.all))
|
||||||
.background(Color.appGroupedBackground.edgesIgnoringSafeArea(.all))
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
content
|
content
|
||||||
.onAppear {
|
|
||||||
UITableView.appearance(whenContainedInInstancesOf: [container]).backgroundColor = .appGroupedBackground
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,14 +48,13 @@ extension HTMLConverter {
|
||||||
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
||||||
// serializing the WebURL as a string and then having Foundation parse it again),
|
// serializing the WebURL as a string and then having Foundation parse it again),
|
||||||
// so, if available, use the system parser which doesn't require another round trip.
|
// so, if available, use the system parser which doesn't require another round trip.
|
||||||
if #available(iOS 16.0, macOS 13.0, *),
|
if let url = try? URL.ParseStrategy().parse(string) {
|
||||||
let url = try? URL.ParseStrategy().parse(string) {
|
|
||||||
url
|
url
|
||||||
} else if let web = WebURL(string),
|
} else if let web = WebURL(string),
|
||||||
let url = URL(web) {
|
let url = URL(web) {
|
||||||
url
|
url
|
||||||
} else {
|
} else {
|
||||||
URL(string: string)
|
nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
//
|
|
||||||
// MultiThreadDictionary.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/6/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import os
|
|
||||||
|
|
||||||
// once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]>
|
|
||||||
// to make the lock semantics more clear
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@available(visionOS 1.0, *)
|
|
||||||
final class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
|
|
||||||
#if os(visionOS)
|
|
||||||
private let lock = OSAllocatedUnfairLock(initialState: [Key: Value]())
|
|
||||||
#else
|
|
||||||
private let lock: any Lock<[Key: Value]>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
init() {
|
|
||||||
#if !os(visionOS)
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.lock = OSAllocatedUnfairLock(initialState: [:])
|
|
||||||
} else {
|
|
||||||
self.lock = UnfairLock(initialState: [:])
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
subscript(key: Key) -> Value? {
|
|
||||||
get {
|
|
||||||
return lock.withLock { dict in
|
|
||||||
dict[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
#if os(visionOS)
|
|
||||||
lock.withLock { dict in
|
|
||||||
dict[key] = value
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
_ = lock.withLock { dict in
|
|
||||||
dict[key] = value
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread.
|
|
||||||
func removeValue(forKey key: Key) -> Value? {
|
|
||||||
return lock.withLock { dict in
|
|
||||||
dict.removeValue(forKey: key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(key: Key) -> Bool {
|
|
||||||
return lock.withLock { dict in
|
|
||||||
dict.keys.contains(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this should really be throws/rethrows but the stupid type-erased lock makes that not possible
|
|
||||||
func withLock<R>(_ body: @Sendable (inout [Key: Value]) throws -> R) rethrows -> R where R: Sendable {
|
|
||||||
return try lock.withLock { dict in
|
|
||||||
return try body(&dict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
// TODO: replace this only with OSAllocatedUnfairLock
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
fileprivate protocol Lock<State> {
|
|
||||||
associatedtype State
|
|
||||||
func withLock<R>(_ body: @Sendable (inout State) throws -> R) rethrows -> R where R: Sendable
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
extension OSAllocatedUnfairLock: Lock {
|
|
||||||
}
|
|
||||||
|
|
||||||
// from http://www.russbishop.net/the-law
|
|
||||||
fileprivate class UnfairLock<State>: Lock {
|
|
||||||
private var lock: UnsafeMutablePointer<os_unfair_lock>
|
|
||||||
private var state: State
|
|
||||||
init(initialState: State) {
|
|
||||||
self.state = initialState
|
|
||||||
self.lock = .allocate(capacity: 1)
|
|
||||||
self.lock.initialize(to: os_unfair_lock())
|
|
||||||
}
|
|
||||||
deinit {
|
|
||||||
self.lock.deinitialize(count: 1)
|
|
||||||
self.lock.deallocate()
|
|
||||||
}
|
|
||||||
func withLock<R>(_ body: (inout State) throws -> R) rethrows -> R where R: Sendable {
|
|
||||||
os_unfair_lock_lock(lock)
|
|
||||||
defer { os_unfair_lock_unlock(lock) }
|
|
||||||
return try body(&state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -274,8 +274,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
} else {
|
} else {
|
||||||
mainVC = MainSplitViewController(mastodonController: mastodonController)
|
mainVC = MainSplitViewController(mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone,
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
#available(iOS 16.0, *) {
|
|
||||||
// TODO: maybe the duckable container should be outside the account switching container
|
// TODO: maybe the duckable container should be outside the account switching container
|
||||||
return DuckableContainerViewController(child: mainVC)
|
return DuckableContainerViewController(child: mainVC)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -86,7 +86,7 @@ struct AddReactionView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationViewStyle(.stack)
|
.navigationViewStyle(.stack)
|
||||||
.mediumPresentationDetentIfAvailable()
|
.presentationDetents([.medium, .large])
|
||||||
.alertWithData("Error Adding Reaction", data: $error, actions: { _ in
|
.alertWithData("Error Adding Reaction", data: $error, actions: { _ in
|
||||||
Button("OK") {}
|
Button("OK") {}
|
||||||
}, message: { error in
|
}, message: { error in
|
||||||
|
@ -171,17 +171,6 @@ private struct AddReactionButton<Label: View>: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
private extension View {
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@available(visionOS 1.0, *)
|
|
||||||
@ViewBuilder
|
|
||||||
func mediumPresentationDetentIfAvailable() -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.presentationDetents([.medium, .large])
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 17.1)
|
@available(iOS, obsoleted: 17.1)
|
||||||
@available(visionOS 1.0, *)
|
@available(visionOS 1.0, *)
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -20,14 +20,10 @@ struct AnnouncementListRow: View {
|
||||||
@State private var isShowingAddReactionSheet = false
|
@State private var isShowingAddReactionSheet = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
mostOfTheBody
|
||||||
mostOfTheBody
|
.alignmentGuide(.listRowSeparatorLeading, computeValue: { dimension in
|
||||||
.alignmentGuide(.listRowSeparatorLeading, computeValue: { dimension in
|
dimension[.leading]
|
||||||
dimension[.leading]
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
mostOfTheBody
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mostOfTheBody: some View {
|
private var mostOfTheBody: some View {
|
||||||
|
@ -54,11 +50,7 @@ struct AnnouncementListRow: View {
|
||||||
Label {
|
Label {
|
||||||
Text("Add Reaction")
|
Text("Add Reaction")
|
||||||
} icon: {
|
} icon: {
|
||||||
if #available(iOS 16.0, *) {
|
Image("face.smiling.badge.plus")
|
||||||
Image("face.smiling.badge.plus")
|
|
||||||
} else {
|
|
||||||
Image(systemName: "face.smiling")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
|
|
|
@ -9,20 +9,6 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
struct AddHashtagPinnedTimelineRepresentable: UIViewControllerRepresentable {
|
|
||||||
typealias UIViewControllerType = UIHostingController<AddHashtagPinnedTimelineView>
|
|
||||||
|
|
||||||
@Binding var pinnedTimelines: [PinnedTimeline]
|
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UIHostingController<AddHashtagPinnedTimelineView> {
|
|
||||||
return UIHostingController(rootView: AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines))
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: UIHostingController<AddHashtagPinnedTimelineView>, context: Context) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AddHashtagPinnedTimelineView: View {
|
struct AddHashtagPinnedTimelineView: View {
|
||||||
@EnvironmentObject private var mastodonController: MastodonController
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@ -49,9 +35,6 @@ struct AddHashtagPinnedTimelineView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
list
|
list
|
||||||
#if !os(visionOS)
|
|
||||||
.appGroupedListBackground(container: AddHashtagPinnedTimelineRepresentable.UIViewControllerType.self)
|
|
||||||
#endif
|
|
||||||
.listStyle(.grouped)
|
.listStyle(.grouped)
|
||||||
.navigationTitle("Add Hashtag")
|
.navigationTitle("Add Hashtag")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|
|
@ -36,15 +36,8 @@ struct CustomizeTimelinesList: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
NavigationStack {
|
||||||
NavigationStack {
|
navigationBody
|
||||||
navigationBody
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NavigationView {
|
|
||||||
navigationBody
|
|
||||||
}
|
|
||||||
.navigationViewStyle(.stack)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,7 +149,7 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.self)
|
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.self)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
.scrollDismissesKeyboard(.interactively)
|
||||||
#endif
|
#endif
|
||||||
.navigationTitle(create ? "Add Filter" : "Edit Filter")
|
.navigationTitle(create ? "Add Filter" : "Edit Filter")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
@ -226,18 +226,6 @@ private struct FilterContextToggleStyle: ToggleStyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.scrollDismissesKeyboard(.interactively)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//struct EditFilterView_Previews: PreviewProvider {
|
//struct EditFilterView_Previews: PreviewProvider {
|
||||||
// static var previews: some View {
|
// static var previews: some View {
|
||||||
// EditFilterView()
|
// EditFilterView()
|
||||||
|
|
|
@ -115,18 +115,8 @@ struct PinnedTimelinesModifier: ViewModifier {
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
|
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
|
||||||
#if os(visionOS)
|
|
||||||
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
#else
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
|
||||||
} else {
|
|
||||||
AddHashtagPinnedTimelineRepresentable(pinnedTimelines: $pinnedTimelines)
|
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
})
|
})
|
||||||
.sheet(isPresented: $isShowingAddInstanceSheet, content: {
|
.sheet(isPresented: $isShowingAddInstanceSheet, content: {
|
||||||
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
||||||
|
|
|
@ -41,9 +41,7 @@ class InlineTrendsViewController: UIViewController {
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
navigationItem.hidesSearchBarWhenScrolling = false
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
if #available(iOS 16.0, *) {
|
navigationItem.preferredSearchBarPlacement = .stacked
|
||||||
navigationItem.preferredSearchBarPlacement = .stacked
|
|
||||||
}
|
|
||||||
|
|
||||||
let trends = TrendsViewController(mastodonController: mastodonController)
|
let trends = TrendsViewController(mastodonController: mastodonController)
|
||||||
trends.view.translatesAutoresizingMaskIntoConstraints = false
|
trends.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
|
@ -525,12 +525,12 @@ extension TrendsViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
@available(visionOS 1.0, *)
|
guard indexPaths.count == 1,
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
let item = dataSource.itemIdentifier(for: indexPaths[0]) else {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
let indexPath = indexPaths[0]
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
||||||
|
@ -584,15 +584,6 @@ extension TrendsViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// implementing the highlightPreviewForItemAt method seems to prevent the old, single IndexPath variant of this method from being called on iOS 16
|
|
||||||
@available(iOS 16.0, visionOS 1.0, *)
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
guard indexPaths.count == 1 else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return self.collectionView(collectionView, contextMenuConfigurationForItemAt: indexPaths[0], point: point)
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||||
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,6 @@ class VideoControlsViewController: UIViewController {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let player: AVPlayer
|
private let player: AVPlayer
|
||||||
#if !os(visionOS)
|
|
||||||
@Box private var playbackSpeed: Float
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private lazy var muteButton = MuteButton().configure {
|
private lazy var muteButton = MuteButton().configure {
|
||||||
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
||||||
|
@ -46,13 +43,8 @@ class VideoControlsViewController: UIViewController {
|
||||||
|
|
||||||
private lazy var optionsButton = MenuButton { [unowned self] in
|
private lazy var optionsButton = MenuButton { [unowned self] in
|
||||||
let imageName: String
|
let imageName: String
|
||||||
#if os(visionOS)
|
|
||||||
let playbackSpeed = player.defaultRate
|
|
||||||
#else
|
|
||||||
let playbackSpeed = self.playbackSpeed
|
|
||||||
#endif
|
|
||||||
if #available(iOS 17.0, *) {
|
if #available(iOS 17.0, *) {
|
||||||
switch playbackSpeed {
|
switch player.defaultRate {
|
||||||
case 0.5:
|
case 0.5:
|
||||||
imageName = "gauge.with.dots.needle.0percent"
|
imageName = "gauge.with.dots.needle.0percent"
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -68,12 +60,8 @@ class VideoControlsViewController: UIViewController {
|
||||||
imageName = "speedometer"
|
imageName = "speedometer"
|
||||||
}
|
}
|
||||||
let speedMenu = UIMenu(title: "Playback Speed", image: UIImage(systemName: imageName), children: PlaybackSpeed.allCases.map { speed in
|
let speedMenu = UIMenu(title: "Playback Speed", image: UIImage(systemName: imageName), children: PlaybackSpeed.allCases.map { speed in
|
||||||
UIAction(title: speed.displayName, state: playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in
|
UIAction(title: speed.displayName, state: self.player.defaultRate == speed.rate ? .on : .off) { [unowned self] _ in
|
||||||
#if os(visionOS)
|
|
||||||
self.player.defaultRate = speed.rate
|
self.player.defaultRate = speed.rate
|
||||||
#else
|
|
||||||
self.playbackSpeed = speed.rate
|
|
||||||
#endif
|
|
||||||
if self.player.rate > 0 {
|
if self.player.rate > 0 {
|
||||||
self.player.rate = speed.rate
|
self.player.rate = speed.rate
|
||||||
}
|
}
|
||||||
|
@ -101,20 +89,11 @@ class VideoControlsViewController: UIViewController {
|
||||||
private var scrubbingTargetTime: CMTime?
|
private var scrubbingTargetTime: CMTime?
|
||||||
private var isSeeking = false
|
private var isSeeking = false
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
init(player: AVPlayer) {
|
init(player: AVPlayer) {
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
|
||||||
self.player = player
|
|
||||||
self._playbackSpeed = playbackSpeed
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
@ -198,11 +177,7 @@ class VideoControlsViewController: UIViewController {
|
||||||
@objc private func scrubbingEnded() {
|
@objc private func scrubbingEnded() {
|
||||||
scrubbingChanged()
|
scrubbingChanged()
|
||||||
if wasPlayingWhenScrubbingStarted {
|
if wasPlayingWhenScrubbingStarted {
|
||||||
#if os(visionOS)
|
|
||||||
player.play()
|
player.play()
|
||||||
#else
|
|
||||||
player.rate = playbackSpeed
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,6 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
private var item: AVPlayerItem
|
private var item: AVPlayerItem
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
|
||||||
@Box private var playbackSpeed: Float = 1
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private var isGrayscale: Bool
|
private var isGrayscale: Bool
|
||||||
|
|
||||||
private var presentationSizeObservation: NSKeyValueObservation?
|
private var presentationSizeObservation: NSKeyValueObservation?
|
||||||
|
@ -161,11 +156,7 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
player.replaceCurrentItem(with: item)
|
player.replaceCurrentItem(with: item)
|
||||||
updateItemObservations()
|
updateItemObservations()
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
#if os(visionOS)
|
|
||||||
player.play()
|
player.play()
|
||||||
#else
|
|
||||||
player.rate = playbackSpeed
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,20 +187,12 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
[VideoActivityItemSource(asset: item.asset, url: url)]
|
[VideoActivityItemSource(asset: item.asset, url: url)]
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
private lazy var overlayVC = VideoOverlayViewController(player: player)
|
private lazy var overlayVC = VideoOverlayViewController(player: player)
|
||||||
#else
|
|
||||||
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
|
||||||
#endif
|
|
||||||
var contentOverlayAccessoryViewController: UIViewController? {
|
var contentOverlayAccessoryViewController: UIViewController? {
|
||||||
overlayVC
|
overlayVC
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
|
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
|
||||||
#else
|
|
||||||
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
overlayVC.setVisible(visible)
|
overlayVC.setVisible(visible)
|
||||||
|
|
|
@ -15,9 +15,6 @@ class VideoOverlayViewController: UIViewController {
|
||||||
private static let pauseImage = UIImage(systemName: "pause.fill")!
|
private static let pauseImage = UIImage(systemName: "pause.fill")!
|
||||||
|
|
||||||
private let player: AVPlayer
|
private let player: AVPlayer
|
||||||
#if !os(visionOS)
|
|
||||||
@Box private var playbackSpeed: Float
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private var dimmingView: UIView!
|
private var dimmingView: UIView!
|
||||||
private var controlsStack: UIStackView!
|
private var controlsStack: UIStackView!
|
||||||
|
@ -26,18 +23,10 @@ class VideoOverlayViewController: UIViewController {
|
||||||
|
|
||||||
private var rateObservation: NSKeyValueObservation?
|
private var rateObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
init(player: AVPlayer) {
|
init(player: AVPlayer) {
|
||||||
self.player = player
|
self.player = player
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
|
||||||
self.player = player
|
|
||||||
self._playbackSpeed = playbackSpeed
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
@ -109,11 +98,7 @@ class VideoOverlayViewController: UIViewController {
|
||||||
if player.currentTime() >= player.currentItem!.duration {
|
if player.currentTime() >= player.currentItem!.duration {
|
||||||
player.seek(to: .zero)
|
player.seek(to: .zero)
|
||||||
}
|
}
|
||||||
#if os(visionOS)
|
|
||||||
player.play()
|
player.play()
|
||||||
#else
|
|
||||||
player.rate = playbackSpeed
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,28 +100,13 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
navigationItem.hidesSearchBarWhenScrolling = false
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
if #available(iOS 16.0, *) {
|
navigationItem.preferredSearchBarPlacement = .stacked
|
||||||
navigationItem.preferredSearchBarPlacement = .stacked
|
|
||||||
|
|
||||||
navigationItem.renameDelegate = self
|
navigationItem.renameDelegate = self
|
||||||
navigationItem.titleMenuProvider = { [unowned self] suggested in
|
navigationItem.titleMenuProvider = { [unowned self] suggested in
|
||||||
var children = suggested
|
var children = suggested
|
||||||
children.append(contentsOf: self.listSettingsMenuElements())
|
children.append(contentsOf: self.listSettingsMenuElements())
|
||||||
return UIMenu(children: children)
|
return UIMenu(children: children)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Edit", menu: UIMenu(children: [
|
|
||||||
// uncached so that menu always reflects the current state of the list
|
|
||||||
UIDeferredMenuElement.uncached({ [unowned self] elementHandler in
|
|
||||||
var elements = self.listSettingsMenuElements()
|
|
||||||
elements.insert(UIAction(title: "Rename…", image: UIImage(systemName: "pencil"), handler: { [unowned self] _ in
|
|
||||||
RenameListService(list: self.list, mastodonController: self.mastodonController, present: {
|
|
||||||
self.present($0, animated: true)
|
|
||||||
}).run()
|
|
||||||
}), at: 0)
|
|
||||||
elementHandler(elements)
|
|
||||||
})
|
|
||||||
]))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -454,15 +454,20 @@ extension NewMainTabBarViewController {
|
||||||
extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
||||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
|
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
|
||||||
if tab.identifier == Tab.compose.rawValue {
|
if tab.identifier == Tab.compose.rawValue {
|
||||||
let currentTab = selectedTab
|
if #unavailable(iOS 18.1) {
|
||||||
// returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254)
|
let currentTab = selectedTab
|
||||||
// returning false and then setting selectedTab=tab and selectedTab=currentTab seems to leave things in a bad state (currentTab's VC is on screen but in the disappeared state)
|
// returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254)
|
||||||
// so return true, and then after the tab bar VC has finished updating, go back to currentTab
|
// returning false and then setting selectedTab=tab and selectedTab=currentTab seems to leave things in a bad state (currentTab's VC is on screen but in the disappeared state)
|
||||||
DispatchQueue.main.async {
|
// so return true, and then after the tab bar VC has finished updating, go back to currentTab
|
||||||
self.selectedTab = currentTab
|
DispatchQueue.main.async {
|
||||||
|
self.selectedTab = currentTab
|
||||||
|
}
|
||||||
|
compose(editing: nil)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
compose(editing: nil)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
compose(editing: nil)
|
|
||||||
return true
|
|
||||||
} else if let selectedTab,
|
} else if let selectedTab,
|
||||||
selectedTab == tab,
|
selectedTab == tab,
|
||||||
let nav = selectedViewController as? any NavigationControllerProtocol {
|
let nav = selectedViewController as? any NavigationControllerProtocol {
|
||||||
|
|
|
@ -41,15 +41,8 @@ struct MuteAccountView: View {
|
||||||
@State private var error: Error?
|
@State private var error: Error?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
NavigationStack {
|
||||||
NavigationStack {
|
navigationViewContent
|
||||||
navigationViewContent
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NavigationView {
|
|
||||||
navigationViewContent
|
|
||||||
}
|
|
||||||
.navigationViewStyle(.stack)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -101,14 +101,8 @@ extension FollowRequestNotificationViewController: UICollectionViewDelegate {
|
||||||
UIAction(title: "Accept", image: UIImage(systemName: "checkmark.circle"), handler: { _ in cell.acceptButtonPressed() }),
|
UIAction(title: "Accept", image: UIImage(systemName: "checkmark.circle"), handler: { _ in cell.acceptButtonPressed() }),
|
||||||
UIAction(title: "Reject", image: UIImage(systemName: "xmark.circle"), handler: { _ in cell.rejectButtonPressed() }),
|
UIAction(title: "Reject", image: UIImage(systemName: "xmark.circle"), handler: { _ in cell.rejectButtonPressed() }),
|
||||||
]
|
]
|
||||||
let acceptRejectMenu: UIMenu
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
acceptRejectMenu = UIMenu(options: .displayInline, preferredElementSize: .medium, children: acceptRejectChildren)
|
|
||||||
} else {
|
|
||||||
acceptRejectMenu = UIMenu(options: .displayInline, children: acceptRejectChildren)
|
|
||||||
}
|
|
||||||
return UIMenu(children: [
|
return UIMenu(children: [
|
||||||
acceptRejectMenu,
|
UIMenu(options: .displayInline, preferredElementSize: .medium, children: acceptRejectChildren),
|
||||||
UIMenu(options: .displayInline, children: self.actionsForProfile(accountID: accountID, source: .view(cell))),
|
UIMenu(options: .displayInline, children: self.actionsForProfile(accountID: accountID, source: .view(cell))),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
|
@ -700,14 +700,8 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
||||||
UIAction(title: "Accept", image: UIImage(systemName: "checkmark.circle"), handler: { _ in cell.acceptButtonPressed() }),
|
UIAction(title: "Accept", image: UIImage(systemName: "checkmark.circle"), handler: { _ in cell.acceptButtonPressed() }),
|
||||||
UIAction(title: "Reject", image: UIImage(systemName: "xmark.circle"), handler: { _ in cell.rejectButtonPressed() }),
|
UIAction(title: "Reject", image: UIImage(systemName: "xmark.circle"), handler: { _ in cell.rejectButtonPressed() }),
|
||||||
]
|
]
|
||||||
let acceptRejectMenu: UIMenu
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
acceptRejectMenu = UIMenu(options: .displayInline, preferredElementSize: .medium, children: acceptRejectChildren)
|
|
||||||
} else {
|
|
||||||
acceptRejectMenu = UIMenu(options: .displayInline, children: acceptRejectChildren)
|
|
||||||
}
|
|
||||||
return UIMenu(children: [
|
return UIMenu(children: [
|
||||||
acceptRejectMenu,
|
UIMenu(options: .displayInline, preferredElementSize: .medium, children: acceptRejectChildren),
|
||||||
UIMenu(options: .displayInline, children: self.actionsForProfile(accountID: accountID, source: .view(cell))),
|
UIMenu(options: .displayInline, children: self.actionsForProfile(accountID: accountID, source: .view(cell))),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,9 +96,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
searchController.searchBar.placeholder = "Search or enter a URL"
|
searchController.searchBar.placeholder = "Search or enter a URL"
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
navigationItem.hidesSearchBarWhenScrolling = false
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
if #available(iOS 16.0, *) {
|
navigationItem.preferredSearchBarPlacement = .stacked
|
||||||
navigationItem.preferredSearchBarPlacement = .stacked
|
|
||||||
}
|
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
urlHandler = urlCheckerSubject
|
urlHandler = urlCheckerSubject
|
||||||
|
|
|
@ -91,14 +91,10 @@ struct AboutView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var iconOrGame: some View {
|
private var iconOrGame: some View {
|
||||||
if #available(iOS 16.0, *) {
|
FlipView {
|
||||||
FlipView {
|
|
||||||
appIcon
|
|
||||||
} back: {
|
|
||||||
TTTView()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appIcon
|
appIcon
|
||||||
|
} back: {
|
||||||
|
TTTView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,14 +27,7 @@ struct AppearancePrefsView: View {
|
||||||
private static let accentColorsAndImages: [(AccentColor, UIImage?)] = AccentColor.allCases.map { color in
|
private static let accentColorsAndImages: [(AccentColor, UIImage?)] = AccentColor.allCases.map { color in
|
||||||
var image: UIImage?
|
var image: UIImage?
|
||||||
if let color = color.color {
|
if let color = color.color {
|
||||||
if #available(iOS 16.0, *) {
|
image = UIImage(systemName: "circle.fill")!.withTintColor(color, renderingMode: .alwaysTemplate).withRenderingMode(.alwaysOriginal)
|
||||||
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)
|
return (color, image)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,12 +36,7 @@ struct NotificationsPrefsView: View {
|
||||||
if #available(iOS 15.4, *) {
|
if #available(iOS 15.4, *) {
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
let str = if #available(iOS 16.0, *) {
|
if let url = URL(string: UIApplication.openNotificationSettingsURLString) {
|
||||||
UIApplication.openNotificationSettingsURLString
|
|
||||||
} else {
|
|
||||||
UIApplicationOpenNotificationSettingsURLString
|
|
||||||
}
|
|
||||||
if let url = URL(string: str) {
|
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -34,7 +34,7 @@ struct PushInstanceSettingsView: View {
|
||||||
HStack {
|
HStack {
|
||||||
PrefsAccountView(account: account)
|
PrefsAccountView(account: account)
|
||||||
Spacer()
|
Spacer()
|
||||||
AsyncToggle("\(account.instanceURL.host!) notifications enabled", labelHidden: true, mode: $mode, onChange: updateNotificationsEnabled(enabled:))
|
AsyncToggle("\(account.instanceURL.host!) notifications enabled", mode: $mode, onChange: updateNotificationsEnabled(enabled:))
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
PushSubscriptionView(account: account, mastodonController: mastodonController, subscription: subscription, updateSubscription: updateSubscription)
|
PushSubscriptionView(account: account, mastodonController: mastodonController, subscription: subscription, updateSubscription: updateSubscription)
|
||||||
|
|
|
@ -43,21 +43,9 @@ struct OppositeCollapseKeywordsView: View {
|
||||||
.listStyle(.grouped)
|
.listStyle(.grouped)
|
||||||
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
}
|
}
|
||||||
#if !os(visionOS)
|
|
||||||
.onAppear(perform: updateAppearance)
|
|
||||||
#endif
|
|
||||||
.navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords")
|
.navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords")
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private func updateAppearance() {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
// no longer necessary
|
|
||||||
} else {
|
|
||||||
UIScrollView.appearance(whenContainedInInstancesOf: [PreferencesNavigationController.self]).keyboardDismissMode = .interactive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func commitExisting(at index: Int) -> () -> Void {
|
private func commitExisting(at index: Int) -> () -> Void {
|
||||||
return {
|
return {
|
||||||
if keywords[index].value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
if keywords[index].value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
|
|
@ -69,34 +69,30 @@ private struct ScrollBackgroundModifier: ViewModifier {
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if #available(iOS 16.0, *) {
|
content
|
||||||
content
|
.scrollContentBackground(.hidden)
|
||||||
.scrollContentBackground(.hidden)
|
.background {
|
||||||
.background {
|
// otherwise the pureBlackDarkMode isn't propagated, for some reason?
|
||||||
// otherwise the pureBlackDarkMode isn't propagated, for some reason?
|
// even though it is for ReportSelectRulesView??
|
||||||
// even though it is for ReportSelectRulesView??
|
let traits: UITraitCollection = {
|
||||||
let traits: UITraitCollection = {
|
var t = UITraitCollection(userInterfaceStyle: colorScheme == .dark ? .dark : .light)
|
||||||
var t = UITraitCollection(userInterfaceStyle: colorScheme == .dark ? .dark : .light)
|
#if os(visionOS)
|
||||||
#if os(visionOS)
|
t = t.modifyingTraits({ mutableTraits in
|
||||||
|
mutableTraits.pureBlackDarkMode = true
|
||||||
|
})
|
||||||
|
#else
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
t = t.modifyingTraits({ mutableTraits in
|
t = t.modifyingTraits({ mutableTraits in
|
||||||
mutableTraits.pureBlackDarkMode = true
|
mutableTraits.pureBlackDarkMode = true
|
||||||
})
|
})
|
||||||
#else
|
} else {
|
||||||
if #available(iOS 17.0, *) {
|
t.obsoletePureBlackDarkMode = true
|
||||||
t = t.modifyingTraits({ mutableTraits in
|
}
|
||||||
mutableTraits.pureBlackDarkMode = true
|
#endif
|
||||||
})
|
return t
|
||||||
} else {
|
}()
|
||||||
t.obsoletePureBlackDarkMode = true
|
Color(uiColor: .appGroupedBackground.resolvedColor(with: traits))
|
||||||
}
|
.edgesIgnoringSafeArea(.all)
|
||||||
#endif
|
}
|
||||||
return t
|
|
||||||
}()
|
|
||||||
Color(uiColor: .appGroupedBackground.resolvedColor(with: traits))
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,26 +49,12 @@ struct ReportSelectRulesView: View {
|
||||||
}
|
}
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
.withAppBackgroundIfAvailable()
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(Color.appGroupedBackground)
|
||||||
.navigationTitle("Rules")
|
.navigationTitle("Rules")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@available(visionOS 1.0, *)
|
|
||||||
@ViewBuilder
|
|
||||||
func withAppBackgroundIfAvailable() -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
.background(Color.appGroupedBackground)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//struct ReportSelectRulesView_Previews: PreviewProvider {
|
//struct ReportSelectRulesView_Previews: PreviewProvider {
|
||||||
// static var previews: some View {
|
// static var previews: some View {
|
||||||
// ReportSelectRulesView()
|
// ReportSelectRulesView()
|
||||||
|
|
|
@ -27,18 +27,11 @@ struct ReportView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
NavigationStack {
|
||||||
NavigationStack {
|
navigationViewContent
|
||||||
navigationViewContent
|
#if !os(visionOS)
|
||||||
#if !os(visionOS)
|
.scrollDismissesKeyboard(.interactively)
|
||||||
.scrollDismissesKeyboard(.interactively)
|
#endif
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NavigationView {
|
|
||||||
navigationViewContent
|
|
||||||
}
|
|
||||||
.navigationViewStyle(.stack)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,9 +39,7 @@ class MastodonSearchController: UISearchController {
|
||||||
searchResultsUpdater = searchResultsController
|
searchResultsUpdater = searchResultsController
|
||||||
automaticallyShowsSearchResultsController = false
|
automaticallyShowsSearchResultsController = false
|
||||||
showsSearchResultsController = true
|
showsSearchResultsController = true
|
||||||
if #available(iOS 16.0, *) {
|
scopeBarActivation = .onSearchActivation
|
||||||
scopeBarActivation = .onSearchActivation
|
|
||||||
}
|
|
||||||
|
|
||||||
searchBar.autocapitalizationType = .none
|
searchBar.autocapitalizationType = .none
|
||||||
searchBar.delegate = self
|
searchBar.delegate = self
|
||||||
|
@ -78,12 +76,8 @@ class MastodonSearchController: UISearchController {
|
||||||
if searchText != defaultLanguage,
|
if searchText != defaultLanguage,
|
||||||
let match = languageRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.utf16.count)) {
|
let match = languageRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.utf16.count)) {
|
||||||
let identifier = (searchText as NSString).substring(with: match.range(at: 1))
|
let identifier = (searchText as NSString).substring(with: match.range(at: 1))
|
||||||
if #available(iOS 16.0, *) {
|
if Locale.LanguageCode.isoLanguageCodes.contains(where: { $0.identifier == identifier }) {
|
||||||
if Locale.LanguageCode.isoLanguageCodes.contains(where: { $0.identifier == identifier }) {
|
langSuggestions.append("language:\(identifier)")
|
||||||
langSuggestions.append("language:\(identifier)")
|
|
||||||
}
|
|
||||||
} else if searchText != "en" {
|
|
||||||
langSuggestions.append("language:\(searchText)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
suggestions.append((.language, langSuggestions))
|
suggestions.append((.language, langSuggestions))
|
||||||
|
|
|
@ -22,8 +22,7 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
override var viewControllers: [UIViewController] {
|
override var viewControllers: [UIViewController] {
|
||||||
didSet {
|
didSet {
|
||||||
poppedViewControllers = []
|
poppedViewControllers = []
|
||||||
if #available(iOS 16.0, *),
|
if useBrowserStyleNavigation {
|
||||||
useBrowserStyleNavigation {
|
|
||||||
// TODO: this for loop might not be necessary
|
// TODO: this for loop might not be necessary
|
||||||
for vc in viewControllers {
|
for vc in viewControllers {
|
||||||
configureNavItem(vc.navigationItem)
|
configureNavItem(vc.navigationItem)
|
||||||
|
@ -40,8 +39,7 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
self.interactivePushTransition = InteractivePushTransition(navigationController: self)
|
self.interactivePushTransition = InteractivePushTransition(navigationController: self)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if #available(iOS 16.0, *),
|
if useBrowserStyleNavigation,
|
||||||
useBrowserStyleNavigation,
|
|
||||||
let topViewController {
|
let topViewController {
|
||||||
configureNavItem(topViewController.navigationItem)
|
configureNavItem(topViewController.navigationItem)
|
||||||
updateTopNavItemState()
|
updateTopNavItemState()
|
||||||
|
@ -52,9 +50,7 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
let popped = performAfterAnimating(block: {
|
let popped = performAfterAnimating(block: {
|
||||||
super.popViewController(animated: animated)
|
super.popViewController(animated: animated)
|
||||||
}, after: {
|
}, after: {
|
||||||
if #available(iOS 16.0, *) {
|
self.updateTopNavItemState()
|
||||||
self.updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}, animated: animated)
|
}, animated: animated)
|
||||||
if let popped {
|
if let popped {
|
||||||
poppedViewControllers.insert(popped, at: 0)
|
poppedViewControllers.insert(popped, at: 0)
|
||||||
|
@ -66,9 +62,7 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
let popped = performAfterAnimating(block: {
|
let popped = performAfterAnimating(block: {
|
||||||
super.popToRootViewController(animated: animated)
|
super.popToRootViewController(animated: animated)
|
||||||
}, after: {
|
}, after: {
|
||||||
if #available(iOS 16.0, *) {
|
self.updateTopNavItemState()
|
||||||
self.updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}, animated: animated)
|
}, animated: animated)
|
||||||
if let popped {
|
if let popped {
|
||||||
poppedViewControllers = popped
|
poppedViewControllers = popped
|
||||||
|
@ -80,9 +74,7 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
let popped = performAfterAnimating(block: {
|
let popped = performAfterAnimating(block: {
|
||||||
super.popToViewController(viewController, animated: animated)
|
super.popToViewController(viewController, animated: animated)
|
||||||
}, after: {
|
}, after: {
|
||||||
if #available(iOS 16.0, *) {
|
self.updateTopNavItemState()
|
||||||
self.updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}, animated: animated)
|
}, animated: animated)
|
||||||
if let popped {
|
if let popped {
|
||||||
poppedViewControllers.insert(contentsOf: popped, at: 0)
|
poppedViewControllers.insert(contentsOf: popped, at: 0)
|
||||||
|
@ -97,15 +89,11 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
self.poppedViewControllers = []
|
self.poppedViewControllers = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
configureNavItem(viewController.navigationItem)
|
||||||
configureNavItem(viewController.navigationItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.pushViewController(viewController, animated: animated)
|
super.pushViewController(viewController, animated: animated)
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
updateTopNavItemState()
|
||||||
updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pushPoppedViewController() {
|
func pushPoppedViewController() {
|
||||||
|
@ -135,9 +123,7 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
pushViewController(target, animated: true)
|
pushViewController(target, animated: true)
|
||||||
}, after: {
|
}, after: {
|
||||||
self.viewControllers.insert(contentsOf: toInsert, at: self.viewControllers.count - 1)
|
self.viewControllers.insert(contentsOf: toInsert, at: self.viewControllers.count - 1)
|
||||||
if #available(iOS 16.0, *) {
|
self.updateTopNavItemState()
|
||||||
self.updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}, animated: true)
|
}, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -204,8 +204,7 @@ extension MenuActionProvider {
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
if #available(iOS 16.0, *),
|
if includeStatusButtonActions {
|
||||||
includeStatusButtonActions {
|
|
||||||
let favorited = status.favourited
|
let favorited = status.favourited
|
||||||
// TODO: move this color into an asset catalog or something
|
// TODO: move this color into an asset catalog or something
|
||||||
var favImage = UIImage(systemName: favorited ? "star.fill" : "star")!
|
var favImage = UIImage(systemName: favorited ? "star.fill" : "star")!
|
||||||
|
@ -368,19 +367,11 @@ extension MenuActionProvider {
|
||||||
|
|
||||||
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
let toggleableAndActions = toggleableSection + actionsSection
|
||||||
let toggleableAndActions = toggleableSection + actionsSection
|
return [
|
||||||
return [
|
UIMenu(options: .displayInline, preferredElementSize: toggleableAndActions.count == 1 ? .large : .medium, children: toggleableAndActions),
|
||||||
UIMenu(options: .displayInline, preferredElementSize: toggleableAndActions.count == 1 ? .large : .medium, children: toggleableAndActions),
|
UIMenu(options: .displayInline, children: shareSection),
|
||||||
UIMenu(options: .displayInline, children: shareSection),
|
]
|
||||||
]
|
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
UIMenu(options: .displayInline, children: shareSection),
|
|
||||||
UIMenu(options: .displayInline, children: toggleableSection),
|
|
||||||
UIMenu(options: .displayInline, children: actionsSection),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] {
|
func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] {
|
||||||
|
|
|
@ -108,8 +108,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft) {
|
func compose(editing draft: Draft) {
|
||||||
if #available(iOS 16.0, *),
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
UIDevice.current.userInterfaceIdiom == .phone {
|
|
||||||
self.root.compose(editing: draft, animated: false, isDucked: true, completion: nil)
|
self.root.compose(editing: draft, animated: false, isDucked: true, completion: nil)
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
@ -123,8 +122,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
||||||
precondition(state > .initial)
|
precondition(state > .initial)
|
||||||
navigation.run()
|
navigation.run()
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
if #available(iOS 16.0, *),
|
if let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {
|
||||||
let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {
|
|
||||||
self.root.compose(editing: duckedDraft, animated: false, isDucked: true, completion: nil)
|
self.root.compose(editing: duckedDraft, animated: false, isDucked: true, completion: nil)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -114,8 +114,7 @@ extension TuskerNavigationDelegate {
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
fatalError("unreachable")
|
fatalError("unreachable")
|
||||||
#else
|
#else
|
||||||
if #available(iOS 16.0, *),
|
if presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) {
|
||||||
presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) {
|
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
present(compose, animated: animated, completion: completion)
|
present(compose, animated: animated, completion: completion)
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
|
import os
|
||||||
|
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ struct AccountDisplayNameView: View {
|
||||||
guard !matches.isEmpty else { return }
|
guard !matches.isEmpty else { return }
|
||||||
|
|
||||||
let emojiSize = self.emojiSize
|
let emojiSize = self.emojiSize
|
||||||
let emojiImages = MultiThreadDictionary<String, Image>()
|
let emojiImages = OSAllocatedUnfairLock(initialState: [String: Image]())
|
||||||
|
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
|
@ -63,7 +64,9 @@ struct AccountDisplayNameView: View {
|
||||||
image.draw(in: CGRect(origin: .zero, size: size))
|
image.draw(in: CGRect(origin: .zero, size: size))
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiImages[emoji.shortcode] = Image(uiImage: resized)
|
emojiImages.withLock {
|
||||||
|
$0[emoji.shortcode] = Image(uiImage: resized)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let request = request {
|
if let request = request {
|
||||||
emojiRequests.append(request)
|
emojiRequests.append(request)
|
||||||
|
@ -78,7 +81,7 @@ struct AccountDisplayNameView: View {
|
||||||
// iterate backwards as to not alter the indices of earlier matches
|
// iterate backwards as to not alter the indices of earlier matches
|
||||||
for match in matches.reversed() {
|
for match in matches.reversed() {
|
||||||
let shortcode = (account.displayName as NSString).substring(with: match.range(at: 1))
|
let shortcode = (account.displayName as NSString).substring(with: match.range(at: 1))
|
||||||
guard let image = emojiImages[shortcode] else { continue }
|
guard let image = emojiImages.withLock({ $0[shortcode] }) else { continue }
|
||||||
|
|
||||||
let afterCurrentMatch = (account.displayName as NSString).substring(with: NSRange(location: match.range.upperBound, length: endIndex - match.range.upperBound))
|
let afterCurrentMatch = (account.displayName as NSString).substring(with: NSRange(location: match.range.upperBound, length: endIndex - match.range.upperBound))
|
||||||
|
|
||||||
|
|
|
@ -263,17 +263,7 @@ class AttachmentView: GIFImageView {
|
||||||
let asset = AVURLAsset(url: attachment.url)
|
let asset = AVURLAsset(url: attachment.url)
|
||||||
let generator = AVAssetImageGenerator(asset: asset)
|
let generator = AVAssetImageGenerator(asset: asset)
|
||||||
generator.appliesPreferredTrackTransform = true
|
generator.appliesPreferredTrackTransform = true
|
||||||
let image: CGImage?
|
guard let image = try? await generator.image(at: .zero).image,
|
||||||
#if os(visionOS)
|
|
||||||
image = try? await generator.image(at: .zero).image
|
|
||||||
#else
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
image = try? await generator.image(at: .zero).image
|
|
||||||
} else {
|
|
||||||
image = try? generator.copyCGImage(at: .zero, actualTime: nil)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
guard let image,
|
|
||||||
let prepared = await UIImage(cgImage: image).byPreparingForDisplay(),
|
let prepared = await UIImage(cgImage: image).byPreparingForDisplay(),
|
||||||
!Task.isCancelled else {
|
!Task.isCancelled else {
|
||||||
return
|
return
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
|
import os
|
||||||
|
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
|
||||||
|
@ -56,7 +57,7 @@ extension BaseEmojiLabel {
|
||||||
return imageSizeMatchingFontSize
|
return imageSizeMatchingFontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
let emojiImages = MultiThreadDictionary<String, UIImage>()
|
let emojiImages = OSAllocatedUnfairLock(initialState: [String: UIImage]())
|
||||||
var foundEmojis = false
|
var foundEmojis = false
|
||||||
|
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
@ -79,9 +80,11 @@ extension BaseEmojiLabel {
|
||||||
// todo: consider caching these somewhere? the cache needs to take into account both the url and the font size, which may vary across labels, so can't just use ImageCache
|
// todo: consider caching these somewhere? the cache needs to take into account both the url and the font size, which may vary across labels, so can't just use ImageCache
|
||||||
if let thumbnail = image.preparingThumbnail(of: emojiImageSize(image)),
|
if let thumbnail = image.preparingThumbnail(of: emojiImageSize(image)),
|
||||||
let cgImage = thumbnail.cgImage {
|
let cgImage = thumbnail.cgImage {
|
||||||
// the thumbnail API takes a pixel size and returns an image with scale 1, but we want the actual screen scale, so convert
|
emojiImages.withLock {
|
||||||
// see FB12187798
|
// the thumbnail API takes a pixel size and returns an image with scale 1, but we want the actual screen scale, so convert
|
||||||
emojiImages[emoji.shortcode] = UIImage(cgImage: cgImage, scale: screenScale, orientation: .up)
|
// see FB12187798
|
||||||
|
$0[emoji.shortcode] = UIImage(cgImage: cgImage, scale: screenScale, orientation: .up)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise, perform the network request
|
// otherwise, perform the network request
|
||||||
|
@ -99,7 +102,9 @@ extension BaseEmojiLabel {
|
||||||
group.leave()
|
group.leave()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emojiImages[emoji.shortcode] = transformedImage
|
emojiImages.withLock {
|
||||||
|
$0[emoji.shortcode] = transformedImage
|
||||||
|
}
|
||||||
group.leave()
|
group.leave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,8 +146,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
|
|
||||||
func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? {
|
func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? {
|
||||||
let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)
|
let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)
|
||||||
if #available(iOS 16.0, *),
|
if let textLayoutManager {
|
||||||
let textLayoutManager {
|
|
||||||
guard let fragment = textLayoutManager.textLayoutFragment(for: locationInTextContainer) else {
|
guard let fragment = textLayoutManager.textLayoutFragment(for: locationInTextContainer) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -305,8 +304,7 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
|
||||||
|
|
||||||
// Determine the line rects that the link takes up in the coordinate space of this view.
|
// Determine the line rects that the link takes up in the coordinate space of this view.
|
||||||
var rects = [CGRect]()
|
var rects = [CGRect]()
|
||||||
if #available(iOS 16.0, *),
|
if let textLayoutManager,
|
||||||
let textLayoutManager,
|
|
||||||
let contentManager = textLayoutManager.textContentManager {
|
let contentManager = textLayoutManager.textContentManager {
|
||||||
// convert from NSRange to NSTextRange
|
// convert from NSRange to NSTextRange
|
||||||
// i have no idea under what circumstances any of these calls could fail
|
// i have no idea under what circumstances any of these calls could fail
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// CopyableLable.swift
|
// CopyableLabel.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 2/4/23.
|
// Created by Shadowfacts on 2/4/23.
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class CopyableLable: UILabel {
|
class CopyableLabel: UILabel {
|
||||||
|
|
||||||
private var _editMenuInteraction: Any!
|
private var _editMenuInteraction: Any!
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
|
@ -28,12 +28,10 @@ class CopyableLable: UILabel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func commonInit() {
|
private func commonInit() {
|
||||||
if #available(iOS 16.0, *) {
|
editMenuInteraction = UIEditMenuInteraction(delegate: nil)
|
||||||
editMenuInteraction = UIEditMenuInteraction(delegate: nil)
|
addInteraction(editMenuInteraction)
|
||||||
addInteraction(editMenuInteraction)
|
isUserInteractionEnabled = true
|
||||||
isUserInteractionEnabled = true
|
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressed)))
|
||||||
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressed)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func copy(_ sender: Any?) {
|
override func copy(_ sender: Any?) {
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
@ -125,16 +124,16 @@
|
||||||
</constraints>
|
</constraints>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="jwU-EH-hmC">
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="jwU-EH-hmC">
|
||||||
<rect key="frame" x="144" y="235" width="103.5" height="23"/>
|
<rect key="frame" x="144" y="235" width="101.5" height="23"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL" customClass="CopyableLable" customModule="Tusker" customModuleProvider="target">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL" customClass="CopyableLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="2.5" width="81" height="18"/>
|
<rect key="frame" x="0.0" y="2.5" width="81" height="18"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" image="lock.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KNY-GD-beC">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" image="lock.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KNY-GD-beC">
|
||||||
<rect key="frame" x="85" y="1.5" width="18.5" height="19.5"/>
|
<rect key="frame" x="85" y="1.5" width="16.5" height="19.5"/>
|
||||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="light"/>
|
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="light"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
|
@ -187,14 +186,14 @@
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
<image name="ellipsis" catalog="system" width="32" height="32"/>
|
||||||
<image name="lock.fill" catalog="system" width="125" height="128"/>
|
<image name="lock.fill" catalog="system" width="32" height="32"/>
|
||||||
<image name="person.badge.plus" catalog="system" width="128" height="124"/>
|
<image name="person.badge.plus" catalog="system" width="32" height="32"/>
|
||||||
<systemColor name="labelColor">
|
<systemColor name="labelColor">
|
||||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<systemColor name="secondaryLabelColor">
|
<systemColor name="secondaryLabelColor">
|
||||||
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<systemColor name="systemBackgroundColor">
|
<systemColor name="systemBackgroundColor">
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
|
|
@ -143,10 +143,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
$0.isEditable = false
|
$0.isEditable = false
|
||||||
$0.isSelectable = true
|
$0.isSelectable = true
|
||||||
$0.emojiFont = ConversationMainStatusCollectionViewCell.contentFont
|
$0.emojiFont = ConversationMainStatusCollectionViewCell.contentFont
|
||||||
$0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
|
$0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber, .money, .physicalValue]
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
$0.dataDetectorTypes.formUnion([.money, .physicalValue])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var translateButton: TranslateButton?
|
private var translateButton: TranslateButton?
|
||||||
|
|
Loading…
Reference in New Issue