| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- import AppKit
- import Foundation
- import SwiftData
- import UniformTypeIdentifiers
- /// Watches for sync files from the iOS MixBoard app.
- /// Checks two locations:
- /// 1. A "MixBoard Sync" folder on the Desktop (for manual AirDrop/copy)
- /// 2. The iOS app's iCloud Drive container (if iCloud Drive sync is enabled)
- @MainActor
- final class SyncWatcher: ObservableObject {
- @Published var lastImportDate: Date?
- @Published var lastImportResult: String?
- @Published var hasPendingSync = false
- @Published var pendingSyncURL: URL?
- private var fileMonitor: DispatchSourceFileSystemObject?
- private var timer: Timer?
- private var lastKnownModDate: Date?
- /// All paths to check for sync files.
- var watchPaths: [URL] {
- var paths: [URL] = []
- // 1. Desktop/MixBoard Sync/ (for manual drops)
- let desktop = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
- paths.append(desktop.appendingPathComponent("MixBoard Sync/mixboard-playlists.json"))
- // 2. Documents/MixBoard Sync/
- let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
- paths.append(docs.appendingPathComponent("MixBoard Sync/mixboard-playlists.json"))
- // 3. iCloud Drive MixBoard app container
- // ~/Library/Mobile Documents/iCloud~com~mixboard~MixBoardiOS/Documents/Sync/mixboard-playlists.json
- let home = FileManager.default.homeDirectoryForCurrentUser
- let iCloudPath = home
- .appendingPathComponent("Library/Mobile Documents")
- .appendingPathComponent("com~apple~CloudDocs") // generic iCloud Drive
- .appendingPathComponent("MixBoard/mixboard-playlists.json")
- paths.append(iCloudPath)
- return paths
- }
- /// Start polling for sync files (every 10 seconds).
- func startWatching() {
- timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] _ in
- Task { @MainActor in
- self?.checkForSyncFile()
- }
- }
- // Check immediately
- checkForSyncFile()
- }
- func stopWatching() {
- timer?.invalidate()
- timer = nil
- }
- /// Check all watch paths for a sync file.
- func checkForSyncFile() {
- let fm = FileManager.default
- for url in watchPaths {
- guard fm.fileExists(atPath: url.path) else { continue }
- // Check if file is newer than last import
- if let attrs = try? fm.attributesOfItem(atPath: url.path),
- let modDate = attrs[.modificationDate] as? Date {
- if let lastKnown = lastKnownModDate, modDate <= lastKnown {
- continue // Already processed this version
- }
- lastKnownModDate = modDate
- hasPendingSync = true
- pendingSyncURL = url
- return
- }
- }
- }
- /// Import from a specific sync file.
- func importSyncFile(_ url: URL, context: ModelContext) {
- do {
- let result = try SyncImporter.importFromFile(url, context: context)
- lastImportDate = Date()
- lastImportResult = result.summary
- hasPendingSync = false
- pendingSyncURL = nil
- print("SyncWatcher: \(result.summary)")
- } catch {
- lastImportResult = "Import failed: \(error.localizedDescription)"
- print("SyncWatcher: Import failed: \(error)")
- }
- }
- /// Import from the pending sync file.
- func importPending(context: ModelContext) {
- guard let url = pendingSyncURL else { return }
- importSyncFile(url, context: context)
- }
- /// Show an open panel to pick a sync file manually.
- func importManually(context: ModelContext) {
- let panel = NSOpenPanel()
- panel.title = "Import Playlists from iPhone"
- panel.allowedContentTypes = [.json]
- panel.allowsMultipleSelection = false
- panel.canChooseDirectories = false
- panel.message = "Select the mixboard-playlists.json file exported from MixBoard iOS"
- if panel.runModal() == .OK, let url = panel.url {
- importSyncFile(url, context: context)
- }
- }
- /// Create the sync drop folders so users know where to put the file.
- func createSyncFolders() {
- let fm = FileManager.default
- let desktop = fm.urls(for: .desktopDirectory, in: .userDomainMask).first!
- let syncFolder = desktop.appendingPathComponent("MixBoard Sync")
- try? fm.createDirectory(at: syncFolder, withIntermediateDirectories: true)
- }
- }
|