diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..3d45b78 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,33 @@ +name: Build + +# Compile-only verification, no signing/notarization/tests. Runs on every +# push and PR so a branch (main, a PR branch, a personal-build branch, +# whatever) shows red/green on GitHub before anyone has to open Xcode. +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + workflow_dispatch: + +jobs: + build: + runs-on: macos-14 + + steps: + - name: Check out repo (with submodules) + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Show Xcode version + run: xcodebuild -version + + - name: Build Subler + run: | + xcodebuild \ + -project Subler.xcodeproj \ + -target Subler \ + -configuration Debug \ + CODE_SIGNING_ALLOWED=NO \ + build diff --git a/Classes/Document.swift b/Classes/Document.swift index 7c1ac11..5e53292 100644 --- a/Classes/Document.swift +++ b/Classes/Document.swift @@ -110,6 +110,26 @@ final class Document: NSDocument { save(self) } + /// Whether the next completed save should prompt to open another file + /// once it finishes (see saveAndOpen(_:) below). Consumed and reset to + /// false as soon as that save finishes, so a plain save triggered + /// afterwards (Cmd-S, etc.) doesn't also bring up the Open panel. + private var openAfterSave: Bool = false + + /// Saves the current document, then -- once that finishes -- brings up + /// the same Open panel Subler shows an untitled launch with + /// (AppDelegate.applicationShouldOpenUntitledFile(_:) / + /// NSDocumentController.openDocument(_:)), so picking up the next file + /// in a batch never needs a trip to File > Open. This previously + /// opened the just-saved file in its default external application + /// instead (e.g. QuickTime) -- a different, unrelated feature that + /// didn't serve the actual goal here: an easy way to move on to + /// another file once the current one is done. + @IBAction func saveAndOpen(_ sender: Any) { + openAfterSave = true + save(self) + } + override func canAsynchronouslyWrite(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType) -> Bool { return true } override func save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, completionHandler: @escaping (Error?) -> Void) { @@ -125,6 +145,10 @@ final class Document: NSDocument { let reloadedFile = try MP42File(url: url) self.mp4 = reloadedFile docController?.reloadData() + if self.openAfterSave { + self.openAfterSave = false + NSDocumentController.shared.openDocument(self) + } completionHandler(error) } catch { completionHandler(error) @@ -260,6 +284,8 @@ final class Document: NSDocument { return true case #selector(save(_:)) where isDocumentEdited == true: return true + case #selector(saveAndOpen(_:)) where isDocumentEdited == true: + return true case #selector(saveAs(_:)), #selector(sendToQueue(_:)): return true diff --git a/Classes/DocumentToolbarDelegate.swift b/Classes/DocumentToolbarDelegate.swift index b8dc630..ae79a2b 100644 --- a/Classes/DocumentToolbarDelegate.swift +++ b/Classes/DocumentToolbarDelegate.swift @@ -14,6 +14,7 @@ private extension NSToolbarItem.Identifier { static let searchMetadata: NSToolbarItem.Identifier = NSToolbarItem.Identifier(rawValue: "SearchMetadata") static let sendToQueue: NSToolbarItem.Identifier = NSToolbarItem.Identifier(rawValue: "SendToQueue") static let showQueue: NSToolbarItem.Identifier = NSToolbarItem.Identifier(rawValue: "ShowQueue") + static let saveAndOpen: NSToolbarItem.Identifier = NSToolbarItem.Identifier(rawValue: "SaveAndOpen") } class DocumentToolbarDelegate: NSObject, NSToolbarDelegate { @@ -143,17 +144,25 @@ class DocumentToolbarDelegate: NSObject, NSToolbarDelegate { symbolName: "photo.stack", target: nil, action: #selector(AppDelegate.showQueueWindow(_:))) + } else if itemIdentifier == .saveAndOpen { + return ButtonToolbarItem(itemIdentifier: itemIdentifier, + label: NSLocalizedString("Open and Save", comment: "Toolbar"), + toolTip: NSLocalizedString("Save the document and open the file", comment: "Toolbar"), + image: "NSShareTemplate", + symbolName: "arrow.up.forward.app", + target: target, + action: #selector(Document.saveAndOpen(_:))) } return nil } @MainActor func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [.importTracks, .searchMetadata, .space, .actions, .space, .deleteTracks, .flexibleSpace, .sendToQueue, .showQueue,] + return [.importTracks, .searchMetadata, .space, .actions, .space, .deleteTracks, .flexibleSpace, .sendToQueue, .saveAndOpen, .showQueue,] } @MainActor func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [.importTracks, .deleteTracks, .actions, .searchMetadata, .sendToQueue, .showQueue, .flexibleSpace, .space] + return [.importTracks, .deleteTracks, .actions, .searchMetadata, .sendToQueue, .saveAndOpen, .showQueue, .flexibleSpace, .space] } } diff --git a/Classes/DocumentWindowController.swift b/Classes/DocumentWindowController.swift index f3b0e65..792df3d 100644 --- a/Classes/DocumentWindowController.swift +++ b/Classes/DocumentWindowController.swift @@ -46,7 +46,15 @@ final class DocumentWindowController: NSWindowController, TracksViewControllerDe toolbarDelegate.target = self - let toolbar = NSToolbar(identifier: "SublerDocumentToolbar") + // Identifier bumped (was "SublerDocumentToolbar") so that anyone + // with a toolbar layout already saved from before "Open and + // Save" existed gets the current toolbarDefaultItemIdentifiers() + // set again -- with autosavesConfiguration on, NSToolbar only + // ever falls back to the delegate's defaults for a layout it + // hasn't seen before, otherwise it keeps replaying whatever was + // saved under this exact identifier indefinitely, new default + // items and all. + let toolbar = NSToolbar(identifier: "SublerDocumentToolbar2") toolbar.delegate = toolbarDelegate toolbar.allowsUserCustomization = true toolbar.autosavesConfiguration = true @@ -299,6 +307,18 @@ final class DocumentWindowController: NSWindowController, TracksViewControllerDe doc.sendToQueue(self) } + /// The "Open and Save" toolbar item's target is this window controller + /// (see DocumentToolbarDelegate), not the document, following the same + /// pattern as sendToQueue(_:) above -- so this forwarding method has to + /// exist here under the same selector name for the click to actually + /// reach Document.saveAndOpen(_:). Without it, NSApplication.sendAction + /// silently does nothing when the toolbar item's explicit target + /// doesn't respond to the action, even though menu/toolbar validation + /// (which walks the responder chain) still reports the item as enabled. + @IBAction func saveAndOpen(_ sender: Any) { + doc.saveAndOpen(self) + } + @IBAction func sendToExternalApp(_ sender: Any) { if let fileURL = doc.fileURL { _ = sendToFileExternalApp(fileURL: fileURL)