diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0f5aff5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## Version 0.2 - 2026-06-18 + +### Fixed + +- Opening a PDF from Finder Open With now opens in its own window state instead of mirroring into an existing window. +- Zoom toolbar and menu commands now apply to the focused PDF window instead of another open window. +- Pressing Return in a new comment editor now saves the comment without requiring the mouse. +- Page and comments sidebar toolbar icons remain visible in narrow windows. + +### Changed + +- Sidebar toolbar controls are grouped together in the leading toolbar area for better visibility. + +## Version 0.1 + +- Initial macOS SwiftUI/PDFKit release. +- Local PDF opening, reading, zoom, fit width, fit page, page navigation, and search. +- Highlight, underline, selection-bound comment, free-text annotation, comments sidebar, save, Save As, and share workflows. diff --git a/README.md b/README.md index 75fdb48..70e594c 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ Supported Mac architectures: Apple Silicon and Intel, subject to the local Swift ## Latest Release -Download the v0.1 macOS DMG from the GitHub release page: +Download the v0.2 macOS DMG from the GitHub release page: - + -Use `IHatePDFs-v0.1-macos.dmg` for normal app installation. Open the DMG, then drag `I Hate PDFs.app` into `/Applications`. +Use `IHatePDFs-v0.2-macos.dmg` for normal app installation. Open the DMG, then drag `I Hate PDFs.app` into `/Applications`. -Signing status for v0.1: the DMG is ad-hoc signed, but it is not Developer ID signed or Apple-notarized yet. macOS Gatekeeper may require opening the app from Finder with Control-click, then Open, on first launch. +Signing status for v0.2: the DMG is ad-hoc signed, but it is not Developer ID signed or Apple-notarized yet. macOS Gatekeeper may require opening the app from Finder with Control-click, then Open, on first launch. ## Features @@ -41,7 +41,7 @@ Signing status for v0.1: the DMG is ad-hoc signed, but it is not Developer ID si ### Download Releases -https://github.com/akkolli/ihatepdfs/releases/tag/v0.1 +https://github.com/akkolli/ihatepdfs/releases/tag/v0.2 ## Build From Source @@ -82,13 +82,13 @@ Create a downloadable `.dmg`: scripts/make-dmg.sh ``` -The packaged app is written to `dist/I Hate PDFs.app`; the disk image is written to `dist/IHatePDFs.dmg`. +The packaged app is written to `dist/I Hate PDFs.app`; the disk image is written to `dist/IHatePDFs-v0.2-macos.dmg`. ## Installation -Download `IHatePDFs-v0.1-macos.dmg` from the latest GitHub release, open it, and move `I Hate PDFs.app` into `/Applications`. +Download `IHatePDFs-v0.2-macos.dmg` from the latest GitHub release, open it, and move `I Hate PDFs.app` into `/Applications`. -For v0.1 and local development builds, the app is not Developer ID signed or notarized. If macOS blocks first launch, open Finder, Control-click `I Hate PDFs.app`, choose Open, then confirm. +For v0.2 and local development builds, the app is not Developer ID signed or notarized. If macOS blocks first launch, open Finder, Control-click `I Hate PDFs.app`, choose Open, then confirm. ## Development diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..1ae4a89 --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,8 @@ +# Release Notes + +## Version 0.2 + +- Fixed multi-window document state so opening a PDF with Finder Open With does not mirror it into an existing window. +- Fixed zoom commands so toolbar and menu zoom actions apply to the focused PDF window instead of another open window. +- Fixed comment entry so pressing Return saves a new comment without requiring the mouse. +- Kept the page and comments sidebar toolbar icons visible in narrow windows by grouping sidebar controls in the leading toolbar. diff --git a/Sources/IHatePDFs/CommentEditorView.swift b/Sources/IHatePDFs/CommentEditorView.swift index c22c0c6..e4bfdf1 100644 --- a/Sources/IHatePDFs/CommentEditorView.swift +++ b/Sources/IHatePDFs/CommentEditorView.swift @@ -69,6 +69,9 @@ struct CommentEditorView: View { .onChange(of: model.author) { _ in model.updateDraft() } + .commitOnPlainReturn { + model.commit() + } } private var header: some View { diff --git a/Sources/IHatePDFs/IHatePDFsApp.swift b/Sources/IHatePDFs/IHatePDFsApp.swift index 5628d13..e548783 100644 --- a/Sources/IHatePDFs/IHatePDFsApp.swift +++ b/Sources/IHatePDFs/IHatePDFsApp.swift @@ -3,154 +3,176 @@ import SwiftUI @main struct IHatePDFsApp: App { - @StateObject private var appState = AppState() - var body: some Scene { WindowGroup { - MainView() - .environmentObject(appState) - .onOpenURL { url in - appState.loadDocument(from: url) - } + AppWindowRoot() } .windowStyle(.titleBar) .commands { - CommandGroup(replacing: .newItem) { - Button("Open...") { - appState.openDocument() - } - .keyboardShortcut("o") - - Button("Save") { - appState.saveDocument() - } - .keyboardShortcut("s") - .disabled(appState.document == nil) - - Button("Save As...") { - appState.saveDocumentAs() - } - .keyboardShortcut("s", modifiers: [.command, .shift]) - .disabled(appState.document == nil) - - Button("Share...") { - appState.shareDocument() - } - .keyboardShortcut("e", modifiers: [.command, .shift]) - .disabled(appState.document == nil) - - Divider() - - Button("Close PDF") { - appState.closeDocument() - } - .keyboardShortcut("w") - .disabled(appState.document == nil) - } - - CommandGroup(after: .textEditing) { - Button("Find in PDF") { - appState.showSearch() - } - .keyboardShortcut("f") - .disabled(appState.document == nil) - - Button("Find Next") { - appState.nextSearchResult() - } - .keyboardShortcut("g") - .disabled(appState.searchResults.isEmpty) - - Button("Find Previous") { - appState.previousSearchResult() - } - .keyboardShortcut("g", modifiers: [.command, .shift]) - .disabled(appState.searchResults.isEmpty) - } - - CommandMenu("View") { - Button("Toggle Page Sidebar") { - appState.showLeftSidebar.toggle() - } - .keyboardShortcut("0", modifiers: [.command, .option]) - .disabled(appState.document == nil) - - Button("Toggle Comments Sidebar") { - appState.showCommentsSidebar.toggle() - } - .keyboardShortcut("1", modifiers: [.command, .option]) - .disabled(appState.document == nil) - - Divider() - - Button("Zoom In") { - appState.zoomIn() - } - .keyboardShortcut("+") - .disabled(appState.document == nil) - - Button("Zoom Out") { - appState.zoomOut() - } - .keyboardShortcut("-") - .disabled(appState.document == nil) - - Button("Fit to Width") { - appState.fitWidth() - } - .keyboardShortcut("9", modifiers: [.command]) - .disabled(appState.document == nil) - - Button("Fit to Page") { - appState.fitPage() - } - .keyboardShortcut("8", modifiers: [.command]) - .disabled(appState.document == nil) - - Button("Two Pages Continuous") { - appState.twoPageContinuous() - } - .keyboardShortcut("7", modifiers: [.command]) - .disabled(appState.document == nil) - } - - CommandMenu("Annotate") { - Button("Highlight Selection") { - appState.addHighlight() - } - .keyboardShortcut("h", modifiers: [.command, .shift]) - .disabled(appState.document == nil) - - Button("Underline Selection") { - appState.addUnderline() - } - .keyboardShortcut("u", modifiers: [.command, .shift]) - .disabled(appState.document == nil) - - Button("Comment on Selection") { - appState.addComment() - } - .keyboardShortcut("n", modifiers: [.command, .shift]) - .disabled(appState.document == nil) - - Button("Add Free Text") { - appState.addFreeText() - } - .keyboardShortcut("t", modifiers: [.command, .shift]) - .disabled(appState.document == nil) - } - - CommandGroup(after: .windowArrangement) { - Button("Minimize") { - appState.minimizeWindow() - } - .keyboardShortcut("m", modifiers: [.command]) - - Button("Toggle Full Screen") { - appState.toggleFullScreen() - } - .keyboardShortcut("f", modifiers: [.command, .control]) - } + AppCommands() + } + } +} + +private struct AppWindowRoot: View { + @StateObject private var appState = AppState() + + var body: some View { + MainView() + .environmentObject(appState) + .focusedObject(appState) + .onOpenURL { url in + appState.loadDocument(from: url) + } + } +} + +private struct AppCommands: Commands { + @FocusedObject private var appState: AppState? + + private var hasDocument: Bool { + appState?.document != nil + } + + var body: some Commands { + CommandGroup(replacing: .newItem) { + Button("Open...") { + appState?.openDocument() + } + .keyboardShortcut("o") + .disabled(appState == nil) + + Button("Save") { + appState?.saveDocument() + } + .keyboardShortcut("s") + .disabled(!hasDocument) + + Button("Save As...") { + appState?.saveDocumentAs() + } + .keyboardShortcut("s", modifiers: [.command, .shift]) + .disabled(!hasDocument) + + Button("Share...") { + appState?.shareDocument() + } + .keyboardShortcut("e", modifiers: [.command, .shift]) + .disabled(!hasDocument) + + Divider() + + Button("Close PDF") { + appState?.closeDocument() + } + .keyboardShortcut("w") + .disabled(!hasDocument) + } + + CommandGroup(after: .textEditing) { + Button("Find in PDF") { + appState?.showSearch() + } + .keyboardShortcut("f") + .disabled(!hasDocument) + + Button("Find Next") { + appState?.nextSearchResult() + } + .keyboardShortcut("g") + .disabled(appState?.searchResults.isEmpty != false) + + Button("Find Previous") { + appState?.previousSearchResult() + } + .keyboardShortcut("g", modifiers: [.command, .shift]) + .disabled(appState?.searchResults.isEmpty != false) + } + + CommandMenu("View") { + Button("Toggle Page Sidebar") { + appState?.showLeftSidebar.toggle() + } + .keyboardShortcut("0", modifiers: [.command, .option]) + .disabled(!hasDocument) + + Button("Toggle Comments Sidebar") { + appState?.showCommentsSidebar.toggle() + } + .keyboardShortcut("1", modifiers: [.command, .option]) + .disabled(!hasDocument) + + Divider() + + Button("Zoom In") { + appState?.zoomIn() + } + .keyboardShortcut("+") + .disabled(!hasDocument) + + Button("Zoom Out") { + appState?.zoomOut() + } + .keyboardShortcut("-") + .disabled(!hasDocument) + + Button("Fit to Width") { + appState?.fitWidth() + } + .keyboardShortcut("9", modifiers: [.command]) + .disabled(!hasDocument) + + Button("Fit to Page") { + appState?.fitPage() + } + .keyboardShortcut("8", modifiers: [.command]) + .disabled(!hasDocument) + + Button("Two Pages Continuous") { + appState?.twoPageContinuous() + } + .keyboardShortcut("7", modifiers: [.command]) + .disabled(!hasDocument) + } + + CommandMenu("Annotate") { + Button("Highlight Selection") { + appState?.addHighlight() + } + .keyboardShortcut("h", modifiers: [.command, .shift]) + .disabled(!hasDocument) + + Button("Underline Selection") { + appState?.addUnderline() + } + .keyboardShortcut("u", modifiers: [.command, .shift]) + .disabled(!hasDocument) + + Button("Comment on Selection") { + appState?.addComment() + } + .keyboardShortcut("n", modifiers: [.command, .shift]) + .disabled(!hasDocument) + + Button("Add Free Text") { + appState?.addFreeText() + } + .keyboardShortcut("t", modifiers: [.command, .shift]) + .disabled(!hasDocument) + } + + CommandGroup(after: .windowArrangement) { + Button("Minimize") { + appState?.minimizeWindow() + } + .keyboardShortcut("m", modifiers: [.command]) + .disabled(appState == nil) + + Button("Toggle Full Screen") { + appState?.toggleFullScreen() + } + .keyboardShortcut("f", modifiers: [.command, .control]) + .disabled(appState == nil) } } } diff --git a/Sources/IHatePDFs/MainView.swift b/Sources/IHatePDFs/MainView.swift index b7a4d9b..6b23f8a 100644 --- a/Sources/IHatePDFs/MainView.swift +++ b/Sources/IHatePDFs/MainView.swift @@ -129,6 +129,15 @@ private struct ReaderToolbar: ToolbarContent { } .disabled(appState.document == nil) .help("Toggle Page Sidebar") + + Button { + appState.showCommentsSidebar.toggle() + } label: { + Label("Comments Sidebar", systemImage: "sidebar.right") + } + .disabled(appState.document == nil) + .help(appState.showCommentsSidebar ? "Hide Comments Sidebar" : "Show Comments Sidebar") + .accessibilityLabel("Toggle Comments Sidebar") } ToolbarItemGroup(placement: .principal) { @@ -288,16 +297,5 @@ private struct ReaderToolbar: ToolbarContent { .disabled(appState.document == nil) .help("Share PDF") } - - ToolbarItemGroup(placement: .primaryAction) { - Button { - appState.showCommentsSidebar.toggle() - } label: { - Label("Comments Sidebar", systemImage: "sidebar.right") - } - .disabled(appState.document == nil) - .help(appState.showCommentsSidebar ? "Hide Comments Sidebar" : "Show Comments Sidebar") - .accessibilityLabel("Toggle Comments Sidebar") - } } } diff --git a/Sources/IHatePDFs/ReturnKeyCommitMonitor.swift b/Sources/IHatePDFs/ReturnKeyCommitMonitor.swift new file mode 100644 index 0000000..50136f0 --- /dev/null +++ b/Sources/IHatePDFs/ReturnKeyCommitMonitor.swift @@ -0,0 +1,45 @@ +import AppKit +import SwiftUI + +extension View { + func commitOnPlainReturn(_ action: @escaping () -> Void) -> some View { + modifier(ReturnKeyCommitMonitor(action: action)) + } +} + +private struct ReturnKeyCommitMonitor: ViewModifier { + let action: () -> Void + @State private var monitor: Any? + + func body(content: Content) -> some View { + content + .onAppear { + installMonitor() + } + .onDisappear { + removeMonitor() + } + } + + private func installMonitor() { + removeMonitor() + monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in + guard isPlainReturn(event) else { return event } + action() + return nil + } + } + + private func removeMonitor() { + guard let monitor else { return } + NSEvent.removeMonitor(monitor) + self.monitor = nil + } + + private func isPlainReturn(_ event: NSEvent) -> Bool { + guard event.keyCode == 36 || event.keyCode == 76 else { return false } + + let multilineModifiers: NSEvent.ModifierFlags = [.shift, .option, .command, .control] + return event.modifierFlags.intersection(multilineModifiers).isEmpty + } +} diff --git a/Sources/IHatePDFs/SidebarViews.swift b/Sources/IHatePDFs/SidebarViews.swift index eeeab9d..600e53f 100644 --- a/Sources/IHatePDFs/SidebarViews.swift +++ b/Sources/IHatePDFs/SidebarViews.swift @@ -574,6 +574,11 @@ private struct SidebarReplyComposer: View { isFocused = true } } + .commitOnPlainReturn { + if !appState.sidebarReplyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + appState.commitSidebarReply() + } + } } } diff --git a/scripts/build-app.sh b/scripts/build-app.sh index b732b90..b7a83c5 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -3,6 +3,8 @@ set -euo pipefail APP_NAME="I Hate PDFs" EXECUTABLE_NAME="IHatePDFs" +APP_VERSION="${APP_VERSION:-0.2.0}" +BUILD_NUMBER="${BUILD_NUMBER:-2}" CONFIGURATION="${CONFIGURATION:-release}" if [[ -z "${ARCHS+x}" && "$CONFIGURATION" == "release" ]]; then ARCHS="arm64 x86_64" @@ -97,9 +99,9 @@ cat > "$CONTENTS_DIR/Info.plist" < CFBundleShortVersionString - 0.1.0 + $APP_VERSION CFBundleVersion - 1 + $BUILD_NUMBER LSMinimumSystemVersion 13.0 NSHighResolutionCapable diff --git a/scripts/make-dmg.sh b/scripts/make-dmg.sh index f649232..e42e7c7 100755 --- a/scripts/make-dmg.sh +++ b/scripts/make-dmg.sh @@ -3,9 +3,10 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" APP_NAME="I Hate PDFs" +RELEASE_VERSION="${RELEASE_VERSION:-0.2}" DIST_DIR="$ROOT_DIR/dist" APP_DIR="$DIST_DIR/$APP_NAME.app" -DMG_PATH="$DIST_DIR/IHatePDFs.dmg" +DMG_PATH="$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos.dmg" if [[ ! -d "$APP_DIR" ]]; then "$ROOT_DIR/scripts/build-app.sh"