Skip to content

Commit f4452f9

Browse files
authored
Merge pull request #90 from VariableThe/feature/v0.5.9-image-support
Release v0.5.9: Image Support, UI Consistency, Linux CI Fix
2 parents d83cd7a + df59465 commit f4452f9

20 files changed

Lines changed: 338 additions & 9 deletions

AUDIT_LOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

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

5+
## 2026-07-01 (v0.5.9 Release: Image Support & UI Consistency)
6+
**Change:** feat(release): bump version to 0.5.9; implement image paste support and markdown image widget; align background blur and font typography across modals and timers; extract audio recording features to external project
7+
8+
**Details/Why:**
9+
1. **Version Bump**: Bumped application version to 0.5.9 across `package.json`, `src-tauri/tauri.conf.json`, and `src-tauri/Cargo.toml`. Added release note `notes/New Features in v0.5.9.md`.
10+
2. **Image Support**: Implemented image paste handler in `src/lib/editor/extensions.ts` which captures pasted images, saves them to `.images` directory via Tauri `save_asset` IPC command, and inserts markdown `![image](path)`. Rendered inline via `ImageWidget` in CodeMirror (`src/lib/editor/widgets.ts`, `markdownPlugin.ts`).
11+
3. **UI Consistency**: Standardized background blur styling across search modal and timers menu (`TimersPage.tsx`, `KeybindsModal.tsx`, `App.css`). Ensured timers menu adheres to custom app typography.
12+
4. **Product Scope Separation**: Extracted voice memo audio recording and floating indicator features into a dedicated standalone repository at `~/Projects/Memo` per user instruction.
13+
14+
**Files changed:** `package.json`, `src-tauri/tauri.conf.json`, `src-tauri/Cargo.toml`, `src/api.ts`, `src/types.d.ts`, `src/App.css`, `src/components/KeybindsModal.tsx`, `src/components/TimersPage.tsx`, `src/lib/editor/extensions.ts`, `src/lib/editor/markdownPlugin.ts`, `src/lib/editor/widgets.ts`, `src/lib/editor/widgets.test.ts`, `src-tauri/src/lib.rs`, `src-tauri/src/commands/fs.rs`, `notes/New Features in v0.5.9.md` [NEW], `CHANGELOG.md`, `AUDIT_LOG.md`.
15+
16+
---
17+
518
## 2026-06-30 (Linux Runner Pinning for `glibc` Compatibility Fix)
619
**Change:** fix(ci): pin Linux workflow runner to `ubuntu-22.04` across CI and release workflows to ensure `glibc 2.35` compatibility
720

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
11+
## [v0.5.9] - 2026-07-01
12+
13+
### Added
14+
- **Image Copy & Paste Support**: You can now copy any image and paste it directly into your notes. Pasted images are automatically saved locally in a hidden `.images` folder and rendered seamlessly inline as image cards.
15+
16+
### Changed
17+
- **Consistent Glassmorphic Backgrounds**: Standardized background blur effects across modal overlays (Command+P Search and Command+T Timers menu) to ensure consistent visual aesthetics.
18+
- **Dynamic Typography in Timers**: Ensure the Timers menu respects custom font family selections made in App Settings.
19+
1020
### Fixed
1121
- **Linux Compatibility (`glibc` version mismatch)**: Pinned GitHub Actions Linux runner to `ubuntu-22.04` across CI and release workflows instead of `ubuntu-latest` (Ubuntu 24.04). This ensures built Linux binaries and AppImages link against `glibc 2.35` so they can run out-of-the-box on Ubuntu 22.04 LTS, Debian 12, and other distributions without throwing `version glibc 2.38 not found` errors.
1222

notes/New Features in v0.5.9.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# New Features in v0.5.9
2+
3+
Welcome to PaperCache v0.5.9!
4+
5+
Here are the new features and improvements implemented in this release:
6+
- **Image Support**: You can now copy and paste images directly into your notes! Images are automatically saved locally in a hidden `.images` folder and rendered seamlessly inside the editor as beautiful image cards.
7+
- **Consistent Visual Design**: Aligned background blur styling across modal overlays (Command+P search and Command+T Timers menu) for a cohesive glassmorphic aesthetic across the app.
8+
- **Unified Typography**: Ensure the Timers menu dynamically respects and follows your chosen custom typography settings from the App Settings.
9+
- **Linux CI Workflow Fix**: Pinned Linux GitHub Action runner to `ubuntu-22.04` for dependable `glibc 2.35` build compatibility.
10+
11+
*(If you have read this note, feel free to delete it)*

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "papercache",
33
"private": true,
4-
"version": "0.5.8",
4+
"version": "0.5.9",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "papercache"
3-
version = "0.5.8"
3+
version = "0.5.9"
44
description = "A PaperCache Tauri App"
55
authors = ["Aditya Sharma"]
66
edition = "2021"

src-tauri/src/commands/fs.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
12
use serde::{Deserialize, Serialize};
23
use std::fs;
34
use std::path::{Path, PathBuf};
@@ -60,6 +61,10 @@ fn walk_dir(dir: &Path, notes: &mut Vec<Note>, base_path: &Path) -> Result<(), S
6061
let entry = entry.map_err(|e| e.to_string())?;
6162
let path = entry.path();
6263
if path.is_dir() {
64+
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
65+
if dir_name == ".images" || dir_name == ".audio" {
66+
continue;
67+
}
6368
walk_dir(&path, notes, base_path)?;
6469
} else if path.is_file() {
6570
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
@@ -496,3 +501,117 @@ pub fn run_onboarding(app: &AppHandle) {
496501
}
497502
}
498503
}
504+
505+
#[tauri::command(async)]
506+
pub async fn save_asset(data_base64: String, ext: String, folder: String) -> Result<String, String> {
507+
if folder.contains("..") || folder.contains('/') || folder.contains('\\') {
508+
return Err("Invalid folder name".to_string());
509+
}
510+
let folder_name = if folder.starts_with('.') {
511+
folder.clone()
512+
} else {
513+
format!(".{}", folder)
514+
};
515+
if folder_name != ".images" && folder_name != ".audio" {
516+
return Err("Unsupported asset folder".to_string());
517+
}
518+
519+
let base = get_papercache_dir()?;
520+
let asset_dir = base.join(&folder_name);
521+
if !asset_dir.exists() {
522+
tokio::fs::create_dir_all(&asset_dir).await.map_err(|e| e.to_string())?;
523+
}
524+
525+
let clean_ext: String = ext
526+
.trim_start_matches('.')
527+
.chars()
528+
.filter(|c| c.is_alphanumeric())
529+
.collect();
530+
let timestamp = std::time::SystemTime::now()
531+
.duration_since(std::time::UNIX_EPOCH)
532+
.map_err(|e| e.to_string())?
533+
.as_millis();
534+
let prefix = folder_name.trim_start_matches('.');
535+
536+
// Generate unique filename with random suffix to avoid collisions
537+
use rand::Rng;
538+
let mut rng = rand::thread_rng();
539+
let random_suffix: u32 = rng.gen();
540+
let filename = format!("{}_{}_{:08x}.{}", prefix, timestamp, random_suffix, clean_ext);
541+
let file_path = asset_dir.join(&filename);
542+
543+
let b64_str = if let Some(idx) = data_base64.find(',') {
544+
&data_base64[idx + 1..]
545+
} else {
546+
&data_base64
547+
};
548+
549+
let decoded = BASE64.decode(b64_str).map_err(|e| format!("Failed to decode base64: {}", e))?;
550+
tokio::fs::write(&file_path, &decoded).await.map_err(|e| e.to_string())?;
551+
552+
Ok(format!("/{}/{}", folder_name, filename))
553+
}
554+
555+
#[tauri::command(async)]
556+
pub async fn read_asset(path: String) -> Result<String, String> {
557+
let clean_path = path.trim_start_matches('/');
558+
if clean_path.contains("..") {
559+
return Err("Invalid asset path".to_string());
560+
}
561+
562+
// Read-only validation: ensure path is within allowed asset folders
563+
let path_parts: Vec<&str> = clean_path.split('/').collect();
564+
if path_parts.is_empty() {
565+
return Err("Invalid asset path".to_string());
566+
}
567+
let first_component = path_parts[0];
568+
if first_component != ".images" && first_component != ".audio" {
569+
return Err("Asset path must start with .images or .audio".to_string());
570+
}
571+
572+
let base = get_papercache_dir()?;
573+
let mut target = base.clone();
574+
for comp in clean_path.split('/') {
575+
if !comp.is_empty() && comp != "." && comp != ".." {
576+
target.push(comp);
577+
} else if comp == ".." {
578+
return Err("Path traversal detected".to_string());
579+
}
580+
}
581+
582+
// Verify the resolved path is within base without creating any directories
583+
let canonical_base = base.canonicalize().map_err(|e| e.to_string())?;
584+
if !target.exists() {
585+
return Err("Asset file not found".to_string());
586+
}
587+
let canonical_target = target.canonicalize().map_err(|e| e.to_string())?;
588+
if !canonical_target.starts_with(&canonical_base) {
589+
return Err("Path traversal detected".to_string());
590+
}
591+
592+
let file_path = canonical_target;
593+
let bytes = tokio::fs::read(&file_path).await.map_err(|e| e.to_string())?;
594+
595+
let ext = file_path
596+
.extension()
597+
.and_then(|e| e.to_str())
598+
.unwrap_or("")
599+
.to_lowercase();
600+
let mime = match ext.as_str() {
601+
"png" => "image/png",
602+
"jpg" | "jpeg" => "image/jpeg",
603+
"gif" => "image/gif",
604+
"webp" => "image/webp",
605+
"svg" => "image/svg+xml",
606+
"webm" => "audio/webm",
607+
"m4a" | "mp4" => "audio/mp4",
608+
"aac" => "audio/aac",
609+
"wav" => "audio/wav",
610+
"mp3" => "audio/mpeg",
611+
"ogg" => "audio/ogg",
612+
_ => "application/octet-stream",
613+
};
614+
615+
let encoded = BASE64.encode(&bytes);
616+
Ok(format!("data:{};base64,{}", mime, encoded))
617+
}

src-tauri/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ pub fn run() {
160160
commands::fs::export_note,
161161
commands::fs::set_dialog_open,
162162
commands::fs::remove_onboarding_files,
163+
commands::fs::save_asset,
164+
commands::fs::read_asset,
163165
commands::system::close_window,
164166
commands::system::restore_window_state,
165167
commands::system::quit_app,

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/2.0.0/tauri.schema.json",
33
"productName": "PaperCache",
4-
"version": "0.5.8",
4+
"version": "0.5.9",
55
"identifier": "com.variablethe.papercache",
66
"build": {
77
"beforeDevCommand": "npm run dev",

src/App.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,9 @@ body {
341341
left: 0;
342342
right: 0;
343343
bottom: 0;
344-
background: rgba(0, 0, 0, 0.4);
344+
background: rgba(0, 0, 0, 0.35);
345+
backdrop-filter: blur(4px);
346+
-webkit-backdrop-filter: blur(4px);
345347
z-index: 9999;
346348
display: flex;
347349
justify-content: center;
@@ -910,6 +912,7 @@ body {
910912
inset: 0;
911913
background: rgba(0, 0, 0, 0.35);
912914
backdrop-filter: blur(4px);
915+
-webkit-backdrop-filter: blur(4px);
913916
z-index: 9000;
914917
display: flex;
915918
align-items: center;

0 commit comments

Comments
 (0)