Add iOS platform service boundaries
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
#if canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public protocol ClipboardWriting: Sendable {
|
||||
func write(text: String) async
|
||||
}
|
||||
|
||||
public struct SystemClipboardWriter: ClipboardWriting {
|
||||
public init() {}
|
||||
|
||||
public func write(text: String) async {
|
||||
#if os(iOS) && canImport(UIKit)
|
||||
await MainActor.run {
|
||||
UIPasteboard.general.string = text
|
||||
}
|
||||
#elseif os(macOS) && canImport(AppKit)
|
||||
await MainActor.run {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(text, forType: .string)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public actor InMemoryClipboardWriter: ClipboardWriting {
|
||||
public private(set) var lastText: String?
|
||||
|
||||
public init() {}
|
||||
|
||||
public func write(text: String) async {
|
||||
lastText = text
|
||||
}
|
||||
|
||||
public func currentText() async -> String? {
|
||||
lastText
|
||||
}
|
||||
}
|
||||
|
||||
public struct NotificationPolicy: Sendable {
|
||||
public init() {}
|
||||
|
||||
public func shouldNotify(chat: ChatSummary, message: Message, mentionOnly: Bool) -> Bool {
|
||||
guard !message.isOutgoing, !chat.isMuted else {
|
||||
return false
|
||||
}
|
||||
if mentionOnly {
|
||||
return message.text.contains("@")
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public protocol NotificationScheduling: Sendable {
|
||||
func schedule(chat: ChatSummary, message: Message) async throws
|
||||
}
|
||||
|
||||
public actor RecordingNotificationScheduler: NotificationScheduling {
|
||||
public private(set) var scheduled: [(ChatSummary, Message)] = []
|
||||
|
||||
public init() {}
|
||||
|
||||
public func schedule(chat: ChatSummary, message: Message) async throws {
|
||||
scheduled.append((chat, message))
|
||||
}
|
||||
}
|
||||
|
||||
public protocol URLOpening: Sendable {
|
||||
func open(_ url: URL) async
|
||||
}
|
||||
|
||||
public struct SystemURLOpener: URLOpening {
|
||||
public init() {}
|
||||
|
||||
public func open(_ url: URL) async {
|
||||
#if os(iOS) && canImport(UIKit)
|
||||
await MainActor.run {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
#elseif os(macOS) && canImport(AppKit)
|
||||
await MainActor.run {
|
||||
_ = NSWorkspace.shared.open(url)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public struct MediaCache: Sendable {
|
||||
public var root: URL
|
||||
|
||||
public init(root: URL) {
|
||||
self.root = root
|
||||
}
|
||||
|
||||
public func photoPath(fileId: Int32) -> URL {
|
||||
root.appendingPathComponent("photos", isDirectory: true).appendingPathComponent("\(fileId).jpg")
|
||||
}
|
||||
|
||||
public func voicePath(fileId: Int32) -> URL {
|
||||
root.appendingPathComponent("voices", isDirectory: true).appendingPathComponent("\(fileId).ogg")
|
||||
}
|
||||
}
|
||||
|
||||
public protocol VoicePlayback: Sendable {
|
||||
func load(url: URL) async throws
|
||||
func play() async
|
||||
func pause() async
|
||||
func seek(to seconds: TimeInterval) async
|
||||
}
|
||||
|
||||
public actor RecordingVoicePlayer: VoicePlayback {
|
||||
public private(set) var loadedURL: URL?
|
||||
public private(set) var isPlaying = false
|
||||
public private(set) var position: TimeInterval = 0
|
||||
|
||||
public init() {}
|
||||
|
||||
public func load(url: URL) async throws {
|
||||
loadedURL = url
|
||||
position = 0
|
||||
}
|
||||
|
||||
public func currentLoadedURL() async -> URL? {
|
||||
loadedURL
|
||||
}
|
||||
|
||||
public func play() async {
|
||||
isPlaying = true
|
||||
}
|
||||
|
||||
public func pause() async {
|
||||
isPlaying = false
|
||||
}
|
||||
|
||||
public func seek(to seconds: TimeInterval) async {
|
||||
position = seconds
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public final class AccountSwitcherViewModel: ObservableObject {
|
||||
@Published public private(set) var accounts: [Account]
|
||||
@Published public private(set) var activeAccount: Account
|
||||
|
||||
public init(accounts: [Account], activeAccount: Account) {
|
||||
self.accounts = accounts
|
||||
self.activeAccount = activeAccount
|
||||
}
|
||||
|
||||
public func switchToAccount(id: String) {
|
||||
guard let next = accounts.first(where: { $0.id == id }) else {
|
||||
return
|
||||
}
|
||||
activeAccount = next
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,13 @@ public struct AppStoragePaths: Sendable {
|
||||
.appendingPathComponent(accountId, isDirectory: true)
|
||||
.appendingPathComponent("tdlib", isDirectory: true)
|
||||
}
|
||||
|
||||
public func mediaCachePath(for accountId: String) -> URL {
|
||||
root
|
||||
.appendingPathComponent("Accounts", isDirectory: true)
|
||||
.appendingPathComponent(accountId, isDirectory: true)
|
||||
.appendingPathComponent("Media", isDirectory: true)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CredentialStore: Sendable {
|
||||
|
||||
@@ -264,8 +264,15 @@ public final class ProfileViewModel: ObservableObject {
|
||||
public final class MediaViewModel: ObservableObject {
|
||||
@Published public private(set) var activePhotoPath: String?
|
||||
@Published public private(set) var activeVoicePath: String?
|
||||
@Published public private(set) var isVoicePlaying = false
|
||||
|
||||
public init() {}
|
||||
private let cache: MediaCache?
|
||||
private let voicePlayer: VoicePlayback?
|
||||
|
||||
public init(cache: MediaCache? = nil, voicePlayer: VoicePlayback? = nil) {
|
||||
self.cache = cache
|
||||
self.voicePlayer = voicePlayer
|
||||
}
|
||||
|
||||
public func showPhoto(path: String) {
|
||||
activePhotoPath = path
|
||||
@@ -274,4 +281,27 @@ public final class MediaViewModel: ObservableObject {
|
||||
public func showVoice(path: String) {
|
||||
activeVoicePath = path
|
||||
}
|
||||
|
||||
public func cachedPhotoPath(fileId: Int32) -> URL? {
|
||||
cache?.photoPath(fileId: fileId)
|
||||
}
|
||||
|
||||
public func cachedVoicePath(fileId: Int32) -> URL? {
|
||||
cache?.voicePath(fileId: fileId)
|
||||
}
|
||||
|
||||
public func playVoice(url: URL) async {
|
||||
do {
|
||||
try await voicePlayer?.load(url: url)
|
||||
await voicePlayer?.play()
|
||||
isVoicePlaying = true
|
||||
} catch {
|
||||
isVoicePlaying = false
|
||||
}
|
||||
}
|
||||
|
||||
public func pauseVoice() async {
|
||||
await voicePlayer?.pause()
|
||||
isVoicePlaying = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user