diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00730a0..93911c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,19 +76,20 @@ jobs: token: ${{ secrets.HOMEBREW_TAP_TOKEN }} path: homebrew-tap - - name: Download macOS DMG + - name: Download macOS App env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION=${{ needs.create-tag.outputs.new_version }} - gh release download v$VERSION --pattern "*.dmg" --repo $GITHUB_REPOSITORY + gh release download v$VERSION --pattern "*aarch64.tar.gz" --repo $GITHUB_REPOSITORY - # Calculate SHA256 of the downloaded DMG - DMG_FILE=$(ls *.dmg | head -n 1) - SHA256=$(shasum -a 256 "$DMG_FILE" | awk '{ print $1 }') + # Calculate SHA256 of the downloaded archive + APP_FILE=$(ls *aarch64.tar.gz | head -n 1) + SHA256=$(shasum -a 256 "$APP_FILE" | awk '{ print $1 }') # Update the rb file cd homebrew-tap + sed -i '' "s/url .*/url \"https:\/\/github.com\/VariableThe\/PaperCache\/releases\/download\/v$VERSION\/$APP_FILE\"/" Casks/papercache.rb sed -i '' "s/version \".*\"/version \"$VERSION\"/" Casks/papercache.rb sed -i '' "s/sha256 arm: * \".*\"/sha256 arm: \"$SHA256\"/" Casks/papercache.rb diff --git a/src-tauri/src/commands/fs.rs b/src-tauri/src/commands/fs.rs index e91cf3c..0db0d35 100644 --- a/src-tauri/src/commands/fs.rs +++ b/src-tauri/src/commands/fs.rs @@ -46,38 +46,36 @@ pub fn get_safe_path(id: &str) -> Result { } } -fn walk_dir(dir: &Path, notes: &mut Vec, base_path: &Path) { - if let Ok(entries) = fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - walk_dir(&path, notes, base_path); - } else if path.is_file() { - let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); - if ext == "md" || ext == "json" { - // Ignore legacy Electron window-state files - if path.file_name().and_then(|n| n.to_str()) == Some("window-state.json") { - continue; - } - - if let Ok(content) = fs::read_to_string(&path) { - let metadata = fs::metadata(&path).ok(); - let mtime = metadata - .and_then(|m| m.modified().ok()) - .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|d| d.as_millis() as u64) - .unwrap_or(0); - let id = path - .strip_prefix(base_path) - .unwrap_or(&path) - .to_string_lossy() - .to_string(); - notes.push(Note { id, content, mtime }); - } - } +fn walk_dir(dir: &Path, notes: &mut Vec, base_path: &Path) -> Result<(), String> { + let entries = fs::read_dir(dir).map_err(|e| e.to_string())?; + for entry in entries { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.is_dir() { + walk_dir(&path, notes, base_path)?; + } else if path.is_file() { + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if ext == "md" || ext == "json" { + let content = fs::read_to_string(&path) + .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + + let metadata = fs::metadata(&path).ok(); + let mtime = metadata + .and_then(|m| m.modified().ok()) + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_millis() as u64) + .unwrap_or_default(); + + let id = path + .strip_prefix(base_path) + .unwrap_or(&path) + .to_string_lossy() + .to_string(); + notes.push(Note { id, content, mtime }); } } } + Ok(()) } fn clean_empty_parents(file_path: &Path, base: &Path) { @@ -104,7 +102,7 @@ fn clean_empty_parents(file_path: &Path, base: &Path) { pub fn get_notes() -> Result, String> { let base = get_papercache_dir()?; let mut notes = Vec::new(); - walk_dir(&base, &mut notes, &base); + walk_dir(&base, &mut notes, &base)?; Ok(notes) } diff --git a/src-tauri/src/commands/keychain.rs b/src-tauri/src/commands/keychain.rs index 3074be5..e414d0d 100644 --- a/src-tauri/src/commands/keychain.rs +++ b/src-tauri/src/commands/keychain.rs @@ -12,6 +12,10 @@ const SERVICE_NAME: &str = "com.variablethe.papercache"; pub fn set_api_key(key: String) -> Result { let entry = Entry::new(SERVICE_NAME, "openai_api_key") .map_err(|e| format!("Failed to access keyring: {}", e))?; + if key.is_empty() { + entry.delete_credential().ok(); + return Ok(true); + } entry .set_password(&key) .map_err(|e| format!("Failed to set API key: {}", e))?; diff --git a/src-tauri/src/commands/shortcuts.rs b/src-tauri/src/commands/shortcuts.rs index 8872836..005d85a 100644 --- a/src-tauri/src/commands/shortcuts.rs +++ b/src-tauri/src/commands/shortcuts.rs @@ -39,17 +39,7 @@ pub fn update_global_shortcut( app.global_shortcut() .on_shortcut(shortcut, move |app, _shortcut, event| { if event.state() == ShortcutState::Pressed { - if let Some(window) = app.get_webview_window("main") { - let is_visible = window.is_visible().unwrap_or(false); - if is_visible { - let _ = window.hide(); - } else { - let _ = window.show(); - let _ = window.set_focus(); - #[cfg(target_os = "macos")] - crate::macos::force_focus(); - } - } + crate::commands::system::toggle_window(app); let _ = app.emit(&format!("trigger-{}", action_clone), ()); } }) @@ -83,17 +73,7 @@ pub fn resume_shortcuts(app: AppHandle) -> Result<(), String> { .global_shortcut() .on_shortcut(shortcut, move |app, _, event| { if event.state() == ShortcutState::Pressed { - if let Some(window) = app.get_webview_window("main") { - let is_visible = window.is_visible().unwrap_or(false); - if is_visible { - let _ = window.hide(); - } else { - let _ = window.show(); - let _ = window.set_focus(); - #[cfg(target_os = "macos")] - crate::macos::force_focus(); - } - } + crate::commands::system::toggle_window(app); let _ = app.emit(&format!("trigger-{}", action_clone), ()); } }); diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index 3d3cbcf..cc39f56 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -1,4 +1,4 @@ -use tauri::{AppHandle, WebviewWindow}; +use tauri::{AppHandle, Manager, WebviewWindow}; use tauri_plugin_opener::OpenerExt; #[tauri::command] @@ -14,6 +14,20 @@ pub fn close_window(window: WebviewWindow) -> Result<(), String> { Ok(()) } +pub fn toggle_window(app: &AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let is_visible = window.is_visible().unwrap_or(false); + if is_visible { + let _ = window.hide(); + } else { + let _ = window.show(); + let _ = window.set_focus(); + #[cfg(target_os = "macos")] + crate::macos::force_focus(); + } + } +} + #[tauri::command] pub fn quit_app(app: AppHandle) { app.exit(0); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 674d0e8..16ece9c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,6 +6,7 @@ mod commands; #[cfg(target_os = "macos")] mod macos; mod tray; +mod window_utils; use commands::shortcuts::GlobalShortcutState; use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index b9fc6b0..92f9f69 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -9,8 +9,14 @@ pub fn create_tray(app: &App) -> Result<(), Box> { let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; let menu = Menu::with_items(app, &[&show_hide, &quit])?; - let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/tray.png")) - .expect("Failed to load tray icon"); + let icon_result = tauri::image::Image::from_bytes(include_bytes!("../icons/tray.png")); + let icon = match icon_result { + Ok(icon) => icon, + Err(e) => { + eprintln!("Failed to load tray icon: {}", e); + return Ok(()); + } + }; TrayIconBuilder::new() .icon(icon) @@ -20,17 +26,7 @@ pub fn create_tray(app: &App) -> Result<(), Box> { .show_menu_on_left_click(false) .on_menu_event(|app, event| { if event.id == "show_hide" { - if let Some(window) = app.get_webview_window("main") { - let is_visible = window.is_visible().unwrap_or(false); - if is_visible { - let _ = window.hide(); - } else { - let _ = window.show(); - let _ = window.set_focus(); - #[cfg(target_os = "macos")] - crate::macos::force_focus(); - } - } + crate::commands::system::toggle_window(app); } else if event.id == "quit" { app.exit(0); } @@ -43,17 +39,7 @@ pub fn create_tray(app: &App) -> Result<(), Box> { } = event { let app = tray.app_handle(); - if let Some(window) = app.get_webview_window("main") { - let is_visible = window.is_visible().unwrap_or(false); - if is_visible { - let _ = window.hide(); - } else { - let _ = window.show(); - let _ = window.set_focus(); - #[cfg(target_os = "macos")] - crate::macos::force_focus(); - } - } + crate::commands::system::toggle_window(&app); } }) .build(app)?; diff --git a/src-tauri/src/window_utils.rs b/src-tauri/src/window_utils.rs new file mode 100644 index 0000000..e824a71 --- /dev/null +++ b/src-tauri/src/window_utils.rs @@ -0,0 +1,11 @@ +use tauri::AppHandle; +use tauri::Manager; + +pub fn show_and_focus_window(app: &AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + #[cfg(target_os = "macos")] + crate::macos::force_focus(); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 81b1ef1..fc9ceb0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -28,7 +28,7 @@ }, "bundle": { "active": true, - "targets": ["nsis", "msi", "appimage", "deb", "dmg", "app"], + "targets": ["nsis", "msi", "appimage", "deb", "app"], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src/App.css b/src/App.css index b36cfc3..e67c530 100644 --- a/src/App.css +++ b/src/App.css @@ -58,9 +58,6 @@ body { width: 150px; } -.settings-btn-container { -} - .open-settings-btn { background: transparent; border: none; @@ -73,9 +70,6 @@ body { opacity: 1; } -.theme-selector { -} - .theme-selector button { margin-left: 8px; font-family: var(--font-family); @@ -178,7 +172,9 @@ body { border-radius: 4px; outline: none; font-size: 11px; - transition: all 0.2s ease; + transition: + background-color 0.2s, + opacity 0.2s; width: 120px; } @@ -198,7 +194,9 @@ body { text-transform: capitalize; font-size: 11px; font-weight: 500; - transition: all 0.2s ease; + transition: + background-color 0.2s, + opacity 0.2s; } .cm-panel.cm-search button:hover { @@ -234,7 +232,9 @@ body { display: flex; align-items: center; justify-content: center; - transition: all 0.2s ease; + transition: + background-color 0.2s, + opacity 0.2s; margin-left: 2px; } @@ -705,7 +705,9 @@ body { vertical-align: middle; color: transparent; background-color: transparent; - transition: all 0.2s ease; + transition: + background-color 0.2s, + opacity 0.2s; user-select: none; margin-top: -2px; } @@ -746,7 +748,9 @@ body { vertical-align: middle; color: transparent; background-color: transparent; - transition: all 0.2s ease; + transition: + background-color 0.2s, + opacity 0.2s; user-select: none; margin-top: -2px; } diff --git a/src/api.ts b/src/api.ts index 1d66ccd..3bc1aa3 100644 --- a/src/api.ts +++ b/src/api.ts @@ -4,7 +4,14 @@ import type { ElectronAPI } from './types' export const tauriApi: ElectronAPI = { // Implemented Phase 2 Commands - getNotes: () => invoke('get_notes'), + getNotes: async () => { + try { + return await invoke('get_notes') + } catch (e) { + console.error('Failed to get notes', e) + return [] + } + }, saveNote: (id, content) => invoke('save_note', { id, content }), readNote: (id) => invoke('read_note', { id }), deleteNote: (id) => invoke('delete_note', { id }), diff --git a/src/hooks/useGlobalHotkey.ts b/src/hooks/useGlobalHotkey.ts index 24b4fbe..1def1f0 100644 --- a/src/hooks/useGlobalHotkey.ts +++ b/src/hooks/useGlobalHotkey.ts @@ -1,5 +1,6 @@ import { useEffect } from 'react' import { useAppStore } from '../store/useAppStore' +import { getCurrentWindow } from '@tauri-apps/api/window' export function useGlobalHotkey() { const setShowMainActionMenu = useAppStore((state) => state.setShowMainActionMenu) @@ -24,8 +25,7 @@ export function useGlobalHotkey() { // Close the app if nothing else was open if (!state.showNoteSearch && !isRenaming && actionMenuIndex === 0) { - // @ts-expect-error - appWindow is globally injected by Tauri in older setups but we ignore it here - window.appWindow?.hide() + await getCurrentWindow().hide() } if (state.showMainActionMenu) { e.preventDefault() diff --git a/src/hooks/useNoteStorage.ts b/src/hooks/useNoteStorage.ts index 651df7d..868976e 100644 --- a/src/hooks/useNoteStorage.ts +++ b/src/hooks/useNoteStorage.ts @@ -1,6 +1,5 @@ import { useEffect } from 'react' import { useAppStore } from '../store/useAppStore' -import type { Note } from '../store/useAppStore' export function useNoteStorage() { const notes = useAppStore((state) => state.notes) @@ -16,7 +15,7 @@ export function useNoteStorage() { setNotes(loaded) const lastOpenNoteId = localStorage.getItem('papercache-last-open-note') if (lastOpenNoteId) { - const idx = loaded.findIndex((n: Note) => n.id === lastOpenNoteId) + const idx = loaded.findIndex((n) => n.id === lastOpenNoteId) if (idx !== -1) { setCurrentNoteIndex(idx) } diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index d5b9dc6..9e05f85 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -9,7 +9,7 @@ export interface Note { interface AppState { notes: Note[] currentNoteIndex: number - themePreset: 'grid-light' | 'grid-dark' | 'blueprint' | 'glass' + themePreset: string // UI state showGraphView: boolean @@ -27,7 +27,7 @@ interface AppState { setNotes: (notes: Note[] | ((prev: Note[]) => Note[])) => void setCurrentNoteIndex: (index: number) => void - setThemePreset: (preset: 'grid-light' | 'grid-dark' | 'blueprint' | 'glass') => void + setThemePreset: (preset: string) => void setShowGraphView: (show: boolean | ((prev: boolean) => boolean)) => void setShowRemindersView: (show: boolean | ((prev: boolean) => boolean)) => void @@ -46,8 +46,7 @@ interface AppState { export const useAppStore = create((set) => ({ notes: [], currentNoteIndex: 0, - themePreset: - (localStorage.getItem('papercache-theme') as AppState['themePreset']) || 'grid-light', + themePreset: (localStorage.getItem('papercache-theme') as string) || 'grid-light', showGraphView: false, showRemindersView: false, diff --git a/src/store/useSettingsStore.ts b/src/store/useSettingsStore.ts index c37b375..8c36846 100644 --- a/src/store/useSettingsStore.ts +++ b/src/store/useSettingsStore.ts @@ -5,7 +5,7 @@ export interface SettingsState { themePreset: string fontFamily: string showRulings: boolean - bgType: 'color' | 'image' + bgType: 'preset' | 'color' | 'image' bgColor: string bgImage: string textColor: string @@ -26,23 +26,43 @@ export interface SettingsState { setAiColor: (color: string) => void setMathColor: (color: string) => void setSettings: ( - settings: Partial< - Omit< - SettingsState, - | 'setSettings' - | 'setThemePreset' - | 'setFontFamily' - | 'setShowRulings' - | 'setBgType' - | 'setBgColor' - | 'setBgImage' - | 'setTextColor' - | 'setNumColor' - | 'setSymColor' - | 'setAiColor' - | 'setMathColor' - > - > + settings: + | Partial< + Omit< + SettingsState, + | 'setSettings' + | 'setThemePreset' + | 'setFontFamily' + | 'setShowRulings' + | 'setBgType' + | 'setBgColor' + | 'setBgImage' + | 'setTextColor' + | 'setNumColor' + | 'setSymColor' + | 'setAiColor' + | 'setMathColor' + > + > + | (( + state: SettingsState + ) => Partial< + Omit< + SettingsState, + | 'setSettings' + | 'setThemePreset' + | 'setFontFamily' + | 'setShowRulings' + | 'setBgType' + | 'setBgColor' + | 'setBgImage' + | 'setTextColor' + | 'setNumColor' + | 'setSymColor' + | 'setAiColor' + | 'setMathColor' + > + >) ) => void } @@ -72,7 +92,11 @@ export const useSettingsStore = create()( setSymColor: (symColor) => set({ symColor }), setAiColor: (aiColor) => set({ aiColor }), setMathColor: (mathColor) => set({ mathColor }), - setSettings: (settings) => set((state) => ({ ...state, ...settings })), + setSettings: (settings) => + set((state) => ({ + ...state, + ...(typeof settings === 'function' ? settings(state) : settings), + })), }), { name: 'papercache-settings-store',