4 Commits
v0.1 ... v0.2

Author SHA1 Message Date
Akshay Kolli
3d112c677a Release v0.2 2026-06-18 16:44:19 -07:00
Akshay Kolli
c2ca546c8c Fix typos and enhance README content
Corrected typos and improved wording in README.
2026-06-12 15:39:36 -04:00
Akshay Kolli
4d030f1d0f Document v0.1 DMG release 2026-06-11 22:20:14 -07:00
Akshay Kolli
eae686a45f Update README.md 2026-06-12 01:12:07 -04:00
10 changed files with 282 additions and 161 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

@@ -1,15 +1,25 @@
# I Hate PDFs # I Hate PDFs
I Hate PDFs is an open-source macOS PDF reader for anyone who hates adobe. I think adobe is . I Hate PDFs is an open-source macOS PDF reader for anyone who hates adobe. I think adobe is worth of any sentient being's disdain.
## Status ## Status
This app is entirely vibe coded, but will somehow still be better than adobe acrobate soon. This app is entirely vibe coded, but will somehow still be better than adobe acrobat soon.
Minimum supported macOS version: macOS 13 Ventura. Minimum supported macOS version: macOS 13 Ventura.
Supported Mac architectures: Apple Silicon and Intel, subject to the local Swift/Xcode toolchain used to build. Supported Mac architectures: Apple Silicon and Intel, subject to the local Swift/Xcode toolchain used to build.
## Latest Release
Download the v0.2 macOS DMG from the GitHub release page:
<https://github.com/akkolli/ihatepdfs/releases/tag/v0.2>
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.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
- Open local `.pdf` files from disk. - Open local `.pdf` files from disk.
@@ -29,6 +39,11 @@ Supported Mac architectures: Apple Silicon and Intel, subject to the local Swift
- Review annotations in a compact list with page number, type, author, date, and first comment line. - Review annotations in a compact list with page number, type, author, date, and first comment line.
- Use an Acrobat-style comments sidebar with total count, page grouping, collapsible groups, an add-comment affordance, comment search, collapsed type/author/status filters, full text, replies, edit/delete, and click-to-navigate. - Use an Acrobat-style comments sidebar with total count, page grouping, collapsible groups, an add-comment affordance, comment search, collapsed type/author/status filters, full text, replies, edit/delete, and click-to-navigate.
### Download Releases
https://github.com/akkolli/ihatepdfs/releases/tag/v0.2
## Build From Source ## Build From Source
Requirements: Requirements:
@@ -67,11 +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.dmg`, open it, and move `I Hate PDFs.app` into `/Applications`. For local development builds that are not notarized, macOS may require opening the app from Finder with Control-click, Open the first time. Download `IHatePDFs-v0.2-macos.dmg` from the latest GitHub release, open it, and move `I Hate PDFs.app` into `/Applications`.
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"