Release v0.2

This commit is contained in:
Akshay Kolli
2026-06-18 16:44:19 -07:00
parent c2ca546c8c
commit 09f6f9d970
10 changed files with 269 additions and 165 deletions

20
CHANGELOG.md Normal file
View File

@@ -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.

View File

@@ -12,13 +12,13 @@ Supported Mac architectures: Apple Silicon and Intel, subject to the local Swift
## Latest Release ## Latest Release
Download the v0.1 macOS DMG from the GitHub release page: Download the v0.2 macOS DMG from the GitHub release page:
<https://github.com/akkolli/ihatepdfs/releases/tag/v0.1> <https://github.com/akkolli/ihatepdfs/releases/tag/v0.2>
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 ## 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 ### Download Releases
https://github.com/akkolli/ihatepdfs/releases/tag/v0.1 https://github.com/akkolli/ihatepdfs/releases/tag/v0.2
## Build From Source ## Build From Source
@@ -82,13 +82,13 @@ Create a downloadable `.dmg`:
scripts/make-dmg.sh 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 ## 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 ## Development

8
RELEASE_NOTES.md Normal file
View File

@@ -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.

View File

@@ -69,6 +69,9 @@ struct CommentEditorView: View {
.onChange(of: model.author) { _ in .onChange(of: model.author) { _ in
model.updateDraft() model.updateDraft()
} }
.commitOnPlainReturn {
model.commit()
}
} }
private var header: some View { private var header: some View {

View File

@@ -3,154 +3,176 @@ import SwiftUI
@main @main
struct IHatePDFsApp: App { struct IHatePDFsApp: App {
@StateObject private var appState = AppState()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
MainView() AppWindowRoot()
.environmentObject(appState)
.onOpenURL { url in
appState.loadDocument(from: url)
}
} }
.windowStyle(.titleBar) .windowStyle(.titleBar)
.commands { .commands {
CommandGroup(replacing: .newItem) { AppCommands()
Button("Open...") { }
appState.openDocument() }
} }
.keyboardShortcut("o")
private struct AppWindowRoot: View {
Button("Save") { @StateObject private var appState = AppState()
appState.saveDocument()
} var body: some View {
.keyboardShortcut("s") MainView()
.disabled(appState.document == nil) .environmentObject(appState)
.focusedObject(appState)
Button("Save As...") { .onOpenURL { url in
appState.saveDocumentAs() appState.loadDocument(from: url)
} }
.keyboardShortcut("s", modifiers: [.command, .shift]) }
.disabled(appState.document == nil) }
Button("Share...") { private struct AppCommands: Commands {
appState.shareDocument() @FocusedObject private var appState: AppState?
}
.keyboardShortcut("e", modifiers: [.command, .shift]) private var hasDocument: Bool {
.disabled(appState.document == nil) appState?.document != nil
}
Divider()
var body: some Commands {
Button("Close PDF") { CommandGroup(replacing: .newItem) {
appState.closeDocument() Button("Open...") {
} appState?.openDocument()
.keyboardShortcut("w") }
.disabled(appState.document == nil) .keyboardShortcut("o")
} .disabled(appState == nil)
CommandGroup(after: .textEditing) { Button("Save") {
Button("Find in PDF") { appState?.saveDocument()
appState.showSearch() }
} .keyboardShortcut("s")
.keyboardShortcut("f") .disabled(!hasDocument)
.disabled(appState.document == nil)
Button("Save As...") {
Button("Find Next") { appState?.saveDocumentAs()
appState.nextSearchResult() }
} .keyboardShortcut("s", modifiers: [.command, .shift])
.keyboardShortcut("g") .disabled(!hasDocument)
.disabled(appState.searchResults.isEmpty)
Button("Share...") {
Button("Find Previous") { appState?.shareDocument()
appState.previousSearchResult() }
} .keyboardShortcut("e", modifiers: [.command, .shift])
.keyboardShortcut("g", modifiers: [.command, .shift]) .disabled(!hasDocument)
.disabled(appState.searchResults.isEmpty)
} Divider()
CommandMenu("View") { Button("Close PDF") {
Button("Toggle Page Sidebar") { appState?.closeDocument()
appState.showLeftSidebar.toggle() }
} .keyboardShortcut("w")
.keyboardShortcut("0", modifiers: [.command, .option]) .disabled(!hasDocument)
.disabled(appState.document == nil) }
Button("Toggle Comments Sidebar") { CommandGroup(after: .textEditing) {
appState.showCommentsSidebar.toggle() Button("Find in PDF") {
} appState?.showSearch()
.keyboardShortcut("1", modifiers: [.command, .option]) }
.disabled(appState.document == nil) .keyboardShortcut("f")
.disabled(!hasDocument)
Divider()
Button("Find Next") {
Button("Zoom In") { appState?.nextSearchResult()
appState.zoomIn() }
} .keyboardShortcut("g")
.keyboardShortcut("+") .disabled(appState?.searchResults.isEmpty != false)
.disabled(appState.document == nil)
Button("Find Previous") {
Button("Zoom Out") { appState?.previousSearchResult()
appState.zoomOut() }
} .keyboardShortcut("g", modifiers: [.command, .shift])
.keyboardShortcut("-") .disabled(appState?.searchResults.isEmpty != false)
.disabled(appState.document == nil) }
Button("Fit to Width") { CommandMenu("View") {
appState.fitWidth() Button("Toggle Page Sidebar") {
} appState?.showLeftSidebar.toggle()
.keyboardShortcut("9", modifiers: [.command]) }
.disabled(appState.document == nil) .keyboardShortcut("0", modifiers: [.command, .option])
.disabled(!hasDocument)
Button("Fit to Page") {
appState.fitPage() Button("Toggle Comments Sidebar") {
} appState?.showCommentsSidebar.toggle()
.keyboardShortcut("8", modifiers: [.command]) }
.disabled(appState.document == nil) .keyboardShortcut("1", modifiers: [.command, .option])
.disabled(!hasDocument)
Button("Two Pages Continuous") {
appState.twoPageContinuous() Divider()
}
.keyboardShortcut("7", modifiers: [.command]) Button("Zoom In") {
.disabled(appState.document == nil) appState?.zoomIn()
} }
.keyboardShortcut("+")
CommandMenu("Annotate") { .disabled(!hasDocument)
Button("Highlight Selection") {
appState.addHighlight() Button("Zoom Out") {
} appState?.zoomOut()
.keyboardShortcut("h", modifiers: [.command, .shift]) }
.disabled(appState.document == nil) .keyboardShortcut("-")
.disabled(!hasDocument)
Button("Underline Selection") {
appState.addUnderline() Button("Fit to Width") {
} appState?.fitWidth()
.keyboardShortcut("u", modifiers: [.command, .shift]) }
.disabled(appState.document == nil) .keyboardShortcut("9", modifiers: [.command])
.disabled(!hasDocument)
Button("Comment on Selection") {
appState.addComment() Button("Fit to Page") {
} appState?.fitPage()
.keyboardShortcut("n", modifiers: [.command, .shift]) }
.disabled(appState.document == nil) .keyboardShortcut("8", modifiers: [.command])
.disabled(!hasDocument)
Button("Add Free Text") {
appState.addFreeText() Button("Two Pages Continuous") {
} appState?.twoPageContinuous()
.keyboardShortcut("t", modifiers: [.command, .shift]) }
.disabled(appState.document == nil) .keyboardShortcut("7", modifiers: [.command])
} .disabled(!hasDocument)
}
CommandGroup(after: .windowArrangement) {
Button("Minimize") { CommandMenu("Annotate") {
appState.minimizeWindow() Button("Highlight Selection") {
} appState?.addHighlight()
.keyboardShortcut("m", modifiers: [.command]) }
.keyboardShortcut("h", modifiers: [.command, .shift])
Button("Toggle Full Screen") { .disabled(!hasDocument)
appState.toggleFullScreen()
} Button("Underline Selection") {
.keyboardShortcut("f", modifiers: [.command, .control]) 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)
} }
} }
} }

View File

@@ -129,6 +129,15 @@ private struct ReaderToolbar: ToolbarContent {
} }
.disabled(appState.document == nil) .disabled(appState.document == nil)
.help("Toggle Page Sidebar") .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) { ToolbarItemGroup(placement: .principal) {
@@ -288,16 +297,5 @@ private struct ReaderToolbar: ToolbarContent {
.disabled(appState.document == nil) .disabled(appState.document == nil)
.help("Share PDF") .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")
}
} }
} }

View File

@@ -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
}
}

View File

@@ -574,6 +574,11 @@ private struct SidebarReplyComposer: View {
isFocused = true isFocused = true
} }
} }
.commitOnPlainReturn {
if !appState.sidebarReplyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
appState.commitSidebarReply()
}
}
} }
} }

View File

@@ -3,6 +3,8 @@ set -euo pipefail
APP_NAME="I Hate PDFs" APP_NAME="I Hate PDFs"
EXECUTABLE_NAME="IHatePDFs" EXECUTABLE_NAME="IHatePDFs"
APP_VERSION="${APP_VERSION:-0.2.0}"
BUILD_NUMBER="${BUILD_NUMBER:-2}"
CONFIGURATION="${CONFIGURATION:-release}" CONFIGURATION="${CONFIGURATION:-release}"
if [[ -z "${ARCHS+x}" && "$CONFIGURATION" == "release" ]]; then if [[ -z "${ARCHS+x}" && "$CONFIGURATION" == "release" ]]; then
ARCHS="arm64 x86_64" ARCHS="arm64 x86_64"
@@ -97,9 +99,9 @@ cat > "$CONTENTS_DIR/Info.plist" <<PLIST
</dict> </dict>
</array> </array>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.1.0</string> <string>$APP_VERSION</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>$BUILD_NUMBER</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>13.0</string> <string>13.0</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>

View File

@@ -3,9 +3,10 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_NAME="I Hate PDFs" APP_NAME="I Hate PDFs"
RELEASE_VERSION="${RELEASE_VERSION:-0.2}"
DIST_DIR="$ROOT_DIR/dist" DIST_DIR="$ROOT_DIR/dist"
APP_DIR="$DIST_DIR/$APP_NAME.app" 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 if [[ ! -d "$APP_DIR" ]]; then
"$ROOT_DIR/scripts/build-app.sh" "$ROOT_DIR/scripts/build-app.sh"