Compare commits
33 Commits
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 5ce9892a9b | |
Shadowfacts | 54376ac585 | |
Shadowfacts | 26c483fc9a | |
Shadowfacts | a68d2ce952 | |
Shadowfacts | adaf8dc217 | |
Shadowfacts | 572c5a0824 | |
Shadowfacts | e469d207b4 | |
Shadowfacts | 82ec120871 | |
Shadowfacts | 242c60d74d | |
Shadowfacts | 20692b0630 | |
Shadowfacts | 9990d50e3e | |
Shadowfacts | 670047af6f | |
Shadowfacts | e8a492ef7d | |
Shadowfacts | 583d9b97dd | |
Shadowfacts | 88176fe599 | |
Shadowfacts | 19c3008c8f | |
Shadowfacts | 51f9f421b8 | |
Shadowfacts | b700e17d7e | |
Shadowfacts | dc01804359 | |
Shadowfacts | a5066140fd | |
Shadowfacts | 351efe4b58 | |
Shadowfacts | c716f03784 | |
Shadowfacts | fa828a5eae | |
Shadowfacts | 56d12295ba | |
Shadowfacts | a442197adf | |
Shadowfacts | a99fb7f0b0 | |
Shadowfacts | f44dae632c | |
Shadowfacts | 0dcb67c44e | |
Shadowfacts | e869fdc38f | |
Shadowfacts | 5c86feccb9 | |
Shadowfacts | 01cf597b5d | |
Shadowfacts | 12bab71b17 | |
Shadowfacts | f4b51c06c1 |
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -1,5 +1,28 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2024.5 (139)
|
||||||
|
Bugfixes:
|
||||||
|
- Fix error decoding certain posts
|
||||||
|
|
||||||
|
## 2024.5 (138)
|
||||||
|
Bugfixes:
|
||||||
|
- Fix potential crash when displaying certain attachments
|
||||||
|
- Fix potential crash due to race condition when opening push notification in app
|
||||||
|
- Fix misaligned text between profile field values/labels
|
||||||
|
- Fix rate limited error message not including reset timestamp
|
||||||
|
- iPadOS/macOS: Fix Cmd+R shortcut not working
|
||||||
|
|
||||||
|
## 2024.5 (137)
|
||||||
|
Features/Improvements:
|
||||||
|
- Improve gallery presentation/dismissal transitions
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Account for bidirectional text in display names
|
||||||
|
- Fix crash when playing back gifv
|
||||||
|
- Fix gallery controls not hiding if video loading fails
|
||||||
|
- iPadOS: Fix incorrect gallery dismiss animation on non-fullscreen windows
|
||||||
|
- iPadOS: Fix hang when switching accounts
|
||||||
|
|
||||||
## 2024.4 (136)
|
## 2024.4 (136)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Import image description when adding attachments from Photos if possible
|
- Import image description when adding attachments from Photos if possible
|
||||||
|
|
|
@ -14,7 +14,6 @@ import OSLog
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Intents
|
import Intents
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
import WebURL
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import TuskerPreferences
|
import TuskerPreferences
|
||||||
|
|
||||||
|
@ -238,8 +237,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
for match in emojiRegex.matches(in: content.body, range: NSRange(location: 0, length: content.body.utf16.count)).reversed() {
|
for match in emojiRegex.matches(in: content.body, range: NSRange(location: 0, length: content.body.utf16.count)).reversed() {
|
||||||
let emojiName = (content.body as NSString).substring(with: match.range(at: 1))
|
let emojiName = (content.body as NSString).substring(with: match.range(at: 1))
|
||||||
guard let emoji = notification.status?.emojis.first(where: { $0.shortcode == emojiName }),
|
guard let emoji = notification.status?.emojis.first(where: { $0.shortcode == emojiName }),
|
||||||
let url = URL(emoji.url),
|
let (data, _) = try? await URLSession.shared.data(from: emoji.url),
|
||||||
let (data, _) = try? await URLSession.shared.data(from: url),
|
|
||||||
let image = UIImage(data: data) else {
|
let image = UIImage(data: data) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -368,18 +366,7 @@ private func decodeBase64URL(_ s: String) -> Data? {
|
||||||
// copied from HTMLConverter.Callbacks, blergh
|
// copied from HTMLConverter.Callbacks, blergh
|
||||||
private struct HTMLCallbacks: HTMLConversionCallbacks {
|
private struct HTMLCallbacks: HTMLConversionCallbacks {
|
||||||
static func makeURL(string: String) -> URL? {
|
static func makeURL(string: String) -> URL? {
|
||||||
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
try? URL.ParseStrategy().parse(string)
|
||||||
// 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.
|
|
||||||
if #available(iOS 16.0, macOS 13.0, *),
|
|
||||||
let url = try? URL.ParseStrategy().parse(string) {
|
|
||||||
url
|
|
||||||
} else if let web = WebURL(string),
|
|
||||||
let url = URL(web) {
|
|
||||||
url
|
|
||||||
} else {
|
|
||||||
URL(string: string)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -26,9 +26,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"]),
|
dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "ComposeUITests",
|
name: "ComposeUITests",
|
||||||
dependencies: ["ComposeUI"]),
|
dependencies: ["ComposeUI"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,10 +322,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -436,7 +430,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)
|
||||||
}
|
}
|
||||||
|
@ -487,31 +481,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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
class KeyboardReader: ObservableObject {
|
class KeyboardReader: ObservableObject {
|
||||||
// @Published var isVisible = false
|
// @Published var isVisible = false
|
||||||
@Published var keyboardHeight: CGFloat = 0
|
@Published var keyboardHeight: CGFloat = 0
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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.
|
||||||
|
@ -14,13 +14,23 @@ let package = Package(
|
||||||
name: "GalleryVC",
|
name: "GalleryVC",
|
||||||
targets: ["GalleryVC"]),
|
targets: ["GalleryVC"]),
|
||||||
],
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(path: "../TuskerComponents"),
|
||||||
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// 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",
|
||||||
|
dependencies: ["TuskerComponents"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "GalleryVCTests",
|
name: "GalleryVCTests",
|
||||||
dependencies: ["GalleryVC"]),
|
dependencies: ["GalleryVC"],
|
||||||
|
swiftSettings: [
|
||||||
|
.swiftLanguageMode(.v5)
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
//
|
//
|
||||||
// FallbackGalleryContentViewController.swift
|
// FallbackGalleryContentViewController.swift
|
||||||
// Tusker
|
// GalleryVC
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/18/24.
|
// Created by Shadowfacts on 3/18/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import GalleryVC
|
|
||||||
import QuickLook
|
import QuickLook
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
private class FallbackGalleryContentViewController: QLPreviewController {
|
private class FallbackGalleryContentViewController: QLPreviewController {
|
||||||
private let previewItem = GalleryPreviewItem()
|
private let previewItem = GalleryPreviewItem()
|
||||||
|
@ -52,40 +50,40 @@ extension FallbackGalleryContentViewController: QLPreviewControllerDataSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FallbackGalleryNavigationController: UINavigationController, GalleryContentViewController {
|
public class FallbackGalleryNavigationController: UINavigationController, GalleryContentViewController {
|
||||||
init(url: URL) {
|
public init(url: URL) {
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
self.viewControllers = [FallbackGalleryContentViewController(url: url)]
|
self.viewControllers = [FallbackGalleryContentViewController(url: url)]
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
public override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
container?.disableGalleryScrollAndZoom()
|
container?.disableGalleryScrollAndZoom()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
public required init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
public weak var container: (any GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
var contentSize: CGSize {
|
public var contentSize: CGSize {
|
||||||
.zero
|
.zero
|
||||||
}
|
}
|
||||||
|
|
||||||
var activityItemsForSharing: [Any] {
|
public var activityItemsForSharing: [Any] {
|
||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
|
|
||||||
var caption: String? {
|
public var caption: String? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var canAnimateFromSourceView: Bool {
|
public var presentationAnimation: GalleryContentPresentationAnimation {
|
||||||
false
|
.fade
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
//
|
//
|
||||||
// ImageGalleryContentViewController.swift
|
// ImageGalleryContentViewController.swift
|
||||||
// Tusker
|
// GalleryVC
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/17/24.
|
// Created by Shadowfacts on 3/17/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import GalleryVC
|
|
||||||
import Pachyderm
|
|
||||||
import TuskerComponents
|
import TuskerComponents
|
||||||
@preconcurrency import VisionKit
|
@preconcurrency import VisionKit
|
||||||
|
|
||||||
class ImageGalleryContentViewController: UIViewController, GalleryContentViewController {
|
open class ImageGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
let url: URL
|
public let caption: String?
|
||||||
let caption: String?
|
public var image: UIImage {
|
||||||
let originalData: Data?
|
didSet {
|
||||||
let image: UIImage
|
imageView?.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
let gifController: GIFController?
|
let gifController: GIFController?
|
||||||
|
|
||||||
private var imageView: GIFImageView!
|
private var imageView: GIFImageView!
|
||||||
|
@ -27,12 +27,8 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
@available(iOS 16.0, macCatalyst 17.0, *)
|
@available(iOS 16.0, macCatalyst 17.0, *)
|
||||||
private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction }
|
private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction }
|
||||||
|
|
||||||
private var isGrayscale = false
|
public init(image: UIImage, caption: String?, gifController: GIFController?) {
|
||||||
|
|
||||||
init(url: URL, caption: String?, originalData: Data?, image: UIImage, gifController: GIFController?) {
|
|
||||||
self.url = url
|
|
||||||
self.caption = caption
|
self.caption = caption
|
||||||
self.originalData = originalData
|
|
||||||
self.image = image
|
self.image = image
|
||||||
self.gifController = gifController
|
self.gifController = gifController
|
||||||
|
|
||||||
|
@ -41,21 +37,14 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
preferredContentSize = image.size
|
preferredContentSize = image.size
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
public override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
imageView = GIFImageView(image: image)
|
||||||
let maybeGrayscaleImage = if isGrayscale {
|
|
||||||
ImageGrayscalifier.convert(url: url, image: image) ?? image
|
|
||||||
} else {
|
|
||||||
image
|
|
||||||
}
|
|
||||||
|
|
||||||
imageView = GIFImageView(image: maybeGrayscaleImage)
|
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
imageView.isUserInteractionEnabled = true
|
imageView.isUserInteractionEnabled = true
|
||||||
|
@ -86,11 +75,9 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
public override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
if let gifController {
|
if let gifController {
|
||||||
|
@ -98,37 +85,23 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
let image = if isGrayscale {
|
|
||||||
ImageGrayscalifier.convert(url: url, image: image)
|
|
||||||
} else {
|
|
||||||
image
|
|
||||||
}
|
|
||||||
if let image {
|
|
||||||
imageView.image = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
public weak var container: (any GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
var contentSize: CGSize {
|
public var contentSize: CGSize {
|
||||||
image.size
|
image.size
|
||||||
}
|
}
|
||||||
|
|
||||||
var activityItemsForSharing: [Any] {
|
open var activityItemsForSharing: [Any] {
|
||||||
if let data = originalData ?? image.pngData() {
|
return [image]
|
||||||
return [ImageActivityItemSource(data: data, url: url, image: image)]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
public var presentationAnimation: GalleryContentPresentationAnimation {
|
||||||
|
gifController != nil ? .fromSourceViewWithoutSnapshot : .fromSourceView
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
if #available(iOS 16.0, macCatalyst 17.0, *),
|
if #available(iOS 16.0, macCatalyst 17.0, *),
|
||||||
let analysisInteraction {
|
let analysisInteraction {
|
||||||
analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated)
|
analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated)
|
||||||
|
@ -138,7 +111,7 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
|
|
||||||
@available(iOS 16.0, macCatalyst 17.0, *)
|
@available(iOS 16.0, macCatalyst 17.0, *)
|
||||||
extension ImageGalleryContentViewController: ImageAnalysisInteractionDelegate {
|
extension ImageGalleryContentViewController: ImageAnalysisInteractionDelegate {
|
||||||
func interaction(_ interaction: ImageAnalysisInteraction, shouldBeginAt point: CGPoint, for interactionType: ImageAnalysisInteraction.InteractionTypes) -> Bool {
|
public func interaction(_ interaction: ImageAnalysisInteraction, shouldBeginAt point: CGPoint, for interactionType: ImageAnalysisInteraction.InteractionTypes) -> Bool {
|
||||||
return container?.galleryControlsVisible ?? true
|
return container?.galleryControlsVisible ?? true
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,43 +7,42 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import GalleryVC
|
|
||||||
|
|
||||||
class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController {
|
public class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
private let fallbackCaption: String?
|
private let fallbackCaption: String?
|
||||||
private let provider: () async -> (any GalleryContentViewController)?
|
private let provider: () async -> (any GalleryContentViewController)?
|
||||||
private var wrapped: (any GalleryContentViewController)!
|
private var wrapped: (any GalleryContentViewController)!
|
||||||
|
|
||||||
weak var container: GalleryContentViewControllerContainer?
|
public weak var container: GalleryContentViewControllerContainer?
|
||||||
|
|
||||||
var contentSize: CGSize {
|
public var contentSize: CGSize {
|
||||||
wrapped?.contentSize ?? .zero
|
wrapped?.contentSize ?? .zero
|
||||||
}
|
}
|
||||||
|
|
||||||
var activityItemsForSharing: [Any] {
|
public var activityItemsForSharing: [Any] {
|
||||||
wrapped?.activityItemsForSharing ?? []
|
wrapped?.activityItemsForSharing ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
var caption: String? {
|
public var caption: String? {
|
||||||
wrapped?.caption ?? fallbackCaption
|
wrapped?.caption ?? fallbackCaption
|
||||||
}
|
}
|
||||||
|
|
||||||
var canAnimateFromSourceView: Bool {
|
public var presentationAnimation: GalleryContentPresentationAnimation {
|
||||||
wrapped?.canAnimateFromSourceView ?? true
|
wrapped?.presentationAnimation ?? .fade
|
||||||
}
|
}
|
||||||
|
|
||||||
init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) {
|
public init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) {
|
||||||
self.fallbackCaption = caption
|
self.fallbackCaption = caption
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
public override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
container?.setGalleryContentLoading(true)
|
container?.setGalleryContentLoading(true)
|
||||||
|
@ -81,7 +80,7 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
|
||||||
|
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "Error Loading"
|
label.text = "Error Loading"
|
||||||
label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold)!, size: 0)
|
||||||
label.textColor = .secondaryLabel
|
label.textColor = .secondaryLabel
|
||||||
label.adjustsFontForContentSizeCategory = true
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
|
@ -102,15 +101,15 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
|
wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryContentDidAppear() {
|
public func galleryContentDidAppear() {
|
||||||
wrapped?.galleryContentDidAppear()
|
wrapped?.galleryContentDidAppear()
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryContentWillDisappear() {
|
public func galleryContentWillDisappear() {
|
||||||
wrapped?.galleryContentWillDisappear()
|
wrapped?.galleryContentWillDisappear()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// VideoControlsViewController.swift
|
// VideoControlsViewController.swift
|
||||||
// Tusker
|
// GalleryVC
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/21/24.
|
// Created by Shadowfacts on 3/21/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
@ -18,41 +18,41 @@ 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 = {
|
||||||
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
let button = MuteButton()
|
||||||
$0.setMuted(false, animated: false)
|
button.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
||||||
}
|
button.setMuted(false, animated: false)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
private let timestampLabel = UILabel().configure {
|
private let timestampLabel: UILabel = {
|
||||||
$0.text = "0:00"
|
let label = UILabel()
|
||||||
$0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
label.text = "0:00"
|
||||||
}
|
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
private lazy var scrubbingControl = VideoScrubbingControl().configure {
|
private lazy var scrubbingControl: VideoScrubbingControl = {
|
||||||
$0.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
let control = VideoScrubbingControl()
|
||||||
$0.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin)
|
control.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
||||||
$0.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged)
|
control.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin)
|
||||||
$0.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd)
|
control.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged)
|
||||||
}
|
control.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd)
|
||||||
|
return control
|
||||||
|
}()
|
||||||
|
|
||||||
private let timeRemainingLabel = UILabel().configure {
|
private let timeRemainingLabel: UILabel = {
|
||||||
$0.text = "-0:00"
|
let label = UILabel()
|
||||||
$0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
label.text = "-0:00"
|
||||||
}
|
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
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 +68,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
|
||||||
}
|
}
|
||||||
|
@ -82,17 +78,19 @@ class VideoControlsViewController: UIViewController {
|
||||||
return UIMenu(children: [speedMenu])
|
return UIMenu(children: [speedMenu])
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var hStack = UIStackView(arrangedSubviews: [
|
private lazy var hStack: UIStackView = {
|
||||||
muteButton,
|
let stack = UIStackView(arrangedSubviews: [
|
||||||
timestampLabel,
|
muteButton,
|
||||||
scrubbingControl,
|
timestampLabel,
|
||||||
timeRemainingLabel,
|
scrubbingControl,
|
||||||
optionsButton,
|
timeRemainingLabel,
|
||||||
]).configure {
|
optionsButton,
|
||||||
$0.axis = .horizontal
|
])
|
||||||
$0.spacing = 8
|
stack.axis = .horizontal
|
||||||
$0.alignment = .center
|
stack.spacing = 8
|
||||||
}
|
stack.alignment = .center
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
private var timestampObserverToken: Any?
|
private var timestampObserverToken: Any?
|
||||||
private var scrubberObserverToken: Any?
|
private var scrubberObserverToken: Any?
|
||||||
|
@ -101,20 +99,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 +187,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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,73 +1,53 @@
|
||||||
//
|
//
|
||||||
// VideoGalleryContentViewController.swift
|
// VideoGalleryContentViewController.swift
|
||||||
// Tusker
|
// GalleryVC
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/19/24.
|
// Created by Shadowfacts on 3/19/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import GalleryVC
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import CoreImage
|
import CoreImage
|
||||||
|
|
||||||
class VideoGalleryContentViewController: UIViewController, GalleryContentViewController {
|
open class VideoGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
private let url: URL
|
public let url: URL
|
||||||
let caption: String?
|
public let caption: String?
|
||||||
private var item: AVPlayerItem
|
public private(set) var item: AVPlayerItem
|
||||||
let player: AVPlayer
|
public 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 presentationSizeObservation: NSKeyValueObservation?
|
private var presentationSizeObservation: NSKeyValueObservation?
|
||||||
private var statusObservation: NSKeyValueObservation?
|
private var statusObservation: NSKeyValueObservation?
|
||||||
private var rateObservation: NSKeyValueObservation?
|
private var rateObservation: NSKeyValueObservation?
|
||||||
private var isFirstAppearance = true
|
|
||||||
private var hideControlsWorkItem: DispatchWorkItem?
|
private var hideControlsWorkItem: DispatchWorkItem?
|
||||||
private var audioSessionToken: AudioSessionCoordinator.Token?
|
private var isShowingError = false
|
||||||
|
|
||||||
init(url: URL, caption: String?) {
|
public init(url: URL, caption: String?) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.caption = caption
|
self.caption = caption
|
||||||
|
|
||||||
self.isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
|
|
||||||
let asset = AVAsset(url: url)
|
let asset = AVAsset(url: url)
|
||||||
self.item = VideoGalleryContentViewController.createItem(asset: asset)
|
self.item = Self.createItem(asset: asset)
|
||||||
self.player = AVPlayer(playerItem: item)
|
self.player = AVPlayer(playerItem: item)
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func createItem(asset: AVAsset) -> AVPlayerItem {
|
open class func createItem(asset: AVAsset) -> AVPlayerItem {
|
||||||
let item = AVPlayerItem(asset: asset)
|
return AVPlayerItem(asset: asset)
|
||||||
if Preferences.shared.grayscaleImages {
|
|
||||||
#if os(visionOS)
|
|
||||||
#warning("Use async AVVideoComposition CIFilter initializer")
|
|
||||||
#else
|
|
||||||
let filter = CIFilter(name: "CIColorMonochrome")!
|
|
||||||
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
|
|
||||||
filter.setValue(1.0, forKey: "inputIntensity")
|
|
||||||
|
|
||||||
item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
|
|
||||||
filter.setValue(request.sourceImage, forKey: "inputImage")
|
|
||||||
request.finish(with: filter.outputImage!, context: nil)
|
|
||||||
})
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
public func replaceCurrentItem(with item: AVPlayerItem) {
|
||||||
|
self.item = item
|
||||||
|
player.replaceCurrentItem(with: item)
|
||||||
|
updateItemObservations()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
container?.setGalleryContentLoading(true)
|
container?.setGalleryContentLoading(true)
|
||||||
|
@ -92,19 +72,17 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
scheduleControlsHide()
|
scheduleControlsHide()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateItemObservations() {
|
private func updateItemObservations() {
|
||||||
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
|
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
|
||||||
MainActor.runUnsafely {
|
MainActor.assumeIsolated {
|
||||||
self.preferredContentSize = item.presentationSize
|
self.preferredContentSize = item.presentationSize
|
||||||
self.container?.galleryContentChanged()
|
self.container?.galleryContentChanged()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
statusObservation = item.observe(\.status, changeHandler: { [unowned self] item, _ in
|
statusObservation = item.observe(\.status, changeHandler: { [unowned self] item, _ in
|
||||||
MainActor.runUnsafely {
|
MainActor.assumeIsolated {
|
||||||
if item.status == .readyToPlay {
|
if item.status == .readyToPlay {
|
||||||
self.container?.setGalleryContentLoading(false)
|
self.container?.setGalleryContentLoading(false)
|
||||||
self.statusObservation = nil
|
self.statusObservation = nil
|
||||||
|
@ -113,19 +91,22 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
self.container?.setGalleryContentLoading(false)
|
self.container?.setGalleryContentLoading(false)
|
||||||
self.showErrorView(error)
|
self.showErrorView(error)
|
||||||
self.statusObservation = nil
|
self.statusObservation = nil
|
||||||
|
self.overlayVC.setVisible(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showErrorView(_ error: any Error) {
|
private func showErrorView(_ error: any Error) {
|
||||||
|
isShowingError = true
|
||||||
|
|
||||||
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
|
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
|
||||||
image.tintColor = .secondaryLabel
|
image.tintColor = .secondaryLabel
|
||||||
image.contentMode = .scaleAspectFit
|
image.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "Error Loading"
|
label.text = "Error Loading"
|
||||||
label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold)!, size: 0)
|
||||||
label.textColor = .secondaryLabel
|
label.textColor = .secondaryLabel
|
||||||
label.adjustsFontForContentSizeCategory = true
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
|
@ -153,26 +134,9 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
|
||||||
let isPlaying = player.rate > 0
|
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
item = VideoGalleryContentViewController.createItem(asset: item.asset)
|
|
||||||
player.replaceCurrentItem(with: item)
|
|
||||||
updateItemObservations()
|
|
||||||
if isPlaying {
|
|
||||||
#if os(visionOS)
|
|
||||||
player.play()
|
|
||||||
#else
|
|
||||||
player.rate = playbackSpeed
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scheduleControlsHide() {
|
private func scheduleControlsHide() {
|
||||||
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
||||||
MainActor.runUnsafely {
|
MainActor.assumeIsolated {
|
||||||
guard let self,
|
guard let self,
|
||||||
let container = self.container,
|
let container = self.container,
|
||||||
container.galleryControlsVisible else {
|
container.galleryControlsVisible else {
|
||||||
|
@ -186,33 +150,32 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
public weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
var contentSize: CGSize {
|
public var contentSize: CGSize {
|
||||||
item.presentationSize
|
item.presentationSize
|
||||||
}
|
}
|
||||||
|
|
||||||
var activityItemsForSharing: [Any] {
|
open var activityItemsForSharing: [Any] {
|
||||||
[VideoActivityItemSource(asset: item.asset, url: url)]
|
// [VideoActivityItemSource(asset: item.asset, url: url)]
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
public var presentationAnimation: GalleryContentPresentationAnimation {
|
||||||
|
isShowingError ? .fade : .fromSourceViewWithoutSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
private lazy var overlayVC = VideoOverlayViewController(player: player)
|
private lazy var overlayVC = VideoOverlayViewController(player: player)
|
||||||
#else
|
public var contentOverlayAccessoryViewController: UIViewController? {
|
||||||
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
|
||||||
#endif
|
|
||||||
var contentOverlayAccessoryViewController: UIViewController? {
|
|
||||||
overlayVC
|
overlayVC
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(visionOS)
|
public 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) {
|
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
overlayVC.setVisible(visible)
|
if !isShowingError {
|
||||||
|
overlayVC.setVisible(visible)
|
||||||
|
}
|
||||||
|
|
||||||
if !visible {
|
if !visible {
|
||||||
hideControlsWorkItem?.cancel()
|
hideControlsWorkItem?.cancel()
|
||||||
|
@ -222,25 +185,11 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryContentDidAppear() {
|
open func galleryContentDidAppear() {
|
||||||
let wasFirstAppearance = isFirstAppearance
|
|
||||||
isFirstAppearance = false
|
|
||||||
|
|
||||||
audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .video) {
|
|
||||||
if wasFirstAppearance {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.player.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryContentWillDisappear() {
|
open func galleryContentWillDisappear() {
|
||||||
player.pause()
|
player.pause()
|
||||||
|
|
||||||
if let audioSessionToken {
|
|
||||||
AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -269,9 +218,9 @@ private class PlayerView: UIView {
|
||||||
playerLayer.player = player
|
playerLayer.player = player
|
||||||
playerLayer.videoGravity = .resizeAspect
|
playerLayer.videoGravity = .resizeAspect
|
||||||
|
|
||||||
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] _, _ in
|
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [weak self] _, _ in
|
||||||
MainActor.runUnsafely {
|
MainActor.assumeIsolated {
|
||||||
self.invalidateIntrinsicContentSize()
|
self?.invalidateIntrinsicContentSize()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// VideoOverlayViewController.swift
|
// VideoOverlayViewController.swift
|
||||||
// Tusker
|
// GalleryVC
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/26/24.
|
// Created by Shadowfacts on 3/26/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
@ -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")
|
||||||
|
@ -90,7 +79,7 @@ class VideoOverlayViewController: UIViewController {
|
||||||
])
|
])
|
||||||
|
|
||||||
rateObservation = player.observe(\.rate, changeHandler: { player, _ in
|
rateObservation = player.observe(\.rate, changeHandler: { player, _ in
|
||||||
MainActor.runUnsafely {
|
MainActor.assumeIsolated {
|
||||||
playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage
|
playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ public protocol GalleryContentViewController: UIViewController {
|
||||||
var caption: String? { get }
|
var caption: String? { get }
|
||||||
var contentOverlayAccessoryViewController: UIViewController? { get }
|
var contentOverlayAccessoryViewController: UIViewController? { get }
|
||||||
var bottomControlsAccessoryViewController: UIViewController? { get }
|
var bottomControlsAccessoryViewController: UIViewController? { get }
|
||||||
var canAnimateFromSourceView: Bool { get }
|
var presentationAnimation: GalleryContentPresentationAnimation { get }
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
|
||||||
func galleryContentDidAppear()
|
func galleryContentDidAppear()
|
||||||
|
@ -31,8 +31,8 @@ public extension GalleryContentViewController {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var canAnimateFromSourceView: Bool {
|
var presentationAnimation: GalleryContentPresentationAnimation {
|
||||||
true
|
.fromSourceView
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
|
@ -44,3 +44,9 @@ public extension GalleryContentViewController {
|
||||||
func galleryContentWillDisappear() {
|
func galleryContentWillDisappear() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum GalleryContentPresentationAnimation {
|
||||||
|
case fade
|
||||||
|
case fromSourceView
|
||||||
|
case fromSourceViewWithoutSnapshot
|
||||||
|
}
|
||||||
|
|
|
@ -30,12 +30,37 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
|
||||||
|
|
||||||
let itemViewController = from.currentItemViewController
|
let itemViewController = from.currentItemViewController
|
||||||
|
|
||||||
if !itemViewController.content.canAnimateFromSourceView || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) {
|
if itemViewController.content.presentationAnimation == .fade || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) {
|
||||||
animateCrossFadeTransition(using: transitionContext)
|
animateCrossFadeTransition(using: transitionContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let container = transitionContext.containerView
|
let container = transitionContext.containerView
|
||||||
|
|
||||||
|
// Moving `to.view` to the container is necessary when the presenting VC (i.e., `to`)
|
||||||
|
// is in the window's root presentation.
|
||||||
|
// But it breaks when the gallery is presented from a sheet-presented VC--in which case
|
||||||
|
// `to.view` is already in the view hierarchy at this point; and adding it to the
|
||||||
|
// container causees it to be removed when the transition completes.
|
||||||
|
if to.view.superview == nil {
|
||||||
|
to.view.frame = container.bounds
|
||||||
|
container.addSubview(to.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceSnapshot: UIView? = if itemViewController.content.presentationAnimation == .fromSourceViewWithoutSnapshot {
|
||||||
|
nil
|
||||||
|
} else {
|
||||||
|
sourceView.snapshotView(afterScreenUpdates: false)
|
||||||
|
}
|
||||||
|
if let sourceSnapshot {
|
||||||
|
let snapshotContainer = sourceView.ancestorForInsertingSnapshot
|
||||||
|
snapshotContainer.addSubview(sourceSnapshot)
|
||||||
|
let sourceFrameInShapshotContainer = snapshotContainer.convert(sourceView.bounds, from: sourceView)
|
||||||
|
sourceSnapshot.frame = sourceFrameInShapshotContainer
|
||||||
|
sourceSnapshot.layer.opacity = 1
|
||||||
|
self.sourceView.layer.opacity = 0
|
||||||
|
}
|
||||||
|
|
||||||
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
|
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
|
||||||
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
|
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
|
||||||
|
|
||||||
|
@ -48,38 +73,39 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
|
||||||
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
|
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
|
||||||
.scaledBy(x: scale, y: scale)
|
.scaledBy(x: scale, y: scale)
|
||||||
sourceView.transform = sourceToDestTransform
|
sourceView.transform = sourceToDestTransform
|
||||||
|
sourceSnapshot?.transform = sourceToDestTransform
|
||||||
} else {
|
} else {
|
||||||
appliedSourceToDestTransform = false
|
appliedSourceToDestTransform = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moving `to.view` to the container is necessary when the presenting VC (i.e., `to`)
|
|
||||||
// is in the window's root presentation.
|
|
||||||
// But it breaks when the gallery is presented from a sheet-presented VC--in which case
|
|
||||||
// `to.view` is already in the view hierarchy at this point; and adding it to the
|
|
||||||
// container causees it to be removed when the transition completes.
|
|
||||||
if to.view.superview == nil {
|
|
||||||
to.view.frame = container.bounds
|
|
||||||
container.addSubview(to.view)
|
|
||||||
}
|
|
||||||
|
|
||||||
from.view.frame = container.bounds
|
from.view.frame = container.bounds
|
||||||
container.addSubview(from.view)
|
container.addSubview(from.view)
|
||||||
|
|
||||||
|
let contentContainer = UIView()
|
||||||
|
contentContainer.layer.masksToBounds = true
|
||||||
|
contentContainer.frame = destFrameInContainer
|
||||||
|
container.addSubview(contentContainer)
|
||||||
|
|
||||||
let content = itemViewController.takeContent()
|
let content = itemViewController.takeContent()
|
||||||
content.view.translatesAutoresizingMaskIntoConstraints = true
|
content.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
content.view.layer.masksToBounds = true
|
content.view.transform = .identity
|
||||||
container.addSubview(content.view)
|
|
||||||
|
|
||||||
content.view.frame = destFrameInContainer
|
|
||||||
content.view.layer.opacity = 1
|
content.view.layer.opacity = 1
|
||||||
|
content.view.frame = contentContainer.bounds
|
||||||
|
contentContainer.addSubview(content.view)
|
||||||
|
|
||||||
container.layoutIfNeeded()
|
container.layoutIfNeeded()
|
||||||
|
|
||||||
|
// Hide overlaid controls immediately, to prevent the Live Text button's position
|
||||||
|
// getting caught up in the rest of the animation.
|
||||||
|
UIView.animate(withDuration: 0.1) {
|
||||||
|
content.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
||||||
|
}
|
||||||
|
|
||||||
let duration = self.transitionDuration(using: transitionContext)
|
let duration = self.transitionDuration(using: transitionContext)
|
||||||
var initialVelocity: CGVector
|
var initialVelocity: CGVector
|
||||||
if let interactiveVelocity,
|
if let interactiveVelocity,
|
||||||
let interactiveTranslation,
|
let interactiveTranslation,
|
||||||
// very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the springs initial undershoot
|
// very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the spring's initial undershoot
|
||||||
sqrt(pow(interactiveTranslation.x, 2) + pow(interactiveTranslation.y, 2)) > 100,
|
sqrt(pow(interactiveTranslation.x, 2) + pow(interactiveTranslation.y, 2)) > 100,
|
||||||
sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 {
|
sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 {
|
||||||
let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX
|
let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX
|
||||||
|
@ -102,14 +128,34 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
|
||||||
|
|
||||||
if appliedSourceToDestTransform {
|
if appliedSourceToDestTransform {
|
||||||
self.sourceView.transform = origSourceTransform
|
self.sourceView.transform = origSourceTransform
|
||||||
|
sourceSnapshot?.transform = origSourceTransform
|
||||||
}
|
}
|
||||||
content.view.frame = sourceFrameInContainer
|
|
||||||
content.view.layer.opacity = 0
|
contentContainer.frame = sourceFrameInContainer
|
||||||
|
// Using sourceSizeWithDestAspectRatioCenteredInContentContainer does not seem to be necessary here.
|
||||||
|
// I guess autoresizing takes care of it?
|
||||||
|
|
||||||
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delay fading out the content because if it's still big while it's semi-transparent,
|
||||||
|
// seeing the stuff behind it looks odd.
|
||||||
|
animator.addAnimations({
|
||||||
|
content.view.layer.opacity = 0
|
||||||
|
}, delayFactor: 0.35)
|
||||||
|
|
||||||
|
if let sourceSnapshot {
|
||||||
|
animator.addAnimations({
|
||||||
|
self.sourceView.layer.opacity = 1
|
||||||
|
sourceSnapshot.layer.opacity = 0
|
||||||
|
}, delayFactor: 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
animator.addCompletion { _ in
|
animator.addCompletion { _ in
|
||||||
|
sourceSnapshot?.removeFromSuperview()
|
||||||
|
|
||||||
|
// Having dismissed, we don't need to undo any of the changes to the content VC.
|
||||||
|
|
||||||
transitionContext.completeTransition(true)
|
transitionContext.completeTransition(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ class GalleryDismissInteraction: NSObject {
|
||||||
private(set) var dismissVelocity: CGPoint?
|
private(set) var dismissVelocity: CGPoint?
|
||||||
private(set) var dismissTranslation: CGPoint?
|
private(set) var dismissTranslation: CGPoint?
|
||||||
|
|
||||||
|
private var cancelAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
init(viewController: GalleryViewController) {
|
init(viewController: GalleryViewController) {
|
||||||
self.viewController = viewController
|
self.viewController = viewController
|
||||||
super.init()
|
super.init()
|
||||||
|
@ -38,6 +40,8 @@ class GalleryDismissInteraction: NSObject {
|
||||||
content = viewController.currentItemViewController.takeContent()
|
content = viewController.currentItemViewController.takeContent()
|
||||||
content!.view.translatesAutoresizingMaskIntoConstraints = true
|
content!.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
content!.view.frame = origContentFrameInGallery!
|
content!.view.frame = origContentFrameInGallery!
|
||||||
|
// Make sure the context remains behind the controls
|
||||||
|
content!.view.layer.zPosition = -1000
|
||||||
viewController.view.addSubview(content!.view)
|
viewController.view.addSubview(content!.view)
|
||||||
|
|
||||||
origControlsVisible = viewController.currentItemViewController.controlsVisible
|
origControlsVisible = viewController.currentItemViewController.controlsVisible
|
||||||
|
@ -53,12 +57,42 @@ class GalleryDismissInteraction: NSObject {
|
||||||
let translation = recognizer.translation(in: viewController.view)
|
let translation = recognizer.translation(in: viewController.view)
|
||||||
let velocity = recognizer.velocity(in: viewController.view)
|
let velocity = recognizer.velocity(in: viewController.view)
|
||||||
|
|
||||||
dismissVelocity = velocity
|
let translationMagnitude = sqrt(translation.x.magnitudeSquared + translation.y.magnitudeSquared)
|
||||||
dismissTranslation = translation
|
let velocityMagnitude = sqrt(velocity.x.magnitudeSquared + velocity.y.magnitudeSquared)
|
||||||
viewController.dismiss(animated: true)
|
|
||||||
|
if translationMagnitude < 150 && velocityMagnitude < 500 {
|
||||||
|
isActive = false
|
||||||
|
|
||||||
|
cancelAnimator?.stopAnimation(true)
|
||||||
|
|
||||||
|
let spring = UISpringTimingParameters(mass: 1, stiffness: 439, damping: 42, initialVelocity: .zero)
|
||||||
|
cancelAnimator = UIViewPropertyAnimator(duration: 0.2, timingParameters: spring)
|
||||||
|
cancelAnimator!.addAnimations {
|
||||||
|
self.content!.view.frame = self.origContentFrameInGallery!
|
||||||
|
self.viewController.currentItemViewController.setControlsVisible(self.origControlsVisible!, animated: false, dueToUserInteraction: false)
|
||||||
|
}
|
||||||
|
cancelAnimator!.addCompletion { _ in
|
||||||
|
guard !self.isActive else {
|
||||||
|
// bail in case the animation finishing raced with the user's interaction
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.content!.view.layer.zPosition = 0
|
||||||
|
self.content!.view.removeFromSuperview()
|
||||||
|
self.viewController.currentItemViewController.addContent()
|
||||||
|
self.content = nil
|
||||||
|
self.origContentFrameInGallery = nil
|
||||||
|
self.origControlsVisible = nil
|
||||||
|
}
|
||||||
|
cancelAnimator!.startAnimation()
|
||||||
|
|
||||||
// don't unset this until after dismiss is called, so that the dismiss animation controller can read it
|
} else {
|
||||||
isActive = false
|
dismissVelocity = velocity
|
||||||
|
dismissTranslation = translation
|
||||||
|
viewController.dismiss(animated: true)
|
||||||
|
|
||||||
|
// don't unset this until after dismiss is called, so that the dismiss animation controller can read it
|
||||||
|
isActive = false
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
|
|
@ -69,6 +69,10 @@ class GalleryItemViewController: UIViewController {
|
||||||
scrollView = UIScrollView()
|
scrollView = UIScrollView()
|
||||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
scrollView.delegate = self
|
scrollView.delegate = self
|
||||||
|
// We calculate zoom/position ignoring the safe area, so content insets need to not incorporate it either.
|
||||||
|
// Otherwise, content that fills the screen (extending into the safe area) may still end up scrollable
|
||||||
|
// (this is readily observable with tall images on a landscape iPad).
|
||||||
|
scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
|
||||||
view.addSubview(scrollView)
|
view.addSubview(scrollView)
|
||||||
|
|
||||||
|
|
|
@ -25,11 +25,31 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
||||||
|
|
||||||
let itemViewController = to.currentItemViewController
|
let itemViewController = to.currentItemViewController
|
||||||
|
|
||||||
if !itemViewController.content.canAnimateFromSourceView || UIAccessibility.prefersCrossFadeTransitions {
|
if itemViewController.content.presentationAnimation == .fade || UIAccessibility.prefersCrossFadeTransitions {
|
||||||
animateCrossFadeTransition(using: transitionContext)
|
animateCrossFadeTransition(using: transitionContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to effectively "fade out" anything that's on top of the source view.
|
||||||
|
// The 0.1 duration makes this happen faster than the rest of the animation,
|
||||||
|
// and so less noticeable.
|
||||||
|
let sourceSnapshot: UIView? = if itemViewController.content.presentationAnimation == .fromSourceViewWithoutSnapshot {
|
||||||
|
nil
|
||||||
|
} else {
|
||||||
|
sourceView.snapshotView(afterScreenUpdates: false)
|
||||||
|
}
|
||||||
|
if let sourceSnapshot {
|
||||||
|
let snapshotContainer = sourceView.ancestorForInsertingSnapshot
|
||||||
|
snapshotContainer.addSubview(sourceSnapshot)
|
||||||
|
let sourceFrameInShapshotContainer = snapshotContainer.convert(sourceView.bounds, from: sourceView)
|
||||||
|
sourceSnapshot.frame = sourceFrameInShapshotContainer
|
||||||
|
sourceSnapshot.transform = sourceView.transform
|
||||||
|
sourceSnapshot.layer.opacity = 0
|
||||||
|
UIView.animate(withDuration: 0.1) {
|
||||||
|
sourceSnapshot.layer.opacity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let container = transitionContext.containerView
|
let container = transitionContext.containerView
|
||||||
to.view.frame = container.bounds
|
to.view.frame = container.bounds
|
||||||
container.addSubview(to.view)
|
container.addSubview(to.view)
|
||||||
|
@ -56,21 +76,70 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
||||||
sourceToDestTransform = nil
|
sourceToDestTransform = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grab these before taking the content out and changing the transform.
|
||||||
|
let origContentTransform = itemViewController.content.view.transform
|
||||||
|
let origContentFrame = itemViewController.content.view.frame
|
||||||
|
|
||||||
|
// The content container provides the clipping for the content view,
|
||||||
|
// which, in case the source/dest aspect ratios don't match, makes
|
||||||
|
// it look like the content is expanding out from the source rect.
|
||||||
|
let contentContainer = UIView()
|
||||||
|
contentContainer.layer.masksToBounds = true
|
||||||
|
container.insertSubview(contentContainer, belowSubview: to.view)
|
||||||
let content = itemViewController.takeContent()
|
let content = itemViewController.takeContent()
|
||||||
content.view.translatesAutoresizingMaskIntoConstraints = true
|
content.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
container.insertSubview(content.view, belowSubview: to.view)
|
content.view.transform = .identity
|
||||||
|
// The fade-in makes the aspect ratio handling look a little bit worse,
|
||||||
|
// but papers over the z-index change and potential corner radius change.
|
||||||
|
content.view.layer.opacity = 0
|
||||||
|
contentContainer.addSubview(content.view)
|
||||||
|
|
||||||
// Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content.
|
// Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content.
|
||||||
let dimmingView = UIView()
|
let dimmingView = UIView()
|
||||||
dimmingView.backgroundColor = .black
|
dimmingView.backgroundColor = .black
|
||||||
dimmingView.frame = container.bounds
|
dimmingView.frame = container.bounds
|
||||||
dimmingView.layer.opacity = 0
|
dimmingView.layer.opacity = 0
|
||||||
container.insertSubview(dimmingView, belowSubview: content.view)
|
container.insertSubview(dimmingView, belowSubview: contentContainer)
|
||||||
|
|
||||||
to.view.backgroundColor = nil
|
to.view.backgroundColor = nil
|
||||||
to.view.layer.opacity = 0
|
to.view.layer.opacity = 0
|
||||||
content.view.frame = sourceFrameInContainer
|
|
||||||
content.view.layer.opacity = 0
|
contentContainer.frame = sourceFrameInContainer
|
||||||
|
|
||||||
|
let sourceAspectRatio: CGFloat = if sourceFrameInContainer.height > 0 {
|
||||||
|
sourceFrameInContainer.width / sourceFrameInContainer.height
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
let destAspectRatio: CGFloat = if destFrameInContainer.height > 0 {
|
||||||
|
destFrameInContainer.width / destFrameInContainer.height
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
let sourceSizeWithDestAspectRatioCenteredInContentContainer: CGRect
|
||||||
|
if 0.001 < abs(sourceAspectRatio - destAspectRatio) {
|
||||||
|
// asepct ratios are effectively equal
|
||||||
|
sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(origin: .zero, size: sourceFrameInContainer.size)
|
||||||
|
} else if sourceAspectRatio < destAspectRatio {
|
||||||
|
// source aspect ratio is narrow/taller than dest
|
||||||
|
let width = sourceFrameInContainer.height * destAspectRatio
|
||||||
|
sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(
|
||||||
|
x: -(width - sourceFrameInContainer.width) / 2,
|
||||||
|
y: 0,
|
||||||
|
width: width,
|
||||||
|
height: sourceFrameInContainer.height
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// source aspect ratio is wider/shorter than dest
|
||||||
|
let height = sourceFrameInContainer.width / destAspectRatio
|
||||||
|
sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(
|
||||||
|
x: 0,
|
||||||
|
y: -(height - sourceFrameInContainer.height) / 2,
|
||||||
|
width: sourceFrameInContainer.width,
|
||||||
|
height: height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content.view.frame = sourceSizeWithDestAspectRatioCenteredInContentContainer
|
||||||
|
|
||||||
container.layoutIfNeeded()
|
container.layoutIfNeeded()
|
||||||
|
|
||||||
|
@ -78,8 +147,14 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
||||||
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
||||||
|
|
||||||
let duration = self.transitionDuration(using: transitionContext)
|
let duration = self.transitionDuration(using: transitionContext)
|
||||||
// rougly equivalent to duration: 0.35, bounce: 0.3
|
// less bounce on bigger screens
|
||||||
let spring = UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero)
|
let spring = if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
|
// roughly equivalent to duration: 0.35, bounce: 0.2
|
||||||
|
UISpringTimingParameters(mass: 1, stiffness: 322, damping: 28, initialVelocity: .zero)
|
||||||
|
} else {
|
||||||
|
// roughly equivalent to duration: 0.35, bounce: 0.3
|
||||||
|
UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero)
|
||||||
|
}
|
||||||
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
|
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
|
||||||
|
|
||||||
animator.addAnimations {
|
animator.addAnimations {
|
||||||
|
@ -87,25 +162,35 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
||||||
|
|
||||||
to.view.layer.opacity = 1
|
to.view.layer.opacity = 1
|
||||||
|
|
||||||
content.view.frame = destFrameInContainer
|
contentContainer.frame = destFrameInContainer
|
||||||
|
content.view.frame = contentContainer.bounds
|
||||||
content.view.layer.opacity = 1
|
content.view.layer.opacity = 1
|
||||||
|
|
||||||
itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false)
|
itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false)
|
||||||
|
|
||||||
if let sourceToDestTransform {
|
if let sourceToDestTransform {
|
||||||
|
sourceSnapshot?.transform = sourceToDestTransform
|
||||||
self.sourceView.transform = sourceToDestTransform
|
self.sourceView.transform = sourceToDestTransform
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.addCompletion { _ in
|
animator.addCompletion { _ in
|
||||||
|
sourceSnapshot?.removeFromSuperview()
|
||||||
|
self.sourceView.layer.opacity = 1
|
||||||
|
if sourceToDestTransform != nil {
|
||||||
|
self.sourceView.transform = origSourceTransform
|
||||||
|
}
|
||||||
|
|
||||||
|
contentContainer.removeFromSuperview()
|
||||||
dimmingView.removeFromSuperview()
|
dimmingView.removeFromSuperview()
|
||||||
|
|
||||||
to.view.backgroundColor = .black
|
to.view.backgroundColor = .black
|
||||||
|
|
||||||
if sourceToDestTransform != nil {
|
// Reset the properties we changed before re-adding the content to the scroll view.
|
||||||
self.sourceView.transform = origSourceTransform
|
// (I would expect UIScrollView to effectively do this itself, but w/e.)
|
||||||
}
|
content.view.transform = origContentTransform
|
||||||
|
content.view.frame = origContentFrame
|
||||||
|
|
||||||
itemViewController.addContent()
|
itemViewController.addContent()
|
||||||
|
|
||||||
transitionContext.completeTransition(true)
|
transitionContext.completeTransition(true)
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
//
|
||||||
|
// UIView+Utilities.swift
|
||||||
|
// GalleryVC
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/24/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIView {
|
||||||
|
|
||||||
|
var ancestorForInsertingSnapshot: UIView {
|
||||||
|
var view = self
|
||||||
|
while let superview = view.superview {
|
||||||
|
if superview.layer.masksToBounds {
|
||||||
|
return superview
|
||||||
|
} else if superview is UIScrollView {
|
||||||
|
return self
|
||||||
|
} else {
|
||||||
|
view = superview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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,8 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "Pachyderm",
|
name: "Pachyderm",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v15),
|
.iOS(.v16),
|
||||||
|
.macOS(.v13),
|
||||||
],
|
],
|
||||||
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.
|
||||||
|
@ -16,7 +17,6 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// Dependencies declare other packages that this package depends on.
|
// Dependencies declare other packages that this package depends on.
|
||||||
.package(url: "https://github.com/karwa/swift-url.git", exact: "0.4.2"),
|
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
|
@ -24,11 +24,15 @@ let package = Package(
|
||||||
.target(
|
.target(
|
||||||
name: "Pachyderm",
|
name: "Pachyderm",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "WebURL", 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)
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The base Mastodon API client.
|
The base Mastodon API client.
|
||||||
|
@ -25,27 +24,30 @@ public struct Client: Sendable {
|
||||||
|
|
||||||
public var timeoutInterval: TimeInterval = 60
|
public var timeoutInterval: TimeInterval = 60
|
||||||
|
|
||||||
static let decoder: JSONDecoder = {
|
private static let dateFormatter: DateFormatter = {
|
||||||
let decoder = JSONDecoder()
|
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
||||||
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
||||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
let iso8601 = ISO8601DateFormatter()
|
return formatter
|
||||||
|
}()
|
||||||
|
private static let iso8601Formatter = ISO8601DateFormatter()
|
||||||
|
private static func decodeDate(string: String) -> Date? {
|
||||||
|
// for the next time mastodon accidentally changes date formats >.>
|
||||||
|
return dateFormatter.date(from: string) ?? iso8601Formatter.date(from: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
static let decoder: JSONDecoder = {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
decoder.dateDecodingStrategy = .custom({ (decoder) in
|
decoder.dateDecodingStrategy = .custom({ (decoder) in
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
let str = try container.decode(String.self)
|
let str = try container.decode(String.self)
|
||||||
// for the next time mastodon accidentally changes date formats >.>
|
if let date = Self.decodeDate(string: str) {
|
||||||
if let date = formatter.date(from: str) {
|
|
||||||
return date
|
|
||||||
} else if let date = iso8601.date(from: str) {
|
|
||||||
return date
|
return date
|
||||||
} else {
|
} else {
|
||||||
throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)"))
|
throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return decoder
|
return decoder
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -105,6 +107,15 @@ public struct Client: Sendable {
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func error(from response: HTTPURLResponse) -> ErrorType {
|
||||||
|
if response.statusCode == 429,
|
||||||
|
let date = response.value(forHTTPHeaderField: "X-RateLimit-Reset").flatMap(Self.decodeDate) {
|
||||||
|
return .rateLimited(date)
|
||||||
|
} else {
|
||||||
|
return .unexpectedStatus(response.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
public func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
||||||
return try await withCheckedThrowingContinuation { continuation in
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
@ -190,8 +201,8 @@ public struct Client: Sendable {
|
||||||
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
|
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
|
||||||
let wellKnownResults = try await run(wellKnown).0
|
let wellKnownResults = try await run(wellKnown).0
|
||||||
if let url = wellKnownResults.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
|
if let url = wellKnownResults.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
|
||||||
let href = WebURL(url.href),
|
let href = try? URL.ParseStrategy().parse(url.href),
|
||||||
href.host == WebURL(self.baseURL)?.host {
|
href.host == self.baseURL.host() {
|
||||||
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
|
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
|
||||||
return try await run(nodeInfo).0
|
return try await run(nodeInfo).0
|
||||||
} else {
|
} else {
|
||||||
|
@ -575,6 +586,8 @@ extension Client {
|
||||||
return "Invalid Model"
|
return "Invalid Model"
|
||||||
case .mastodonError(let code, let error):
|
case .mastodonError(let code, let error):
|
||||||
return "Server Error (\(code)): \(error)"
|
return "Server Error (\(code)): \(error)"
|
||||||
|
case .rateLimited(let reset):
|
||||||
|
return "Rate Limited Until \(reset.formatted(date: .omitted, time: .standard))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -585,6 +598,7 @@ extension Client {
|
||||||
case invalidResponse
|
case invalidResponse
|
||||||
case invalidModel(Swift.Error)
|
case invalidModel(Swift.Error)
|
||||||
case mastodonError(Int, String)
|
case mastodonError(Int, String)
|
||||||
|
case rateLimited(Date)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NodeInfoError: LocalizedError {
|
enum NodeInfoError: LocalizedError {
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Announcement: Decodable, Sendable, Hashable, Identifiable {
|
public struct Announcement: Decodable, Sendable, Hashable, Identifiable {
|
||||||
public let id: String
|
public let id: String
|
||||||
|
@ -60,7 +59,7 @@ extension Announcement {
|
||||||
public struct Account: Decodable, Sendable, Hashable {
|
public struct Account: Decodable, Sendable, Hashable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let username: String
|
public let username: String
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
public let acct: String
|
public let acct: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +67,7 @@ extension Announcement {
|
||||||
extension Announcement {
|
extension Announcement {
|
||||||
public struct Status: Decodable, Sendable, Hashable {
|
public struct Status: Decodable, Sendable, Hashable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,18 +7,17 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Card: Codable, Sendable {
|
public struct Card: Codable, Sendable {
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
public let title: String
|
public let title: String
|
||||||
public let description: String
|
public let description: String
|
||||||
public let image: WebURL?
|
@OptionalURLDecoder public var image: URL?
|
||||||
public let kind: Kind
|
public let kind: Kind
|
||||||
public let authorName: String?
|
public let authorName: String?
|
||||||
public let authorURL: WebURL?
|
@OptionalURLDecoder public var authorURL: URL?
|
||||||
public let providerName: String?
|
public let providerName: String?
|
||||||
public let providerURL: WebURL?
|
@OptionalURLDecoder public var providerURL: URL?
|
||||||
public let html: String?
|
public let html: String?
|
||||||
public let width: Int?
|
public let width: Int?
|
||||||
public let height: Int?
|
public let height: Int?
|
||||||
|
@ -27,15 +26,15 @@ public struct Card: Codable, Sendable {
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
url: WebURL,
|
url: URL,
|
||||||
title: String,
|
title: String,
|
||||||
description: String,
|
description: String,
|
||||||
image: WebURL? = nil,
|
image: URL? = nil,
|
||||||
kind: Card.Kind,
|
kind: Card.Kind,
|
||||||
authorName: String? = nil,
|
authorName: String? = nil,
|
||||||
authorURL: WebURL? = nil,
|
authorURL: URL? = nil,
|
||||||
providerName: String? = nil,
|
providerName: String? = nil,
|
||||||
providerURL: WebURL? = nil,
|
providerURL: URL? = nil,
|
||||||
html: String? = nil,
|
html: String? = nil,
|
||||||
width: Int? = nil,
|
width: Int? = nil,
|
||||||
height: Int? = nil,
|
height: Int? = nil,
|
||||||
|
@ -61,15 +60,15 @@ public struct Card: Codable, Sendable {
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
self._url = try container.decode(URLDecoder.self, forKey: .url)
|
||||||
self.title = try container.decode(String.self, forKey: .title)
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
self.description = try container.decode(String.self, forKey: .description)
|
self.description = try container.decode(String.self, forKey: .description)
|
||||||
self.kind = try container.decode(Kind.self, forKey: .kind)
|
self.kind = try container.decode(Kind.self, forKey: .kind)
|
||||||
self.image = try? container.decodeIfPresent(WebURL.self, forKey: .image)
|
self._image = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .image) ?? nil
|
||||||
self.authorName = try? container.decodeIfPresent(String.self, forKey: .authorName)
|
self.authorName = try? container.decodeIfPresent(String.self, forKey: .authorName)
|
||||||
self.authorURL = try? container.decodeIfPresent(WebURL.self, forKey: .authorURL)
|
self._authorURL = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .authorURL) ?? nil
|
||||||
self.providerName = try? container.decodeIfPresent(String.self, forKey: .providerName)
|
self.providerName = try? container.decodeIfPresent(String.self, forKey: .providerName)
|
||||||
self.providerURL = try? container.decodeIfPresent(WebURL.self, forKey: .providerURL)
|
self._providerURL = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .providerURL) ?? nil
|
||||||
self.html = try? container.decodeIfPresent(String.self, forKey: .html)
|
self.html = try? container.decodeIfPresent(String.self, forKey: .html)
|
||||||
self.width = try? container.decodeIfPresent(Int.self, forKey: .width)
|
self.width = try? container.decodeIfPresent(Int.self, forKey: .width)
|
||||||
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)
|
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)
|
||||||
|
|
|
@ -7,14 +7,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Emoji: Codable, Sendable {
|
public struct Emoji: Codable, Sendable {
|
||||||
public let shortcode: String
|
public let shortcode: String
|
||||||
// these shouldn't need to be WebURLs as they're not external resources,
|
@URLDecoder public var url: URL
|
||||||
// but some instances (pleroma?) has emoji urls that Foundation considers malformed so we use WebURL to be more lenient
|
@URLDecoder public var staticURL: URL
|
||||||
public let url: WebURL
|
|
||||||
public let staticURL: WebURL
|
|
||||||
public let visibleInPicker: Bool
|
public let visibleInPicker: Bool
|
||||||
public let category: String?
|
public let category: String?
|
||||||
|
|
||||||
|
@ -22,8 +19,8 @@ public struct Emoji: Codable, Sendable {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
self.shortcode = try container.decode(String.self, forKey: .shortcode)
|
self.shortcode = try container.decode(String.self, forKey: .shortcode)
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
self._url = try container.decode(URLDecoder.self, forKey: .url)
|
||||||
self.staticURL = try container.decode(WebURL.self, forKey: .staticURL)
|
self._staticURL = try container.decode(URLDecoder.self, forKey: .staticURL)
|
||||||
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
|
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
|
||||||
self.category = try container.decodeIfPresent(String.self, forKey: .category)
|
self.category = try container.decodeIfPresent(String.self, forKey: .category)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,10 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
public struct Hashtag: Codable, Sendable {
|
public struct Hashtag: Codable, Sendable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
/// Only present when returned from the trending hashtags endpoint
|
/// Only present when returned from the trending hashtags endpoint
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
/// Only present on Mastodon >= 4 and when logged in
|
/// Only present on Mastodon >= 4 and when logged in
|
||||||
|
@ -20,7 +18,7 @@ public struct Hashtag: Codable, Sendable {
|
||||||
|
|
||||||
public init(name: String, url: URL) {
|
public init(name: String, url: URL) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = WebURL(url)!
|
self.url = url
|
||||||
self.history = nil
|
self.history = nil
|
||||||
self.following = nil
|
self.following = nil
|
||||||
}
|
}
|
||||||
|
@ -29,7 +27,7 @@ public struct Hashtag: Codable, Sendable {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
self._url = try container.decode(URLDecoder.self, forKey: .url)
|
||||||
self.history = try container.decodeIfPresent([History].self, forKey: .history)
|
self.history = try container.decodeIfPresent([History].self, forKey: .history)
|
||||||
self.following = try container.decodeIfPresent(Bool.self, forKey: .following)
|
self.following = try container.decodeIfPresent(Bool.self, forKey: .following)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,9 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Mention: Codable, Sendable {
|
public struct Mention: Codable, Sendable {
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
public let username: String
|
public let username: String
|
||||||
public let acct: String
|
public let acct: String
|
||||||
/// The instance-local ID of the user being mentioned.
|
/// The instance-local ID of the user being mentioned.
|
||||||
|
@ -21,15 +20,10 @@ public struct Mention: Codable, Sendable {
|
||||||
self.username = try container.decode(String.self, forKey: .username)
|
self.username = try container.decode(String.self, forKey: .username)
|
||||||
self.acct = try container.decode(String.self, forKey: .acct)
|
self.acct = try container.decode(String.self, forKey: .acct)
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
do {
|
self._url = try container.decode(URLDecoder.self, forKey: .url)
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
|
||||||
} catch {
|
|
||||||
let s = try? container.decode(String.self, forKey: .url)
|
|
||||||
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "Could not decode URL '\(s ?? "<failed to decode string>")'")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(url: WebURL, username: String, acct: String, id: String) {
|
public init(url: URL, username: String, acct: String, id: String) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.username = username
|
self.username = username
|
||||||
self.acct = acct
|
self.acct = acct
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Notification: Decodable, Sendable {
|
public struct Notification: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
|
@ -18,7 +17,7 @@ public struct Notification: Decodable, Sendable {
|
||||||
// Only present for pleroma emoji reactions
|
// Only present for pleroma emoji reactions
|
||||||
// Either an emoji or :shortcode: (for akkoma custom emoji reactions)
|
// Either an emoji or :shortcode: (for akkoma custom emoji reactions)
|
||||||
public let emoji: String?
|
public let emoji: String?
|
||||||
public let emojiURL: WebURL?
|
@OptionalURLDecoder public var emojiURL: URL?
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
@ -33,7 +32,7 @@ public struct Notification: Decodable, Sendable {
|
||||||
self.account = try container.decode(Account.self, forKey: .account)
|
self.account = try container.decode(Account.self, forKey: .account)
|
||||||
self.status = try container.decodeIfPresent(Status.self, forKey: .status)
|
self.status = try container.decodeIfPresent(Status.self, forKey: .status)
|
||||||
self.emoji = try container.decodeIfPresent(String.self, forKey: .emoji)
|
self.emoji = try container.decodeIfPresent(String.self, forKey: .emoji)
|
||||||
self.emojiURL = try container.decodeIfPresent(WebURL.self, forKey: .emojiURL)
|
self._emojiURL = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .emojiURL) ?? nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func dismiss(id notificationID: String) -> Request<Empty> {
|
public static func dismiss(id notificationID: String) -> Request<Empty> {
|
||||||
|
|
|
@ -6,14 +6,13 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct PushNotification: Decodable {
|
public struct PushNotification: Decodable {
|
||||||
public let accessToken: String
|
public let accessToken: String
|
||||||
public let preferredLocale: String
|
public let preferredLocale: String
|
||||||
public let notificationID: String
|
public let notificationID: String
|
||||||
public let notificationType: Notification.Kind
|
public let notificationType: Notification.Kind
|
||||||
public let icon: WebURL
|
@URLDecoder public var icon: URL
|
||||||
public let title: String
|
public let title: String
|
||||||
public let body: String
|
public let body: String
|
||||||
|
|
||||||
|
@ -29,7 +28,7 @@ public struct PushNotification: Decodable {
|
||||||
self.notificationID = i.description
|
self.notificationID = i.description
|
||||||
}
|
}
|
||||||
self.notificationType = try container.decode(Notification.Kind.self, forKey: .notificationType)
|
self.notificationType = try container.decode(Notification.Kind.self, forKey: .notificationType)
|
||||||
self.icon = try container.decode(WebURL.self, forKey: .icon)
|
self._icon = try container.decode(URLDecoder.self, forKey: .icon)
|
||||||
self.title = try container.decode(String.self, forKey: .title)
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
self.body = try container.decode(String.self, forKey: .body)
|
self.body = try container.decode(String.self, forKey: .body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public final class Status: StatusProtocol, Decodable, Sendable {
|
public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
/// The pseudo-visibility used by instance types (Akkoma) that overload the visibility for local-only posts.
|
/// The pseudo-visibility used by instance types (Akkoma) that overload the visibility for local-only posts.
|
||||||
|
@ -15,7 +14,8 @@ public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let url: WebURL?
|
private let _url: OptionalURLDecoder
|
||||||
|
public var url: URL? { _url.wrappedValue }
|
||||||
public let account: Account
|
public let account: Account
|
||||||
public let inReplyToID: String?
|
public let inReplyToID: String?
|
||||||
public let inReplyToAccountID: String?
|
public let inReplyToAccountID: String?
|
||||||
|
@ -55,13 +55,13 @@ public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
self.uri = try container.decode(String.self, forKey: .uri)
|
self.uri = try container.decode(String.self, forKey: .uri)
|
||||||
do {
|
do {
|
||||||
self.url = try container.decodeIfPresent(WebURL.self, forKey: .url)
|
self._url = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .url) ?? nil
|
||||||
} catch {
|
} catch {
|
||||||
let s = try? container.decode(String.self, forKey: .url)
|
let s = try? container.decode(String.self, forKey: .url)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
self.url = nil
|
self._url = OptionalURLDecoder(wrappedValue: nil)
|
||||||
} else {
|
} else {
|
||||||
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath + [CodingKeys.url], debugDescription: "Could not decode URL '\(s ?? "<failed to decode string>")'", underlyingError: error))
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.account = try container.decode(Account.self, forKey: .account)
|
self.account = try container.decode(Account.self, forKey: .account)
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
||||||
public private(set) var notifications: [Notification]
|
public private(set) var notifications: [Notification]
|
||||||
|
@ -150,7 +149,7 @@ public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
||||||
case poll
|
case poll
|
||||||
case update
|
case update
|
||||||
case status
|
case status
|
||||||
case emojiReaction(String, WebURL?)
|
case emojiReaction(String, URL?)
|
||||||
case unknown
|
case unknown
|
||||||
|
|
||||||
var notificationKind: Notification.Kind {
|
var notificationKind: Notification.Kind {
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// URLDecoder.swift
|
||||||
|
// Pachyderm
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/15/24.
|
||||||
|
//
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
private let parseStrategy = URL.ParseStrategy()
|
||||||
|
.scheme(.required)
|
||||||
|
.user(.optional)
|
||||||
|
.password(.optional)
|
||||||
|
.host(.required)
|
||||||
|
.port(.optional)
|
||||||
|
.path(.optional)
|
||||||
|
.query(.optional)
|
||||||
|
.fragment(.optional)
|
||||||
|
|
||||||
|
private let formatStyle = URL.FormatStyle()
|
||||||
|
.scheme(.always)
|
||||||
|
.user(.omitWhen(.user, matches: [""]))
|
||||||
|
.password(.omitWhen(.password, matches: [""]))
|
||||||
|
.host(.always)
|
||||||
|
.port(.omitIfHTTPFamily)
|
||||||
|
.path(.always)
|
||||||
|
.query(.omitWhen(.query, matches: [""]))
|
||||||
|
.fragment(.omitWhen(.fragment, matches: [""]))
|
||||||
|
|
||||||
|
@propertyWrapper
|
||||||
|
public struct URLDecoder: Codable, Sendable, Hashable {
|
||||||
|
public var wrappedValue: URL
|
||||||
|
|
||||||
|
public init(wrappedValue: URL) {
|
||||||
|
self.wrappedValue = wrappedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: any Decoder) throws {
|
||||||
|
let s = try decoder.singleValueContainer().decode(String.self)
|
||||||
|
self.wrappedValue = try parseStrategy.parse(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
try container.encode(wrappedValue.formatted(formatStyle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@propertyWrapper
|
||||||
|
public struct OptionalURLDecoder: Codable, Sendable, Hashable, ExpressibleByNilLiteral {
|
||||||
|
public var wrappedValue: URL?
|
||||||
|
|
||||||
|
public init(wrappedValue: URL?) {
|
||||||
|
self.wrappedValue = wrappedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(nilLiteral: ()) {
|
||||||
|
self.wrappedValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: any Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
if container.decodeNil() {
|
||||||
|
self.wrappedValue = nil
|
||||||
|
} else {
|
||||||
|
let s = try container.decode(String.self)
|
||||||
|
if s.isEmpty {
|
||||||
|
self.wrappedValue = nil
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
self.wrappedValue = try parseStrategy.parse(s)
|
||||||
|
} catch {
|
||||||
|
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Could not decode URL '\(s)'", underlyingError: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
if let wrappedValue {
|
||||||
|
try container.encode(wrappedValue.formatted(formatStyle))
|
||||||
|
} else {
|
||||||
|
try container.encodeNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -197,72 +197,72 @@ class NotificationGroupTests: XCTestCase {
|
||||||
|
|
||||||
func testGroupSimple() {
|
func testGroupSimple() {
|
||||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeA2], only: [.favourite])
|
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeA2], only: [.favourite])
|
||||||
XCTAssertEqual(groups, [NotificationGroup(notifications: [likeA1, likeA2])!])
|
XCTAssertEqual(groups, [NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGroupWithOtherGroupableInBetween() {
|
func testGroupWithOtherGroupableInBetween() {
|
||||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeB, likeA2], only: [.favourite])
|
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeB, likeA2], only: [.favourite])
|
||||||
XCTAssertEqual(groups, [
|
XCTAssertEqual(groups, [
|
||||||
NotificationGroup(notifications: [likeA1, likeA2])!,
|
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!,
|
||||||
NotificationGroup(notifications: [likeB])!,
|
NotificationGroup(notifications: [likeB], kind: .favourite)!,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDontGroupWithUngroupableInBetween() {
|
func testDontGroupWithUngroupableInBetween() {
|
||||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, mentionB, likeA2], only: [.favourite])
|
let groups = NotificationGroup.createGroups(notifications: [likeA1, mentionB, likeA2], only: [.favourite])
|
||||||
XCTAssertEqual(groups, [
|
XCTAssertEqual(groups, [
|
||||||
NotificationGroup(notifications: [likeA1])!,
|
NotificationGroup(notifications: [likeA1], kind: .favourite)!,
|
||||||
NotificationGroup(notifications: [mentionB])!,
|
NotificationGroup(notifications: [mentionB], kind: .mention)!,
|
||||||
NotificationGroup(notifications: [likeA2])!,
|
NotificationGroup(notifications: [likeA2], kind: .favourite)!,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMergeSimpleGroups() {
|
func testMergeSimpleGroups() {
|
||||||
let group1 = NotificationGroup(notifications: [likeA1])!
|
let group1 = NotificationGroup(notifications: [likeA1], kind: .favourite)!
|
||||||
let group2 = NotificationGroup(notifications: [likeA2])!
|
let group2 = NotificationGroup(notifications: [likeA2], kind: .favourite)!
|
||||||
let merged = NotificationGroup.mergeGroups(first: [group1], second: [group2], only: [.favourite])
|
let merged = NotificationGroup.mergeGroups(first: [group1], second: [group2], only: [.favourite])
|
||||||
XCTAssertEqual(merged, [
|
XCTAssertEqual(merged, [
|
||||||
NotificationGroup(notifications: [likeA1, likeA2])!
|
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMergeGroupsWithOtherGroupableInBetween() {
|
func testMergeGroupsWithOtherGroupableInBetween() {
|
||||||
let group1 = NotificationGroup(notifications: [likeA1])!
|
let group1 = NotificationGroup(notifications: [likeA1], kind: .favourite)!
|
||||||
let group2 = NotificationGroup(notifications: [likeB])!
|
let group2 = NotificationGroup(notifications: [likeB], kind: .favourite)!
|
||||||
let group3 = NotificationGroup(notifications: [likeA2])!
|
let group3 = NotificationGroup(notifications: [likeA2], kind: .favourite)!
|
||||||
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
||||||
XCTAssertEqual(merged, [
|
XCTAssertEqual(merged, [
|
||||||
NotificationGroup(notifications: [likeA1, likeA2])!,
|
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!,
|
||||||
NotificationGroup(notifications: [likeB])!,
|
NotificationGroup(notifications: [likeB], kind: .favourite)!,
|
||||||
])
|
])
|
||||||
|
|
||||||
let merged2 = NotificationGroup.mergeGroups(first: [group1], second: [group2, group3], only: [.favourite])
|
let merged2 = NotificationGroup.mergeGroups(first: [group1], second: [group2, group3], only: [.favourite])
|
||||||
XCTAssertEqual(merged2, [
|
XCTAssertEqual(merged2, [
|
||||||
NotificationGroup(notifications: [likeA1, likeA2])!,
|
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!,
|
||||||
NotificationGroup(notifications: [likeB])!,
|
NotificationGroup(notifications: [likeB], kind: .favourite)!,
|
||||||
])
|
])
|
||||||
|
|
||||||
let group4 = NotificationGroup(notifications: [likeB2])!
|
let group4 = NotificationGroup(notifications: [likeB2], kind: .favourite)!
|
||||||
let group5 = NotificationGroup(notifications: [mentionB])!
|
let group5 = NotificationGroup(notifications: [mentionB], kind: .mention)!
|
||||||
let merged3 = NotificationGroup.mergeGroups(first: [group1, group5, group2], second: [group4, group3], only: [.favourite])
|
let merged3 = NotificationGroup.mergeGroups(first: [group1, group5, group2], second: [group4, group3], only: [.favourite])
|
||||||
print(merged3.count)
|
print(merged3.count)
|
||||||
XCTAssertEqual(merged3, [
|
XCTAssertEqual(merged3, [
|
||||||
group1,
|
group1,
|
||||||
group5,
|
group5,
|
||||||
NotificationGroup(notifications: [likeB, likeB2]),
|
NotificationGroup(notifications: [likeB, likeB2], kind: .favourite),
|
||||||
group3
|
group3
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDontMergeWithUngroupableInBetween() {
|
func testDontMergeWithUngroupableInBetween() {
|
||||||
let group1 = NotificationGroup(notifications: [likeA1])!
|
let group1 = NotificationGroup(notifications: [likeA1], kind: .favourite)!
|
||||||
let group2 = NotificationGroup(notifications: [mentionB])!
|
let group2 = NotificationGroup(notifications: [mentionB], kind: .mention)!
|
||||||
let group3 = NotificationGroup(notifications: [likeA2])!
|
let group3 = NotificationGroup(notifications: [likeA2], kind: .favourite)!
|
||||||
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
||||||
XCTAssertEqual(merged, [
|
XCTAssertEqual(merged, [
|
||||||
NotificationGroup(notifications: [likeA1])!,
|
NotificationGroup(notifications: [likeA1], kind: .favourite)!,
|
||||||
NotificationGroup(notifications: [mentionB])!,
|
NotificationGroup(notifications: [mentionB], kind: .mention)!,
|
||||||
NotificationGroup(notifications: [likeA2])!,
|
NotificationGroup(notifications: [likeA2], kind: .favourite)!,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,20 +6,21 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
import WebURL
|
@testable import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class URLTests: XCTestCase {
|
class URLTests: XCTestCase {
|
||||||
|
|
||||||
func testDecodeURL() {
|
func testDecodeURL() {
|
||||||
XCTAssertNotNil(WebURL(URL(string: "https://xn--baw-joa.social/@unituebingen")!))
|
XCTAssertNotNil(try? URL.ParseStrategy().parse("https://xn--baw-joa.social/test/é"))
|
||||||
XCTAssertNotNil(WebURL("https://xn--baw-joa.social/@unituebingen"))
|
XCTAssertNotNil(try? URL.ParseStrategy().parse("http://見.香港/热狗/🌭"))
|
||||||
XCTAssertNotNil(URLComponents(string: "https://xn--baw-joa.social/test/é"))
|
}
|
||||||
XCTAssertNotNil(WebURL("https://xn--baw-joa.social/test/é"))
|
|
||||||
if #available(iOS 16.0, *) {
|
func testRoundtripURL() throws {
|
||||||
XCTAssertNotNil(try? URL.ParseStrategy().parse("https://xn--baw-joa.social/test/é"))
|
let orig = URLDecoder(wrappedValue: URL(string: "https://example.com")!)
|
||||||
XCTAssertNotNil(try? URL.ParseStrategy().parse("http://見.香港/热狗/🌭"))
|
let encoded = try JSONEncoder().encode(orig)
|
||||||
}
|
print(String(data: encoded, encoding: .utf8)!)
|
||||||
|
let decoded = try JSONDecoder().decode(URLDecoder.self, from: encoded)
|
||||||
|
XCTAssertEqual(orig.wrappedValue, decoded.wrappedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]),
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ComposeUI
|
import ComposeUI
|
||||||
import TuskerComponents
|
import TuskerComponents
|
||||||
import WebURLFoundationExtras
|
|
||||||
import Combine
|
import Combine
|
||||||
import TuskerPreferences
|
import TuskerPreferences
|
||||||
|
|
||||||
|
@ -46,7 +45,7 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
|
||||||
currentAccountContainerView: { AnyView(SwitchAccountContainerView(content: $0, mastodonContextPublisher: mastodonContextPublisher)) },
|
currentAccountContainerView: { AnyView(SwitchAccountContainerView(content: $0, mastodonContextPublisher: mastodonContextPublisher)) },
|
||||||
replyContentView: { _, _ in fatalError("replies aren't allowed in share sheet") },
|
replyContentView: { _, _ in fatalError("replies aren't allowed in share sheet") },
|
||||||
emojiImageView: {
|
emojiImageView: {
|
||||||
AnyView(AsyncImage(url: URL($0.url)!) {
|
AnyView(AsyncImage(url: $0.url) {
|
||||||
$0
|
$0
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
|
|
|
@ -103,7 +103,6 @@
|
||||||
D630C3E12BC61C6700208903 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E02BC61C6700208903 /* UserAccounts */; };
|
D630C3E12BC61C6700208903 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E02BC61C6700208903 /* UserAccounts */; };
|
||||||
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E42BC6313400208903 /* Pachyderm */; };
|
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E42BC6313400208903 /* Pachyderm */; };
|
||||||
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = D630C4222BC7842C00208903 /* HTMLStreamer */; };
|
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = D630C4222BC7842C00208903 /* HTMLStreamer */; };
|
||||||
D630C4252BC7845800208903 /* WebURL in Frameworks */ = {isa = PBXBuildFile; productRef = D630C4242BC7845800208903 /* WebURL */; };
|
|
||||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
|
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
||||||
|
@ -141,7 +140,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 */; };
|
||||||
|
@ -165,7 +163,6 @@
|
||||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
|
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
|
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
||||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
||||||
|
@ -204,20 +201,16 @@
|
||||||
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; };
|
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; };
|
||||||
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; };
|
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; };
|
||||||
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; };
|
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; };
|
||||||
D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */; };
|
|
||||||
D69261272BB3BA610023152C /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261262BB3BA610023152C /* Box.swift */; };
|
D69261272BB3BA610023152C /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261262BB3BA610023152C /* Box.swift */; };
|
||||||
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; };
|
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; };
|
||||||
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; };
|
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; };
|
||||||
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */; };
|
D6934F302BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */; };
|
||||||
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */; };
|
|
||||||
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */; };
|
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */; };
|
||||||
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */; };
|
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */; };
|
||||||
D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F372BA8E2B7002B1C8D /* GifvController.swift */; };
|
D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F372BA8E2B7002B1C8D /* GifvController.swift */; };
|
||||||
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */; };
|
D6934F3C2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */; };
|
||||||
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */; };
|
|
||||||
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; };
|
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; };
|
||||||
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
|
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
|
||||||
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; };
|
|
||||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
||||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
||||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
||||||
|
@ -337,7 +330,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 +565,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>"; };
|
||||||
|
@ -639,19 +631,15 @@
|
||||||
D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = "<group>"; };
|
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = "<group>"; };
|
||||||
D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = "<group>"; };
|
D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = "<group>"; };
|
||||||
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOverlayViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D69261262BB3BA610023152C /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
|
D69261262BB3BA610023152C /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
|
||||||
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsGalleryDataSource.swift; sourceTree = "<group>"; };
|
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsGalleryDataSource.swift; sourceTree = "<group>"; };
|
||||||
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryContentViewController.swift; sourceTree = "<group>"; };
|
D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrayscalableImageGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||||
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingGalleryContentViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryDataSource.swift; sourceTree = "<group>"; };
|
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryDataSource.swift; sourceTree = "<group>"; };
|
||||||
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvGalleryContentViewController.swift; sourceTree = "<group>"; };
|
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||||
D6934F372BA8E2B7002B1C8D /* GifvController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvController.swift; sourceTree = "<group>"; };
|
D6934F372BA8E2B7002B1C8D /* GifvController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvController.swift; sourceTree = "<group>"; };
|
||||||
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackGalleryContentViewController.swift; sourceTree = "<group>"; };
|
D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrayscalableVideoGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||||
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoGalleryContentViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageActivityItemSource.swift; sourceTree = "<group>"; };
|
D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
|
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
||||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
||||||
|
@ -778,7 +766,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>"; };
|
||||||
|
@ -828,7 +816,6 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D630C4252BC7845800208903 /* WebURL in Frameworks */,
|
|
||||||
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */,
|
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */,
|
||||||
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
|
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
|
||||||
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
|
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
|
||||||
|
@ -865,7 +852,6 @@
|
||||||
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */,
|
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */,
|
||||||
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
||||||
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
|
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
|
||||||
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
||||||
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
|
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
|
||||||
);
|
);
|
||||||
|
@ -901,13 +887,9 @@
|
||||||
children = (
|
children = (
|
||||||
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */,
|
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */,
|
||||||
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */,
|
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */,
|
||||||
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */,
|
D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */,
|
||||||
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */,
|
|
||||||
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
|
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
|
||||||
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */,
|
D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */,
|
||||||
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */,
|
|
||||||
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */,
|
|
||||||
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */,
|
|
||||||
);
|
);
|
||||||
path = Gallery;
|
path = Gallery;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1490,7 +1472,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 +1609,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 */,
|
||||||
|
@ -1796,7 +1777,6 @@
|
||||||
D630C3E02BC61C6700208903 /* UserAccounts */,
|
D630C3E02BC61C6700208903 /* UserAccounts */,
|
||||||
D630C3E42BC6313400208903 /* Pachyderm */,
|
D630C3E42BC6313400208903 /* Pachyderm */,
|
||||||
D630C4222BC7842C00208903 /* HTMLStreamer */,
|
D630C4222BC7842C00208903 /* HTMLStreamer */,
|
||||||
D630C4242BC7845800208903 /* WebURL */,
|
|
||||||
D62220462C7EA8DF003E43B7 /* TuskerPreferences */,
|
D62220462C7EA8DF003E43B7 /* TuskerPreferences */,
|
||||||
);
|
);
|
||||||
productName = NotificationExtension;
|
productName = NotificationExtension;
|
||||||
|
@ -1848,7 +1828,6 @@
|
||||||
);
|
);
|
||||||
name = Tusker;
|
name = Tusker;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
|
||||||
D674A50827F9128D00BA03AC /* Pachyderm */,
|
D674A50827F9128D00BA03AC /* Pachyderm */,
|
||||||
D6552366289870790048A653 /* ScreenCorners */,
|
D6552366289870790048A653 /* ScreenCorners */,
|
||||||
D63CC701290EC0B8000E19DE /* Sentry */,
|
D63CC701290EC0B8000E19DE /* Sentry */,
|
||||||
|
@ -1975,7 +1954,6 @@
|
||||||
);
|
);
|
||||||
mainGroup = D6D4DDC3212518A000E1C4BB;
|
mainGroup = D6D4DDC3212518A000E1C4BB;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
|
|
||||||
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
|
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
|
||||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
||||||
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */,
|
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */,
|
||||||
|
@ -2125,7 +2103,6 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */,
|
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
|
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
|
||||||
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */,
|
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */,
|
||||||
|
@ -2172,8 +2149,7 @@
|
||||||
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
||||||
D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */,
|
D6934F3C2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift in Sources */,
|
||||||
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */,
|
|
||||||
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||||
D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */,
|
D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */,
|
||||||
|
@ -2193,7 +2169,6 @@
|
||||||
D6D94955298963A900C59229 /* Colors.swift in Sources */,
|
D6D94955298963A900C59229 /* Colors.swift in Sources */,
|
||||||
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
||||||
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
|
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
|
||||||
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
|
|
||||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||||
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */,
|
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */,
|
||||||
|
@ -2221,7 +2196,6 @@
|
||||||
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
|
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
|
||||||
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
||||||
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */,
|
|
||||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
||||||
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
|
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||||
|
@ -2306,7 +2280,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 */,
|
||||||
|
@ -2344,7 +2318,7 @@
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */,
|
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */,
|
||||||
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */,
|
D6934F302BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift in Sources */,
|
||||||
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */,
|
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
||||||
|
@ -2390,7 +2364,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 +2520,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 +2553,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 +2585,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 +2676,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 +2720,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 +2743,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 +2771,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 +2800,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 +2829,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 +2985,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 +3018,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 +3083,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 +3103,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 +3126,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 +3151,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",
|
||||||
|
@ -3282,14 +3267,6 @@
|
||||||
minimumVersion = 1.0.1;
|
minimumVersion = 1.0.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/karwa/swift-url";
|
|
||||||
requirement = {
|
|
||||||
kind = exactVersion;
|
|
||||||
version = 0.4.2;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
@ -3327,11 +3304,6 @@
|
||||||
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
|
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
|
||||||
productName = HTMLStreamer;
|
productName = HTMLStreamer;
|
||||||
};
|
};
|
||||||
D630C4242BC7845800208903 /* WebURL */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
|
|
||||||
productName = WebURL;
|
|
||||||
};
|
|
||||||
D635237029B78A7D009ED5E7 /* TuskerComponents */ = {
|
D635237029B78A7D009ED5E7 /* TuskerComponents */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = TuskerComponents;
|
productName = TuskerComponents;
|
||||||
|
@ -3350,11 +3322,6 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = TTTKit;
|
productName = TTTKit;
|
||||||
};
|
};
|
||||||
D6676CA427A8D0020052936B /* WebURLFoundationExtras */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
|
|
||||||
productName = WebURLFoundationExtras;
|
|
||||||
};
|
|
||||||
D674A50827F9128D00BA03AC /* Pachyderm */ = {
|
D674A50827F9128D00BA03AC /* Pachyderm */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Pachyderm;
|
productName = Pachyderm;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
@objc(FollowedHashtag)
|
@objc(FollowedHashtag)
|
||||||
public final class FollowedHashtag: NSManagedObject {
|
public final class FollowedHashtag: NSManagedObject {
|
||||||
|
@ -33,6 +32,6 @@ extension FollowedHashtag {
|
||||||
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
||||||
self.init(context: context)
|
self.init(context: context)
|
||||||
self.name = hashtag.name
|
self.name = hashtag.name
|
||||||
self.url = URL(hashtag.url)!
|
self.url = hashtag.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -375,13 +375,14 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer, @unchecked Se
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
|
func addAll(notifications: [Pachyderm.Notification], in context: NSManagedObjectContext? = nil, completion: (() -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
let context = context ?? backgroundContext
|
||||||
|
context.perform {
|
||||||
let statuses = notifications.compactMap { $0.status }
|
let statuses = notifications.compactMap { $0.status }
|
||||||
let accounts = notifications.map { $0.account }
|
let accounts = notifications.map { $0.account }
|
||||||
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, context: context) }
|
||||||
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
accounts.forEach { self.upsert(account: $0, in: context) }
|
||||||
self.save(context: self.backgroundContext)
|
self.save(context: context)
|
||||||
completion?()
|
completion?()
|
||||||
statuses.forEach { self.statusSubject.send($0.id) }
|
statuses.forEach { self.statusSubject.send($0.id) }
|
||||||
accounts.forEach { self.accountSubject.send($0.id) }
|
accounts.forEach { self.accountSubject.send($0.id) }
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
|
|
||||||
@objc(SavedHashtag)
|
@objc(SavedHashtag)
|
||||||
|
@ -42,6 +41,6 @@ extension SavedHashtag {
|
||||||
self.init(context: context)
|
self.init(context: context)
|
||||||
self.accountID = account.id
|
self.accountID = account.id
|
||||||
self.name = hashtag.name
|
self.name = hashtag.name
|
||||||
self.url = URL(hashtag.url)!
|
self.url = hashtag.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
@objc(StatusMO)
|
@objc(StatusMO)
|
||||||
public final class StatusMO: NSManagedObject, StatusProtocol {
|
public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
|
@ -136,7 +135,7 @@ extension StatusMO {
|
||||||
self.sensitive = status.sensitive
|
self.sensitive = status.sensitive
|
||||||
self.spoilerText = status.spoilerText
|
self.spoilerText = status.spoilerText
|
||||||
self.uri = status.uri
|
self.uri = status.uri
|
||||||
self.url = status.url != nil ? URL(status.url!) : nil
|
self.url = status.url
|
||||||
self.visibility = status.visibility
|
self.visibility = status.visibility
|
||||||
self.poll = status.poll
|
self.poll = status.poll
|
||||||
self.localOnly = status.localOnly ?? false
|
self.localOnly = status.localOnly ?? false
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
import WebURL
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class HTMLConverter {
|
class HTMLConverter {
|
||||||
|
|
||||||
|
@ -45,18 +44,7 @@ extension HTMLConverter {
|
||||||
// note: this is duplicated in NotificationExtension
|
// note: this is duplicated in NotificationExtension
|
||||||
struct Callbacks: HTMLConversionCallbacks {
|
struct Callbacks: HTMLConversionCallbacks {
|
||||||
static func makeURL(string: String) -> URL? {
|
static func makeURL(string: String) -> URL? {
|
||||||
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
try? URL.ParseStrategy().parse(string)
|
||||||
// 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.
|
|
||||||
if #available(iOS 16.0, macOS 13.0, *),
|
|
||||||
let url = try? URL.ParseStrategy().parse(string) {
|
|
||||||
url
|
|
||||||
} else if let web = WebURL(string),
|
|
||||||
let url = URL(web) {
|
|
||||||
url
|
|
||||||
} else {
|
|
||||||
URL(string: string)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURL
|
|
||||||
|
|
||||||
class AnnouncementContentTextView: ContentTextView {
|
class AnnouncementContentTextView: ContentTextView {
|
||||||
|
|
||||||
|
@ -30,7 +29,7 @@ class AnnouncementContentTextView: ContentTextView {
|
||||||
|
|
||||||
override func getMention(for url: URL, text: String) -> Mention? {
|
override func getMention(for url: URL, text: String) -> Mention? {
|
||||||
announcement?.mentions.first {
|
announcement?.mentions.first {
|
||||||
URL($0.url) == url
|
$0.url == url
|
||||||
}.map {
|
}.map {
|
||||||
Mention(url: $0.url, username: $0.username, acct: $0.acct, id: $0.id)
|
Mention(url: $0.url, username: $0.username, acct: $0.acct, id: $0.id)
|
||||||
}
|
}
|
||||||
|
@ -38,7 +37,7 @@ class AnnouncementContentTextView: ContentTextView {
|
||||||
|
|
||||||
override func getHashtag(for url: URL, text: String) -> Hashtag? {
|
override func getHashtag(for url: URL, text: String) -> Hashtag? {
|
||||||
announcement?.tags.first {
|
announcement?.tags.first {
|
||||||
URL($0.url) == url
|
$0.url == url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import TuskerComponents
|
import TuskerComponents
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
struct AnnouncementListRow: View {
|
struct AnnouncementListRow: View {
|
||||||
@Binding var announcement: Announcement
|
@Binding var announcement: Announcement
|
||||||
|
@ -20,14 +19,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 +49,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)
|
||||||
|
@ -124,8 +115,8 @@ struct AnnouncementListRow: View {
|
||||||
let url: URL?
|
let url: URL?
|
||||||
let staticURL: URL?
|
let staticURL: URL?
|
||||||
if case .custom(let emoji) = reaction {
|
if case .custom(let emoji) = reaction {
|
||||||
url = URL(emoji.url)
|
url = emoji.url
|
||||||
staticURL = URL(emoji.staticURL)
|
staticURL = emoji.staticURL
|
||||||
} else {
|
} else {
|
||||||
url = nil
|
url = nil
|
||||||
staticURL = nil
|
staticURL = nil
|
||||||
|
|
|
@ -8,8 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURL
|
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
private let mastodonRemoteStatusRegex = try! NSRegularExpression(pattern: "^/@.+@.+/\\d{18}")
|
private let mastodonRemoteStatusRegex = try! NSRegularExpression(pattern: "^/@.+@.+/\\d{18}")
|
||||||
private func isLikelyMastodonRemoteStatus(url: URL) -> Bool {
|
private func isLikelyMastodonRemoteStatus(url: URL) -> Bool {
|
||||||
|
@ -229,10 +227,10 @@ class ConversationViewController: UIViewController {
|
||||||
let location = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "location") {
|
let location = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "location") {
|
||||||
effectiveURL = location
|
effectiveURL = location
|
||||||
} else {
|
} else {
|
||||||
effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
|
effectiveURL = url.formatted(.url.fragment(.never))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
|
effectiveURL = url.formatted(.url.fragment(.never))
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true)
|
let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true)
|
||||||
|
@ -484,3 +482,11 @@ extension ConversationViewController: StatusBarTappableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ConversationViewController: RefreshableViewController {
|
||||||
|
func refresh() {
|
||||||
|
Task {
|
||||||
|
await refreshContext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -10,7 +10,6 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import CoreData
|
import CoreData
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
|
class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
|
||||||
|
|
||||||
|
@ -560,10 +559,7 @@ extension ExploreViewController: UICollectionViewDragDelegate {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider = NSItemProvider(object: activity)
|
provider = NSItemProvider(object: activity)
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
guard let url = URL(hashtag.url) else {
|
provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
return []
|
|
||||||
}
|
|
||||||
provider = NSItemProvider(object: url as NSURL)
|
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class TrendingHashtagsViewController: UIViewController, CollectionViewController {
|
class TrendingHashtagsViewController: UIViewController, CollectionViewController {
|
||||||
|
@ -277,11 +276,10 @@ extension TrendingHashtagsViewController: UICollectionViewDelegate {
|
||||||
extension TrendingHashtagsViewController: UICollectionViewDragDelegate {
|
extension TrendingHashtagsViewController: UICollectionViewDragDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
case let .tag(hashtag) = item,
|
case let .tag(hashtag) = item else {
|
||||||
let url = URL(hashtag.url) else {
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: url as NSURL)
|
let provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
|
|
||||||
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
|
@ -71,7 +70,7 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
self.card = card
|
self.card = card
|
||||||
self.thumbnailView.image = nil
|
self.thumbnailView.image = nil
|
||||||
|
|
||||||
thumbnailView.update(for: card.image.flatMap { URL($0) }, blurhash: card.blurhash)
|
thumbnailView.update(for: card.image, blurhash: card.blurhash)
|
||||||
|
|
||||||
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
titleLabel.text = title
|
titleLabel.text = title
|
||||||
|
|
|
@ -9,22 +9,17 @@
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
|
|
||||||
struct TrendingLinkCardView: View {
|
struct TrendingLinkCardView: View {
|
||||||
let card: Card
|
let card: Card
|
||||||
|
|
||||||
private var imageURL: URL? {
|
private var imageURL: URL? {
|
||||||
if let image = card.image {
|
card.image
|
||||||
URL(image)
|
|
||||||
} else {
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var descriptionText: String {
|
private var descriptionText: String {
|
||||||
var converter = TextConverter(configuration: .init(insertNewlines: false))
|
let converter = TextConverter(configuration: .init(insertNewlines: false))
|
||||||
return converter.convert(html: card.description)
|
return converter.convert(html: card.description)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import Combine
|
import Combine
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
|
@ -293,21 +292,19 @@ extension TrendingLinksViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
|
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
let url = URL(card.url) else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selected(url: url)
|
selected(url: card.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
|
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
|
||||||
let url = URL(card.url),
|
|
||||||
let cell = collectionView.cellForItem(at: indexPath) else {
|
let cell = collectionView.cellForItem(at: indexPath) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
let vc = SFSafariViewController(url: url)
|
let vc = SFSafariViewController(url: card.url)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
||||||
#endif
|
#endif
|
||||||
|
@ -324,11 +321,10 @@ extension TrendingLinksViewController: UICollectionViewDelegate {
|
||||||
|
|
||||||
extension TrendingLinksViewController: UICollectionViewDragDelegate {
|
extension TrendingLinksViewController: UICollectionViewDragDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
|
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
let url = URL(card.url) else {
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
|
return [UIDragItem(itemProvider: NSItemProvider(object: card.url as NSURL))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -513,9 +513,7 @@ extension TrendsViewController: UICollectionViewDelegate {
|
||||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
case let .link(card):
|
case let .link(card):
|
||||||
if let url = URL(card.url) {
|
selected(url: card.url)
|
||||||
selected(url: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
case let .status(id, state):
|
case let .status(id, state):
|
||||||
selected(status: id, state: state.copy())
|
selected(status: id, state: state.copy())
|
||||||
|
@ -525,12 +523,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(_):
|
||||||
|
@ -544,12 +542,9 @@ extension TrendsViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .link(card):
|
case let .link(card):
|
||||||
guard let url = URL(card.url) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let cell = collectionView.cellForItem(at: indexPath)!
|
let cell = collectionView.cellForItem(at: indexPath)!
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
let vc = SFSafariViewController(url: url)
|
let vc = SFSafariViewController(url: card.url)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
||||||
#endif
|
#endif
|
||||||
|
@ -584,15 +579,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)
|
||||||
}
|
}
|
||||||
|
@ -633,10 +619,7 @@ extension TrendsViewController: UICollectionViewDragDelegate {
|
||||||
return []
|
return []
|
||||||
|
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
guard let url = URL(hashtag.url) else {
|
let provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
return []
|
|
||||||
}
|
|
||||||
let provider = NSItemProvider(object: url as NSURL)
|
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
@ -644,10 +627,7 @@ extension TrendsViewController: UICollectionViewDragDelegate {
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
|
|
||||||
case let .link(card):
|
case let .link(card):
|
||||||
guard let url = URL(card.url) else {
|
return [UIDragItem(itemProvider: NSItemProvider(object: card.url as NSURL))]
|
||||||
return []
|
|
||||||
}
|
|
||||||
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
|
|
||||||
|
|
||||||
case let .status(id, _):
|
case let .status(id, _):
|
||||||
guard let status = mastodonController.persistentContainer.status(for: id),
|
guard let status = mastodonController.persistentContainer.status(for: id),
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
import WebURL
|
|
||||||
|
|
||||||
class FastSwitchingAccountView: UIView {
|
class FastSwitchingAccountView: UIView {
|
||||||
|
|
||||||
|
@ -131,11 +130,7 @@ class FastSwitchingAccountView: UIView {
|
||||||
|
|
||||||
private func setupAccount(account: UserAccountInfo) {
|
private func setupAccount(account: UserAccountInfo) {
|
||||||
usernameLabel.text = account.username
|
usernameLabel.text = account.username
|
||||||
if let domain = WebURL.Domain(account.instanceURL.host!) {
|
instanceLabel.text = account.instanceURL.host(percentEncoded: false)
|
||||||
instanceLabel.text = domain.render(.uncheckedUnicodeString)
|
|
||||||
} else {
|
|
||||||
instanceLabel.text = account.instanceURL.host!
|
|
||||||
}
|
|
||||||
let controller = MastodonController.getForAccount(account)
|
let controller = MastodonController.getForAccount(account)
|
||||||
avatarTask = Task {
|
avatarTask = Task {
|
||||||
guard let account = try? await controller.getOwnAccount(),
|
guard let account = try? await controller.getOwnAccount(),
|
||||||
|
|
|
@ -69,4 +69,8 @@ class GifvGalleryContentViewController: UIViewController, GalleryContentViewCont
|
||||||
[VideoActivityItemSource(asset: controller.item.asset, url: url)]
|
[VideoActivityItemSource(asset: controller.item.asset, url: url)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var presentationAnimation: GalleryContentPresentationAnimation {
|
||||||
|
.fromSourceViewWithoutSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
//
|
||||||
|
// GrayscalableImageGalleryContentViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/21/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import TuskerComponents
|
||||||
|
import GalleryVC
|
||||||
|
|
||||||
|
class GrayscalableImageGalleryContentViewController: GalleryVC.ImageGalleryContentViewController {
|
||||||
|
private let url: URL
|
||||||
|
private let originalImage: UIImage
|
||||||
|
private let originalData: Data?
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
|
init(url: URL, caption: String?, originalData: Data?, image: UIImage, gifController: GIFController?) {
|
||||||
|
self.url = url
|
||||||
|
self.originalImage = image
|
||||||
|
self.originalData = originalData
|
||||||
|
|
||||||
|
super.init(image: image, caption: caption, gifController: gifController)
|
||||||
|
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
if isGrayscale {
|
||||||
|
self.image = ImageGrayscalifier.convert(url: url, image: image) ?? image
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
let image = if isGrayscale {
|
||||||
|
ImageGrayscalifier.convert(url: url, image: originalImage)
|
||||||
|
} else {
|
||||||
|
originalImage
|
||||||
|
}
|
||||||
|
if let image {
|
||||||
|
self.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var activityItemsForSharing: [Any] {
|
||||||
|
if let data = originalData ?? image.pngData() {
|
||||||
|
return [ImageActivityItemSource(data: data, url: url, image: image)]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
//
|
||||||
|
// GrayscalableVideoGalleryContentViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/21/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class GrayscalableVideoGalleryContentViewController: GalleryVC.VideoGalleryContentViewController {
|
||||||
|
private var audioSessionToken: AudioSessionCoordinator.Token?
|
||||||
|
private var isGrayscale: Bool
|
||||||
|
private var isFirstAppearance = true
|
||||||
|
|
||||||
|
override init(url: URL, caption: String?) {
|
||||||
|
self.isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
|
super.init(url: url, caption: caption)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override class func createItem(asset: AVAsset) -> AVPlayerItem {
|
||||||
|
let item = AVPlayerItem(asset: asset)
|
||||||
|
if Preferences.shared.grayscaleImages {
|
||||||
|
#if os(visionOS)
|
||||||
|
#warning("Use async AVVideoComposition CIFilter initializer")
|
||||||
|
#else
|
||||||
|
let filter = CIFilter(name: "CIColorMonochrome")!
|
||||||
|
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
|
||||||
|
filter.setValue(1.0, forKey: "inputIntensity")
|
||||||
|
|
||||||
|
item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
|
||||||
|
filter.setValue(request.sourceImage, forKey: "inputImage")
|
||||||
|
request.finish(with: filter.outputImage!, context: nil)
|
||||||
|
})
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
let isPlaying = player.rate > 0
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
replaceCurrentItem(with: Self.createItem(asset: item.asset))
|
||||||
|
if isPlaying {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func galleryContentDidAppear() {
|
||||||
|
super.galleryContentDidAppear()
|
||||||
|
|
||||||
|
let wasFirstAppearance = isFirstAppearance
|
||||||
|
isFirstAppearance = false
|
||||||
|
|
||||||
|
audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .video) {
|
||||||
|
if wasFirstAppearance {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func galleryContentWillDisappear() {
|
||||||
|
super.galleryContentWillDisappear()
|
||||||
|
|
||||||
|
if let audioSessionToken {
|
||||||
|
AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ class ImageGalleryDataSource: GalleryDataSource {
|
||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
return ImageGalleryContentViewController(
|
return GrayscalableImageGalleryContentViewController(
|
||||||
url: url,
|
url: url,
|
||||||
caption: nil,
|
caption: nil,
|
||||||
originalData: entry.data,
|
originalData: entry.data,
|
||||||
|
@ -52,7 +52,7 @@ class ImageGalleryDataSource: GalleryDataSource {
|
||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
return ImageGalleryContentViewController(
|
return GrayscalableImageGalleryContentViewController(
|
||||||
url: self.url,
|
url: self.url,
|
||||||
caption: nil,
|
caption: nil,
|
||||||
originalData: data,
|
originalData: data,
|
||||||
|
|
|
@ -33,7 +33,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
case .image:
|
case .image:
|
||||||
if let view = attachmentView(for: attachment),
|
if let view = attachmentView(for: attachment),
|
||||||
let image = view.attachmentImage {
|
let image = view.attachmentImage {
|
||||||
return ImageGalleryContentViewController(
|
return GrayscalableImageGalleryContentViewController(
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
caption: attachment.description,
|
caption: attachment.description,
|
||||||
originalData: view.originalData,
|
originalData: view.originalData,
|
||||||
|
@ -49,7 +49,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
return ImageGalleryContentViewController(
|
return GrayscalableImageGalleryContentViewController(
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
caption: attachment.description,
|
caption: attachment.description,
|
||||||
originalData: entry.data,
|
originalData: entry.data,
|
||||||
|
@ -68,7 +68,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
return ImageGalleryContentViewController(
|
return GrayscalableImageGalleryContentViewController(
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
caption: attachment.description,
|
caption: attachment.description,
|
||||||
originalData: data,
|
originalData: data,
|
||||||
|
@ -91,10 +91,10 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
}
|
}
|
||||||
return GifvGalleryContentViewController(controller: controller, url: attachment.url, caption: attachment.description)
|
return GifvGalleryContentViewController(controller: controller, url: attachment.url, caption: attachment.description)
|
||||||
case .video:
|
case .video:
|
||||||
return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
return GrayscalableVideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
||||||
case .audio:
|
case .audio:
|
||||||
// TODO: use separate content VC with audio visualization?
|
// TODO: use separate content VC with audio visualization?
|
||||||
return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
return GrayscalableVideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
||||||
case .unknown:
|
case .unknown:
|
||||||
return LoadingGalleryContentViewController(caption: nil) {
|
return LoadingGalleryContentViewController(caption: nil) {
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
]))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -151,6 +151,22 @@ class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewC
|
||||||
return false
|
return false
|
||||||
#endif // !os(visionOS)
|
#endif // !os(visionOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Keyboard shortcuts
|
||||||
|
|
||||||
|
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
|
||||||
|
// This is a silly workaround for when the sidebar is focused (and therefore first responder), which,
|
||||||
|
// unfortunately, is almost always. Because the content view controller then isn't in the responder chain,
|
||||||
|
// we manually delegate to the top view controller if possible.
|
||||||
|
if action == #selector(RefreshableViewController.refresh),
|
||||||
|
let selected = selectedViewController as? NavigationControllerProtocol,
|
||||||
|
let top = selected.topViewController as? RefreshableViewController {
|
||||||
|
return top
|
||||||
|
} else {
|
||||||
|
return super.target(forAction: action, withSender: sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
|
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
|
||||||
|
|
|
@ -219,6 +219,19 @@ class MainSplitViewController: UISplitViewController {
|
||||||
@objc func handleComposeKeyCommand() {
|
@objc func handleComposeKeyCommand() {
|
||||||
compose(editing: nil)
|
compose(editing: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
|
||||||
|
// This is a silly workaround for when the sidebar is focused (and therefore first responder), which,
|
||||||
|
// unfortunately, is almost always. Because the content view controller then isn't in the responder chain,
|
||||||
|
// we manually delegate to the top view controller if possible.
|
||||||
|
if action == #selector(RefreshableViewController.refresh),
|
||||||
|
traitCollection.horizontalSizeClass == .regular,
|
||||||
|
let top = secondaryNavController.topViewController as? RefreshableViewController {
|
||||||
|
return top
|
||||||
|
} else {
|
||||||
|
return super.target(forAction: action, withSender: sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,13 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
|
||||||
private var isCompact: Bool?
|
private var isCompact: Bool?
|
||||||
@Box fileprivate var myProfileCell: UIView?
|
@Box fileprivate var myProfileCell: UIView?
|
||||||
private var sidebarTapRecognizer: UITapGestureRecognizer?
|
private var sidebarTapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
|
private lazy var fastAccountSwitcherIndicator: UIView = {
|
||||||
|
let indicator = FastAccountSwitcherIndicatorView()
|
||||||
|
// need to explicitly set the frame to get it vertically centered
|
||||||
|
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
|
||||||
|
return indicator
|
||||||
|
}()
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
@ -454,15 +461,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 {
|
||||||
|
@ -508,13 +520,6 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fastAccountSwitcherIndicator: UIView = {
|
|
||||||
let indicator = FastAccountSwitcherIndicatorView()
|
|
||||||
// need to explicitly set the frame to get it vertically centered
|
|
||||||
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
|
|
||||||
return indicator
|
|
||||||
}()
|
|
||||||
|
|
||||||
@available(iOS 18.0, *)
|
@available(iOS 18.0, *)
|
||||||
extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
|
extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
|
||||||
func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) {
|
func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -155,7 +155,7 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
||||||
fetchCustomEmojiImage?.1.cancel()
|
fetchCustomEmojiImage?.1.cancel()
|
||||||
case .emojiReaction(let emojiOrShortcode, let url):
|
case .emojiReaction(let emojiOrShortcode, let url):
|
||||||
iconImageView.image = nil
|
iconImageView.image = nil
|
||||||
if let url = url.flatMap({ URL($0) }),
|
if let url,
|
||||||
fetchCustomEmojiImage?.0 != url {
|
fetchCustomEmojiImage?.0 != url {
|
||||||
fetchCustomEmojiImage?.1.cancel()
|
fetchCustomEmojiImage?.1.cancel()
|
||||||
let task = Task {
|
let task = Task {
|
||||||
|
|
|
@ -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))),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,9 @@ class NotificationLoadingViewController: UIViewController {
|
||||||
do {
|
do {
|
||||||
let (notification, _) = try await mastodonController.run(request)
|
let (notification, _) = try await mastodonController.run(request)
|
||||||
await withCheckedContinuation { continuation in
|
await withCheckedContinuation { continuation in
|
||||||
mastodonController.persistentContainer.addAll(notifications: [notification]) {
|
let container = mastodonController.persistentContainer
|
||||||
|
let context = container.viewContext
|
||||||
|
container.addAll(notifications: [notification], in: context) {
|
||||||
continuation.resume()
|
continuation.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -746,7 +740,7 @@ extension NotificationsCollectionViewController: UICollectionViewDragDelegate {
|
||||||
return cell.dragItemsForBeginning(session: session)
|
return cell.dragItemsForBeginning(session: session)
|
||||||
case .poll, .update:
|
case .poll, .update:
|
||||||
let status = group.notifications.first!.status!
|
let status = group.notifications.first!.status!
|
||||||
let provider = NSItemProvider(object: URL(status.url!)! as NSURL)
|
let provider = NSItemProvider(object: status.url! as NSURL)
|
||||||
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: mastodonController.accountInfo!.id)
|
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: mastodonController.accountInfo!.id)
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
|
|
@ -180,3 +180,9 @@ extension NotificationsPageViewController: StateRestorableViewController {
|
||||||
return currentPage.userActivity(accountID: mastodonController.accountInfo!.id)
|
return currentPage.userActivity(accountID: mastodonController.accountInfo!.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NotificationsPageViewController: RefreshableViewController {
|
||||||
|
func refresh() {
|
||||||
|
(currentViewController as? RefreshableViewController)?.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURL
|
|
||||||
|
|
||||||
struct MockStatusView: View {
|
struct MockStatusView: View {
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
@ObservedObject private var preferences = Preferences.shared
|
||||||
|
@ -136,8 +135,8 @@ private struct MockStatusCardView: UIViewRepresentable {
|
||||||
let view = StatusCardView()
|
let view = StatusCardView()
|
||||||
view.isUserInteractionEnabled = false
|
view.isUserInteractionEnabled = false
|
||||||
let card = StatusCardView.CardData(
|
let card = StatusCardView.CardData(
|
||||||
url: WebURL("https://vaccor.space/tusker")!,
|
url: URL(string: "https://vaccor.space/tusker")!,
|
||||||
image: WebURL("https://vaccor.space/tusker/img/icon.png")!,
|
image: URL(string: "https://vaccor.space/tusker/img/icon.png")!,
|
||||||
title: "Tusker",
|
title: "Tusker",
|
||||||
description: "Tusker is an iOS app for Mastodon"
|
description: "Tusker is an iOS app for Mastodon"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
import WebURL
|
|
||||||
|
|
||||||
struct PrefsAccountView: View {
|
struct PrefsAccountView: View {
|
||||||
let account: UserAccountInfo
|
let account: UserAccountInfo
|
||||||
|
@ -19,12 +18,7 @@ struct PrefsAccountView: View {
|
||||||
VStack(alignment: .prefsAvatar) {
|
VStack(alignment: .prefsAvatar) {
|
||||||
Text(verbatim: account.username)
|
Text(verbatim: account.username)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
let instance = if let domain = WebURL.Domain(account.instanceURL.host!) {
|
Text(verbatim: account.instanceURL.host(percentEncoded: false)!)
|
||||||
domain.render(.uncheckedUnicodeString)
|
|
||||||
} else {
|
|
||||||
account.instanceURL.host!
|
|
||||||
}
|
|
||||||
Text(verbatim: instance)
|
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue