Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions AUDIT_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

This log tracks all significant changes, updates, and versions in the PaperCache project.

## 2026-06-27 (Settings & Onboarding Fixes)
**Change:** feat: add Bug Report option and About section in Settings; fix Windows onboarding file path generation

**Details/Why:**
1. Added "Submit a Bug Report" button under Settings > System linking directly to GitHub Issues creation page (`https://github.com/VariableThe/PaperCache/issues/new`).
2. Added dedicated "About" section in Settings featuring app logo (`/icon.png`), dynamic app version number (`getVersion`), check for updates button, Ko-fi support button (`https://ko-fi.com/thevariable`), and user thank you message.
3. Resolved onboarding note generation and `/file` linking failures on Windows. Previously, `walk_dir` generated note IDs with backslashes on Windows (e.g. `onboarding\Editor.md`), causing internal `/file` links using forward slashes in `Welcome.md` to fail lookup and duplicate empty notes. Normalized path generation across Rust (`fs.rs`) and frontend (`useNoteStorage.ts`, `markdownPlugin.ts`, `GraphView.tsx`) to guarantee consistent forward slash IDs across all OS targets. Updated `write_onboarding_file` to regenerate onboarding files on version bumps.

**Files changed:** `src/Settings.tsx`, `src/setupTests.ts`, `src-tauri/src/commands/fs.rs`, `src/hooks/useNoteStorage.ts`, `src/lib/editor/markdownPlugin.ts`, `src/GraphView.tsx`, `CHANGELOG.md`, `AUDIT_LOG.md`.

---

## 2026-06-27 (Lockfile Sync)
**Change:** build(deps): move @emnapi WASM fallbacks to devDependencies for deterministic lockfile resolution across OS targets

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [v0.5.4] - 2026-06-25

### Added
- **Settings Bug Report & About Menu**: Added a "Submit a Bug Report" button under System settings linking directly to the GitHub issue creation form. Added a dedicated "About" section displaying the app logo, current version number, update checker, Ko-fi support link, and a thank you message.

### Fixed
- **Windows Onboarding File Linking & Generation**: Fixed a bug on Windows where backslashes in generated note IDs caused internal `/file` links in `Welcome.md` to fail and create duplicate empty notes. Normalized note ID generation across Rust and TypeScript to consistently use forward slashes on all platforms, and ensured onboarding template files regenerate correctly on application updates.
- **Window position/size now persists across restarts**: The window-state plugin's `on_window_ready` fires before the macOS display server is ready, causing `available_monitors()` to return empty and the saved position to be silently discarded. Fixed by deferring window-state restoration via a background thread + `run_on_main_thread` 300ms after `setup()` completes, bypassing the plugin's monitor-intersection check with a direct file read. Both the tray "Quit" and Settings "Quit" buttons now explicitly save window state before exit.
- **Launch at Startup now registers as a proper Login Item**: Changed `MacosLauncher` from `LaunchAgent` to `AppleScript`, which registers PaperCache in System Settings > General > Login Items instead of creating a hidden `launchd` plist. Users can now see and manage the autostart entry directly from System Settings.
- **Window no longer shifts down on restart on macOS**: Removed the +/-28px compensation that was incorrectly added for a macOS frameless window offset which does not exist in `tauri-plugin-window-state` v2.4.1 — the plugin correctly saves `outer_position()` and restores via `set_position()` with no titlebar offset for frameless windows.
Expand Down
27 changes: 20 additions & 7 deletions src-tauri/src/commands/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ pub fn get_papercache_dir() -> Result<PathBuf, String> {

pub fn get_safe_path(id: &str) -> Result<PathBuf, String> {
let base = get_papercache_dir()?;
let target = base.join(id);
let clean_id = id.replace('\\', "/");
let mut target = base.clone();
for comp in clean_id.split('/') {
if !comp.is_empty() && comp != "." && comp != ".." {
target.push(comp);
} else if comp == ".." {
return Err("Path traversal detected".to_string());
}
}

let parent = target.parent().ok_or("Invalid path parent")?;
if !parent.exists() {
Expand Down Expand Up @@ -74,7 +82,7 @@ fn walk_dir(dir: &Path, notes: &mut Vec<Note>, base_path: &Path) -> Result<(), S
.strip_prefix(base_path)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
.replace('\\', "/");
notes.push(Note { id, content, mtime });
}
}
Expand Down Expand Up @@ -125,7 +133,7 @@ pub fn read_note(id: String) -> Result<String, String> {

#[tauri::command]
pub fn delete_note(id: String) -> Result<bool, String> {
if id.starts_with("commands/") {
if id.replace('\\', "/").starts_with("commands/") {
return Err("Cannot delete protected command files".into());
}
let path = get_safe_path(&id)?;
Expand Down Expand Up @@ -197,13 +205,18 @@ pub fn set_dialog_open(state: tauri::State<'_, crate::DialogState>, open: bool)
state.is_open.store(open, Ordering::SeqCst);
}

fn write_onboarding_file(base: &Path, rel_path: &str, content: &str, _is_new_version: bool) {
let path = base.join(rel_path);
fn write_onboarding_file(base: &Path, rel_path: &str, content: &str, is_new_version: bool) {
let mut path = base.to_path_buf();
for comp in rel_path.replace('\\', "/").split('/') {
if !comp.is_empty() {
path.push(comp);
}
}
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
// Only write if file doesn't exist — preserve user edits across version changes
if !path.exists() {
// Only write if file doesn't exist, or if upgrading to a new app version
if !path.exists() || is_new_version {
let _ = fs::write(&path, content);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/GraphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export default function GraphView({
const re = /\]\(\/file\s+([^)]+)\)/g
let match
while ((match = re.exec(note.content)) !== null) {
let targetId = match[1]
let targetId = match[1].trim().replace(/\\/g, '/')
if (!targetId.endsWith('.md')) targetId += '.md'
if (nodeIds.has(targetId)) {
links.push({ source: note.id, target: targetId })
Expand Down
96 changes: 96 additions & 0 deletions src/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect, Fragment } from 'react'
import { getVersion } from '@tauri-apps/api/app'
import { SETTINGS_KEYS } from './lib/settingsKeys'
import { useAppStore } from './store/useAppStore'
import { useSettingsStore } from './store/useSettingsStore'
Expand Down Expand Up @@ -58,6 +59,13 @@ export default function Settings({ onClose }: { onClose?: () => void }) {
})
}, [])

const [appVersion, setAppVersion] = useState('0.5.3')
useEffect(() => {
getVersion()
.then((ver) => setAppVersion(ver))
.catch(() => {})
}, [])

// Appearance State
const initialSettings = useSettingsStore.getState()
const [fontFamily, setFontFamily] = useState(initialSettings.fontFamily)
Expand Down Expand Up @@ -250,6 +258,28 @@ export default function Settings({ onClose }: { onClose?: () => void }) {
Check for Updates Now
</button>
</div>
<div className="setting-group">
<label>Submit a Bug Report</label>
<button
onClick={() =>
window.electronAPI.openExternal(
'https://github.com/VariableThe/PaperCache/issues/new'
)
}
style={{
padding: '6px 12px',
background: 'rgba(128,128,128,0.1)',
border: '1px solid rgba(128,128,128,0.2)',
borderRadius: '6px',
cursor: 'pointer',
color: 'inherit',
fontFamily: 'inherit',
margin: '0 auto',
}}
>
Report Issue on GitHub 🐞
</button>
</div>
</section>

<section>
Expand Down Expand Up @@ -348,6 +378,72 @@ export default function Settings({ onClose }: { onClose?: () => void }) {
<input type="color" value={aiColor} onChange={(e) => setAiColor(e.target.value)} />
</div>
</section>

<section>
<h3>About</h3>
<div style={{ textAlign: 'center', padding: '10px 0' }}>
<img
src="/icon.png"
alt="PaperCache Logo"
style={{
width: '64px',
height: '64px',
margin: '0 auto 12px',
display: 'block',
borderRadius: '12px',
}}
/>
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '4px' }}>PaperCache</div>
<div style={{ fontSize: '13px', color: '#888', marginBottom: '16px' }}>
Version {appVersion}
</div>
<p
style={{
fontSize: '13px',
color: '#ccc',
lineHeight: '1.5',
maxWidth: '400px',
margin: '0 auto 20px',
}}
>
Thank you for using PaperCache! We hope it helps organize your thoughts and boost your
daily productivity.
</p>
<div
style={{ display: 'flex', gap: '12px', justifyContent: 'center', flexWrap: 'wrap' }}
>
<button
onClick={() => window.electronAPI.checkForUpdates()}
style={{
padding: '6px 14px',
background: 'rgba(128,128,128,0.1)',
border: '1px solid rgba(128,128,128,0.2)',
borderRadius: '6px',
cursor: 'pointer',
color: 'inherit',
fontFamily: 'inherit',
}}
>
Check for Updates
</button>
<button
onClick={() => window.electronAPI.openExternal('https://ko-fi.com/thevariable')}
style={{
padding: '6px 14px',
background: 'rgba(255,94,91,0.15)',
border: '1px solid rgba(255,94,91,0.3)',
borderRadius: '6px',
cursor: 'pointer',
color: '#ff5e5b',
fontFamily: 'inherit',
fontWeight: 500,
}}
>
Support on Ko-fi ☕
</button>
</div>
</div>
</section>
</div>

<div className="settings-footer">
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useNoteStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function useNoteStorage() {
useEffect(() => {
const handleOpenNote = (e: Event) => {
const customEvent = e as CustomEvent
let path = customEvent.detail.path
let path = customEvent.detail.path.replace(/\\/g, '/')
if (!path.endsWith('.md')) path += '.md'

// We need the latest notes, so use useAppStore.getState()
Expand Down
2 changes: 1 addition & 1 deletion src/lib/editor/markdownPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const markdownPlugin = ViewPlugin.fromClass(

if (linkPath.startsWith('/file')) {
isFile = true
linkPath = linkPath.substring(5).trim()
linkPath = linkPath.substring(5).trim().replace(/\\/g, '/')
} else if (linkPath.startsWith('/url')) {
linkPath = linkPath.substring(4).trim()
}
Expand Down
4 changes: 4 additions & 0 deletions src/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import '@testing-library/jest-dom'
import { vi, afterEach } from 'vitest'
import type { ElectronAPI } from './types'

vi.mock('@tauri-apps/api/app', () => ({
getVersion: vi.fn().mockResolvedValue('0.5.3'),
}))

// Mock matchMedia which is not present in jsdom but might be needed by some components
if (typeof window !== 'undefined') {
Object.defineProperty(window, 'matchMedia', {
Expand Down
Loading