Release v0.2
This commit is contained in:
20
CHANGELOG.md
Normal file
20
CHANGELOG.md
Normal 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.
|
||||||
16
README.md
16
README.md
@@ -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
8
RELEASE_NOTES.md
Normal 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.
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
Sources/IHatePDFs/ReturnKeyCommitMonitor.swift
Normal file
45
Sources/IHatePDFs/ReturnKeyCommitMonitor.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -574,6 +574,11 @@ private struct SidebarReplyComposer: View {
|
|||||||
isFocused = true
|
isFocused = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.commitOnPlainReturn {
|
||||||
|
if !appState.sidebarReplyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
appState.commitSidebarReply()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user