diff --git a/README.md b/README.md index 123f67b..7868215 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,8 @@ Per-release detail lives on the [changelog](https://markamd.vercel.app/changelog - [x] v1.5 core loop: context tray, file tabs, CSV preview, grouped themes, interface languages, PDF/export polish, file workflow improvements, preview link handling, and scroll memory - [x] v1.5.12 polish: smoother preview scrolling, cleaner sidebar, refreshed demo doc, quickstart tutorial updates, and smarter command palette search - [x] v1.5.18 polish: playable media previews and faster tab shortcuts +- [x] v1.6.0 workflow: separate preview windows, command-palette markdown insertions, and Traditional Chinese localization +- [x] v1.6.1 workflow: automatic folder monitoring, native file watching, and lazy markdown highlighting - [ ] next: native/silent PDF generation - [ ] next: context handoff presets for bring-your-own-ai workflows, starting with markdown and XML-tag bundle formats diff --git a/bun.lock b/bun.lock index c3fa7c2..7b55cec 100644 --- a/bun.lock +++ b/bun.lock @@ -35,11 +35,11 @@ "devDependencies": { "@tauri-apps/cli": "^2", "@types/markdown-it": "^14.1.2", - "@types/node": "^25.7.0", + "@types/node": "^26.0.0", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^5.2.0", - "typescript": "~5.9.3", + "typescript": "~7.0.2", "vite": "^7.0.4", }, }, @@ -389,7 +389,7 @@ "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], - "@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -399,6 +399,46 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], @@ -669,11 +709,11 @@ "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], - "undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], diff --git a/docs/release-notes/v1.6.0.md b/docs/release-notes/v1.6.0.md new file mode 100644 index 0000000..64de9fa --- /dev/null +++ b/docs/release-notes/v1.6.0.md @@ -0,0 +1,22 @@ +# v1.6.0 + +v1.6.0 is a workflow release for people who write and review markdown side by side. + +## added + +- Added a separate live preview window for keeping rendered markdown on another display or workspace (#97). +- Added a small floating preview-pane control for opening the separate preview window without crowding the command palette or toolbar. +- Added command palette insertions for common markdown blocks: 2x2 tables, 3x3 tables, bulleted lists, numbered lists, and fenced code blocks (#98). +- Added Traditional Chinese localization (#103). + +## improved + +- Kept layout/navigation commands grouped under workspace in the command palette, so insert commands stay easier to scan. +- Updated the welcome and help overlays for the current markdown insertion workflow. +- Preserved live preview sync when the separate preview window is reopened, focused, or already running. +- Added visible failure feedback if the separate preview window cannot be created. + +## notes + +- This release closes #97 and includes the already-closed #98 markdown insertion work. +- The signed updater setup is unchanged. diff --git a/docs/release-notes/v1.6.1.md b/docs/release-notes/v1.6.1.md new file mode 100644 index 0000000..13c0b4c --- /dev/null +++ b/docs/release-notes/v1.6.1.md @@ -0,0 +1,17 @@ +# v1.6.1 + +v1.6.1 is a small workflow and performance release for working with markdown projects. + +## added + +- Added automatic folder monitoring so open project trees refresh when files or folders are created, removed, or renamed (#110). + +## improved + +- Replaced active-file polling with native filesystem watching for faster external edit detection and lower idle work. +- Lazy-loaded Markdown syntax highlighting so documents without code blocks start with less work. +- Ignored access and metadata-only filesystem events when refreshing the active document. + +## notes + +- The signed updater setup is unchanged. diff --git a/docs/release-notes/v1.6.2.md b/docs/release-notes/v1.6.2.md new file mode 100644 index 0000000..b6a4aee --- /dev/null +++ b/docs/release-notes/v1.6.2.md @@ -0,0 +1,25 @@ +# v1.6.2 + +v1.6.2 is a stability patch for Windows startup and the native filesystem workflow. + +## fixed + +- Prevented startup hangs when a persisted folder session contains a filesystem root such as `V:\` (#118). +- Rejected filesystem roots as folder selections and skipped recursive watchers for unsafe restored paths. +- Cleared both folder-restore keys when an unsafe root is found, while preserving unrelated settings. + +## updated + +- Updated `serde` to 1.0.229. +- Updated `serde_json` to 1.0.151. +- Updated `tauri-plugin-dialog` to 2.7.2. +- Updated `tauri-plugin-single-instance` to 2.4.3. +- Updated `window-vibrancy` to 0.8.0. + +## thanks + +- Thanks to [@Icatme](https://github.com/Icatme) for the Windows reproduction, diagnosis, fix, and regression tests. + +## notes + +- The signed updater setup is unchanged. diff --git a/package.json b/package.json index 70d8701..f9fb251 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "marka-md", "private": true, - "version": "1.5.18", + "version": "1.6.2", "type": "module", "scripts": { "dev": "vite", @@ -48,7 +48,7 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^5.2.0", - "typescript": "~5.9.3", + "typescript": "~7.0.2", "vite": "^7.0.4" } } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f65651a..406eb21 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -975,6 +975,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "filetime" version = "0.2.29" @@ -1055,6 +1064,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -1596,7 +1614,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -1765,6 +1783,26 @@ dependencies = [ "cfb", ] +[[package]] +name = "inotify" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +dependencies = [ + "bitflags 2.11.1", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1938,6 +1976,26 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.11.1", + "libc", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -2031,7 +2089,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "marka" -version = "1.5.18" +version = "1.6.2" dependencies = [ "serde", "serde_json", @@ -2043,7 +2101,7 @@ dependencies = [ "tauri-plugin-process", "tauri-plugin-single-instance", "tauri-plugin-updater", - "window-vibrancy 0.7.1", + "window-vibrancy 0.8.0", ] [[package]] @@ -2101,6 +2159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -2156,6 +2215,47 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.1", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375bd3a138be7bfeff3480e4a623df4cbfb55b79df617c055cd810ba466fa078" +dependencies = [ + "file-id", + "log", + "notify", + "notify-types", + "walkdir", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.1", + "serde", +] + [[package]] name = "num-conv" version = "0.2.1" @@ -2214,6 +2314,7 @@ dependencies = [ "objc2", "objc2-core-foundation", "objc2-foundation", + "objc2-quartz-core", ] [[package]] @@ -3161,9 +3262,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -3183,22 +3284,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.2", ] [[package]] @@ -3214,9 +3315,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -3256,9 +3357,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", "bs58", @@ -3276,9 +3377,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", @@ -3516,6 +3617,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3584,7 +3696,7 @@ dependencies = [ "unicode-segmentation", "url", "windows", - "windows-core 0.61.2", + "windows-core", "windows-version", "x11-dl", ] @@ -3748,9 +3860,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" dependencies = [ "log", "raw-window-handle", @@ -3774,6 +3886,8 @@ dependencies = [ "dunce", "glob", "log", + "notify", + "notify-debouncer-full", "objc2-foundation", "percent-encoding", "schemars 0.8.22", @@ -3822,14 +3936,15 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.4.2" +version = "2.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +checksum = "b3214becf9ef5783c0ae99a3bb25adf5353a7a16ebf53e74b909e29205735c6c" dependencies = [ "serde", "serde_json", "tauri", "thiserror 2.0.18", + "tokio", "tracing", "windows-sys 0.60.2", "zbus", @@ -4746,7 +4861,7 @@ dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows", - "windows-core 0.61.2", + "windows-core", "windows-implement", "windows-interface", ] @@ -4770,7 +4885,7 @@ checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", "windows", - "windows-core 0.61.2", + "windows-core", ] [[package]] @@ -4821,14 +4936,15 @@ dependencies = [ [[package]] name = "window-vibrancy" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010797bd7c40396fbc59d3105089fed0885fe267a0ef4a0a4646df54e28647f6" +checksum = "fe6461139557c2245c3b9b34de2092e7af39460bf97ef4723bc5feb0cf66f831" dependencies = [ "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", "windows-sys 0.60.2", "windows-version", @@ -4841,7 +4957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -4853,7 +4969,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.2", + "windows-core", ] [[package]] @@ -4865,21 +4981,8 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-result", + "windows-strings", ] [[package]] @@ -4888,7 +4991,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link 0.1.3", "windows-threading", ] @@ -4933,7 +5036,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link 0.1.3", ] @@ -4946,15 +5049,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -4964,15 +5058,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -5395,7 +5480,7 @@ dependencies = [ "webkit2gtk-sys", "webview2-com", "windows", - "windows-core 0.61.2", + "windows-core", "windows-version", "x11-dl", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f18967b..0ab7314 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "marka" -version = "1.5.18" +version = "1.6.2" description = "marka.md — a local markdown editor for the notes you share with ai" authors = ["Matt Enarle"] edition = "2021" @@ -21,7 +21,7 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = ["macos-private-api"] } tauri-plugin-single-instance = "2" tauri-plugin-opener = "2" -tauri-plugin-fs = "2" +tauri-plugin-fs = { version = "2", features = ["watch"] } tauri-plugin-dialog = "2" tauri-plugin-updater = "2" tauri-plugin-process = "2" @@ -29,4 +29,4 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" [target.'cfg(target_os = "macos")'.dependencies] -window-vibrancy = "0.7" +window-vibrancy = "0.8" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 2b80841..5419b61 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,10 +2,15 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main"], + "windows": ["main", "preview"], "permissions": [ "core:default", + "core:webview:allow-create-webview-window", "core:window:allow-start-dragging", + "core:window:allow-show", + "core:window:allow-unminimize", + "core:window:allow-set-focus", + "core:window:allow-set-title", "core:window:allow-set-fullscreen", "core:window:allow-is-fullscreen", "core:window:allow-toggle-maximize", @@ -47,6 +52,10 @@ "identifier": "fs:allow-read-dir", "allow": [{ "path": "**" }] }, + { + "identifier": "fs:allow-watch", + "allow": [{ "path": "**" }] + }, { "identifier": "fs:allow-write-text-file", "allow": [{ "path": "**" }] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index adc41eb..bec8166 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -196,6 +196,9 @@ fn complete_wait_sessions(markers: Vec) -> Result<(), String> { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + #[cfg(target_os = "linux")] + configure_linux_graphics(); + if handle_wait_client() { return; } @@ -280,3 +283,15 @@ pub fn run() { } }); } + +#[cfg(target_os = "linux")] +fn configure_linux_graphics() { + // WebKitGTK can fail to initialize EGL on some Wayland + Mesa setups. + // Keep an explicit user setting intact while using the software-backed + // path as the default for the affected session type. + if std::env::var_os("XDG_SESSION_TYPE").as_deref() == Some(std::ffi::OsStr::new("wayland")) + && std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() + { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8a38c7f..10feba3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "marka.md", "mainBinaryName": "marka.md", - "version": "1.5.18", + "version": "1.6.2", "identifier": "com.mattenarle.markamd", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/app.css b/src/app.css index 0abf8e5..918266d 100644 --- a/src/app.css +++ b/src/app.css @@ -8,6 +8,7 @@ @import "./styles/editor/panes.css"; @import "./styles/editor/prose.css"; @import "./styles/editor/reading-find.css"; +@import "./styles/editor/toc.css"; @import "./styles/files/sidebar.css"; @import "./styles/overlays/overlay.css"; @import "./styles/overlays/palette.css"; @@ -15,6 +16,7 @@ @import "./styles/overlays/welcome.css"; @import "./styles/overlays/drop.css"; @import "./styles/overlays/about.css"; +@import "./styles/overlays/file-preview.css"; /* shell layout */ .mdv-app { @@ -44,6 +46,23 @@ .mdv-app > .mdv-shell { grid-row: 3; } .mdv-app > .mdv-statusbar { grid-row: 4; } +.mdv-preview-window { + --mdv-prose-font-family: var(--font-ui); + --mdv-prose-font-size: 15px; + --mdv-prose-line-height: 1.65; + --mdv-reading-content-width: 880px; + --mdv-reading-prose-width: 720px; + height: 100vh; + width: 100vw; + background: var(--bg); + color: var(--fg); +} + +.mdv-preview-window > .mdv-preview { + height: 100%; + padding: 42px max(36px, calc((100% - var(--mdv-reading-content-width, 880px)) / 2)) 72px; +} + /* titlebar hidden mode */ .mdv-app.has-hidden-titlebar { grid-template-rows: 0 var(--breadcrumb-h) 1fr var(--statusbar-h); @@ -109,6 +128,28 @@ html.is-mac .mdv-app.has-hidden-titlebar:not(.is-reading) { to { opacity: 1; transform: translateY(0); } } +/* reading width = "full": content fills the pane and multi-column tables + span full width (no horizontal scrollbar). Applies in both the split + preview pane and reading mode. */ +.mdv-app.is-reading-width-full .mdv-preview { + padding-left: 16px; + padding-right: 16px; +} +.mdv-app.is-reading-width-full .mdv-prose { + max-width: none; +} +.mdv-app.is-reading-width-full .mdv-prose table { + width: 100%; +} +/* reading mode's own width/padding rules are more specific, so match them */ +.mdv-app.is-reading-width-full.is-reading .mdv-shell > .mdv-preview { + padding-left: 16px; + padding-right: 16px; +} +.mdv-app.is-reading-width-full.is-reading .mdv-shell > .mdv-preview .mdv-prose { + max-width: none; +} + /* theme transition smoothing — common surfaces fade between palettes */ .mdv-titlebar, .mdv-statusbar, diff --git a/src/app.tsx b/src/app.tsx index 7b05af2..90203b7 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,15 +1,16 @@ import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react"; import type { EditorView } from "@codemirror/view"; import { Breadcrumb, StatusBar, TitleBar, type VimMode } from "@/components/chrome"; -import { Editor, OpenTabs, Preview, ReadingFind, Splitter } from "@/components/editor"; +import { Editor, OpenTabs, Preview, ReadingFind, Splitter, TocPanel } from "@/components/editor"; import { ContextMenu, Sidebar, type ContextMenuItem } from "@/components/files"; -import { AboutOverlay, CommandPalette, DropOverlay, HelpOverlay, Toast, WelcomeOverlay } from "@/components/overlays"; +import { AboutOverlay, CommandPalette, DropOverlay, FilePreviewOverlay, HelpOverlay, Toast, WelcomeOverlay } from "@/components/overlays"; import { TooltipRoot } from "@/components/primitives"; import { useContextMenu, useDebouncedValue, useFileOps, useFileSession, + useFolderWatcher, type LoadError, useNotifications, useOverlays, @@ -21,8 +22,9 @@ import { useUpdateFlow, } from "@/hooks"; import { getCurrentWindow } from "@tauri-apps/api/window"; +import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; import { getVersion } from "@tauri-apps/api/app"; -import { listen } from "@tauri-apps/api/event"; +import { emitTo, listen } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/core"; import { openPath, openUrl } from "@tauri-apps/plugin-opener"; import { @@ -36,7 +38,9 @@ import { getWritingDisplayVars, getContextBundleStats, getWhatsNewToastMessage, + isFilesystemRoot, isSupportedTextPath, + markdownInsertion, normalizeProseFontFamily, normalizeReadingFontSize, normalizeReadingWidth, @@ -50,6 +54,7 @@ import { removeEntry, STORAGE_KEYS, useI18n, + type MarkdownInsertion, type ProseFontFamily, type ReadingFontSize, type ReadingWidth, @@ -150,15 +155,55 @@ export function App() { }); }, []); - const handleOpenFileRequest = useCallback(async (request: OpenFileRequest) => { - if (typeof request === "string") { - if (request.length > 0) await loadFile(request); - return; - } - if (request?.path) { - await loadFile(request.path, { waitMarker: request.waitMarker }); - } - }, [loadFile]); + const [previewPath, setPreviewPath] = useState(null); + const openPreview = useCallback((path: string) => setPreviewPath(path), []); + const closePreview = useCallback(() => setPreviewPath(null), []); + + // .md/.csv open in the editor; every other file opens the quick-preview overlay. + const handleSelectFile = useCallback( + (path: string) => { + if (isSupportedTextPath(path)) { + void loadFile(path); + } else { + openPreview(path); + } + }, + [loadFile, openPreview], + ); + + // Space previews the focused sidebar file (Finder Quick Look style). + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key !== " " || e.metaKey || e.ctrlKey || e.altKey) return; + const row = (document.activeElement as HTMLElement | null)?.closest( + "[data-mdv-tree-path]", + ) as HTMLElement | null; + const p = row?.dataset.mdvTreePath; + // let Space activate the row (open in editor) for editable files; preview otherwise. + if (!p || isSupportedTextPath(p)) return; + e.preventDefault(); + openPreview(p); + }; + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [openPreview]); + + const handleOpenFileRequest = useCallback( + async (request: OpenFileRequest) => { + const reqPath = typeof request === "string" ? request : request?.path; + if (!reqPath) return; + if (isSupportedTextPath(reqPath)) { + const waitMarker = typeof request === "string" ? null : request.waitMarker ?? null; + await loadFile(reqPath, waitMarker ? { waitMarker } : {}); + } else { + // preview isn't an editing session — release any --wait client at once. + openPreview(reqPath); + const waitMarker = typeof request === "string" ? null : request.waitMarker ?? null; + if (waitMarker) completeWaitSessions([waitMarker]); + } + }, + [loadFile, openPreview, completeWaitSessions], + ); useEffect(() => { waitMarkersRef.current = tabs.flatMap((tab) => tab.waitMarkers); @@ -266,6 +311,7 @@ export function App() { startNewBuffer, onError: setLoadError, }); + useFolderWatcher(folders, bumpTree); const { paletteOpen, @@ -409,6 +455,10 @@ export function App() { const [findOpen, setFindOpen] = useState(false); const [findFocusRequest, setFindFocusRequest] = useState(0); const [proseEl, setProseEl] = useState(null); + const [tocVisible, setTocVisible] = usePersistedState( + STORAGE_KEYS.tocVisible, + false, + ); useEffect(() => { if (!readingMode) { setProseEl(null); @@ -423,6 +473,73 @@ export function App() { }, [readingMode]); const copyMarkdown = useCallback(() => copyMarkdownCore(source), [copyMarkdownCore, source]); + const previewWindowTitle = useMemo(() => activePath ? basename(activePath) : "untitled", [activePath]); + const sendPreviewWindowState = useCallback(async () => { + await emitTo("preview", "marka:preview-state", { + source, + filePath: activePath, + title: previewWindowTitle, + }); + }, [activePath, previewWindowTitle, source]); + + const openPreviewWindow = useCallback(async () => { + try { + const existing = await WebviewWindow.getByLabel("preview"); + if (existing) { + await existing.show(); + await existing.unminimize(); + await existing.setFocus(); + await sendPreviewWindowState(); + return; + } + + const preview = new WebviewWindow("preview", { + url: "index.html?window=preview", + title: `${previewWindowTitle} - preview`, + width: 900, + height: 720, + minWidth: 520, + minHeight: 360, + decorations: true, + focus: true, + }); + + await new Promise((resolve, reject) => { + let settled = false; + const unlisten: Array<() => void> = []; + const settle = (next: () => void) => { + if (settled) return; + settled = true; + unlisten.forEach((fn) => fn()); + next(); + }; + + void preview.once("tauri://created", () => settle(resolve)).then((fn) => unlisten.push(fn)); + void preview.once("tauri://error", (event) => { + settle(() => reject(event.payload)); + }).then((fn) => unlisten.push(fn)); + }); + } catch (err) { + console.warn("marka.md: preview window failed", err); + showSaveAsToast(t("title.openPreviewWindowFailed")); + } + }, [previewWindowTitle, sendPreviewWindowState, showSaveAsToast, t]); + + useEffect(() => { + let unlisten: (() => void) | undefined; + void listen("marka:preview-ready", () => { + void sendPreviewWindowState().catch(() => undefined); + }).then((fn) => { + unlisten = fn; + }); + return () => { + unlisten?.(); + }; + }, [sendPreviewWindowState]); + + useEffect(() => { + void sendPreviewWindowState().catch(() => undefined); + }, [sendPreviewWindowState]); const toggleStagedPath = useCallback((path: string) => { setStagedPaths((prev) => @@ -505,6 +622,28 @@ export function App() { }, []); const editorViewRef = useRef(null); + const insertMarkdown = useCallback((kind: MarkdownInsertion) => { + const view = editorViewRef.current; + if (!view) { + const insertion = markdownInsertion(kind); + const prefix = source.length > 0 && !source.endsWith("\n") ? "\n\n" : ""; + setSource(`${source}${prefix}${insertion.text}`); + return; + } + + const { from, to } = view.state.selection.main; + const selected = view.state.sliceDoc(from, to); + const insertion = markdownInsertion(kind, selected); + view.dispatch({ + changes: { from, to, insert: insertion.text }, + selection: { + anchor: from + insertion.selectionFrom, + head: from + insertion.selectionTo, + }, + scrollIntoView: true, + }); + view.focus(); + }, [setSource, source]); // proportional editor <-> preview scroll sync; rebinds when active file changes useSyncScroll({ rebindKey: activePath ?? "untitled" }); @@ -530,9 +669,13 @@ export function App() { const handleOpenFolder = useCallback(async () => { const folder = await pickFolder(); if (!folder) return; + if (isFilesystemRoot(folder)) { + setLoadError({ message: t("app.folderRootUnsupported") }); + return; + } setFolders((prev) => (prev.includes(folder) ? prev : [...prev, folder])); setSidebarOpen(true); - }, [setFolders, setSidebarOpen]); + }, [setFolders, setLoadError, setSidebarOpen, t]); const handleOpenFile = useCallback(async () => { const file = await pickMarkdownFile(); @@ -554,27 +697,33 @@ export function App() { const contextItems = useMemo(() => { if (!contextMenu) return []; const { path, isDir } = contextMenu; - const items: ContextMenuItem[] = [ - { - label: t("menu.rename"), - onSelect: () => setEditingPath(path), - }, - "divider", - { - label: t("menu.copyPath"), - onSelect: () => { - void navigator.clipboard.writeText(path); - showSaveAsToast(t("menu.pathCopied")); - }, + const items: ContextMenuItem[] = []; + if (!isDir && !isSupportedTextPath(path)) { + items.push({ + label: t("menu.quickPreview"), + onSelect: () => openPreview(path), + }); + items.push("divider"); + } + items.push({ + label: t("menu.rename"), + onSelect: () => setEditingPath(path), + }); + items.push("divider"); + items.push({ + label: t("menu.copyPath"), + onSelect: () => { + void navigator.clipboard.writeText(path); + showSaveAsToast(t("menu.pathCopied")); }, - { - label: t("menu.copyRelativePath"), - onSelect: () => { - void navigator.clipboard.writeText(relativePath(path, rootPath)); - showSaveAsToast(t("menu.pathCopied")); - }, + }); + items.push({ + label: t("menu.copyRelativePath"), + onSelect: () => { + void navigator.clipboard.writeText(relativePath(path, rootPath)); + showSaveAsToast(t("menu.pathCopied")); }, - ]; + }); items.push("divider"); items.push({ label: t("menu.revealExplorer"), @@ -626,7 +775,7 @@ export function App() { }, }); return items; - }, [contextMenu, activePath, setActivePath, bumpTree, t]); + }, [contextMenu, activePath, setActivePath, bumpTree, openPreview, t]); // OS "Open With → marka.md" and CLI launches — Rust emits marka:open-file. useEffect(() => { @@ -867,6 +1016,7 @@ export function App() { copyContextBundle, clearContextBundle, exportToPdf, + insertMarkdown, toggleFullscreen, openRecent: (path: string) => void loadFile(path), recentFiles, @@ -875,6 +1025,8 @@ export function App() { readingMode, editorOnly, toggleEditorOnly, + tocVisible, + toggleToc: () => setTocVisible((v) => !v), contextCount: stagedPaths.length, }, t), [ @@ -896,11 +1048,13 @@ export function App() { handleUndoFileOp, handleManualUpdateCheck, exportToPdf, + insertMarkdown, toggleFullscreen, handleToggleSidebar, loadFile, recentFiles, stagedPaths.length, + tocVisible, t, ], ); @@ -909,7 +1063,7 @@ export function App() { return (
setTocVisible((v) => !v)} /> {readingMode ? ( <> - + + void loadFile(path)} + onSelectFile={handleSelectFile} onMove={handleMove} onContextMenu={handleContextMenu} stagedPaths={stagedPaths} @@ -1017,7 +1178,7 @@ export function App() { ) : ( } - right={} + right={} /> )}
@@ -1165,6 +1326,15 @@ export function App() { /> + + { + closePreview(); + void loadPlainTextFile(p); + }} + /> void; onProseFontFamilyChange: (value: ProseFontFamily) => void; onResetWritingDisplay: () => void; + tocVisible?: boolean; + onToggleToc?: () => void; }; export function TitleBar({ @@ -53,6 +55,8 @@ export function TitleBar({ onReadingWidthChange, onProseFontFamilyChange, onResetWritingDisplay, + tocVisible = false, + onToggleToc, }: TitleBarProps) { const { t } = useI18n(); const readingFontIndex = Math.max( @@ -133,6 +137,15 @@ export function TitleBar({ onResetWritingDisplay={onResetWritingDisplay} /> ) : null} + {readingMode && onToggleToc ? ( + + + ))} + + {items.length === 0 ? ( + {t("toc.empty")} + ) : null} + + ); +} diff --git a/src/components/files/folder-node.tsx b/src/components/files/folder-node.tsx index f7615ff..cbbdbf1 100644 --- a/src/components/files/folder-node.tsx +++ b/src/components/files/folder-node.tsx @@ -195,6 +195,7 @@ export function FileNode({ onClick={handleClick} onContextMenu={onCtx} onDragStart={onDragStart} + data-mdv-tree-path={entry.path} title={entry.path} > diff --git a/src/components/overlays/file-preview-overlay.tsx b/src/components/overlays/file-preview-overlay.tsx new file mode 100644 index 0000000..305600c --- /dev/null +++ b/src/components/overlays/file-preview-overlay.tsx @@ -0,0 +1,415 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { readFile, readTextFile, stat } from "@tauri-apps/plugin-fs"; +import { openPath } from "@tauri-apps/plugin-opener"; +import { useI18n } from "@/lib"; +import { basename, isSupportedTextPath, previewKindForPath, previewMimeForPath, validatePlainTextFile } from "@/lib"; + +const CLOSE_ICON_SVG = ``; +const EYE_ICON_SVG = ``; +const ZOOM_IN_ICON_SVG = ``; +const ZOOM_OUT_ICON_SVG = ``; +const FIT_ICON_SVG = ``; +const ACTUAL_ICON_SVG = ``; + +const MIN_SCALE = 0.05; +const MAX_SCALE = 8; + +function clampScale(s: number): number { + return Math.min(MAX_SCALE, Math.max(MIN_SCALE, s)); +} + +function bytesToBase64(bytes: Uint8Array): string { + const chunks: string[] = []; + const CHUNK = 8192; + for (let i = 0; i < bytes.byteLength; i += CHUNK) { + chunks.push(String.fromCharCode(...bytes.subarray(i, i + CHUNK))); + } + return btoa(chunks.join("")); +} + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +type LoadStatus = "loading" | "ready" | "error"; + +type Props = { + path: string | null; + onClose: () => void; + onOpenAsText: (path: string) => void; +}; + +export function FilePreviewOverlay({ path, onClose, onOpenAsText }: Props) { + const { t } = useI18n(); + const kind = useMemo( + () => (path ? previewKindForPath(path) : "unsupported"), + [path], + ); + + const [status, setStatus] = useState("loading"); + const [url, setUrl] = useState(null); + const [text, setText] = useState(""); + const [size, setSize] = useState(null); + const [errMsg, setErrMsg] = useState(""); + + const urlRef = useRef(null); + + const revokeUrl = useCallback(() => { + if (urlRef.current && urlRef.current.startsWith("blob:")) { + URL.revokeObjectURL(urlRef.current); + } + urlRef.current = null; + }, []); + + // Esc to close. + useEffect(() => { + if (!path) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onClose(); + } + }; + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [path, onClose]); + + // Load the file payload whenever the target changes. + useEffect(() => { + if (!path) return; + let cancelled = false; + revokeUrl(); + setUrl(null); + setText(""); + setSize(null); + setErrMsg(""); + setStatus("loading"); + + void (async () => { + try { + if (kind === "office" || kind === "unsupported") { + let s: number | null = null; + try { + s = (await stat(path)).size; + } catch { + s = null; + } + if (cancelled) return; + setSize(s); + setStatus("ready"); + return; + } + + if (kind === "text") { + const check = await validatePlainTextFile(path); + if (cancelled) return; + if (!check.ok) { + setErrMsg(check.reason); + setStatus("error"); + return; + } + const content = await readTextFile(path); + if (cancelled) return; + setText(content); + setStatus("ready"); + return; + } + + // image / video / audio / pdf — read bytes once. + const bytes = await readFile(path); + if (cancelled) return; + if (kind === "pdf") { + const dataUrl = `data:application/pdf;base64,${bytesToBase64(bytes)}`; + urlRef.current = dataUrl; + setUrl(dataUrl); + } else { + const blobUrl = URL.createObjectURL( + new Blob([bytes], { type: previewMimeForPath(path) }), + ); + urlRef.current = blobUrl; + setUrl(blobUrl); + } + setStatus("ready"); + } catch (err) { + if (cancelled) return; + console.error("marka.md: preview load failed", err); + setErrMsg(err instanceof Error ? err.message : String(err)); + setStatus("error"); + } + })(); + + return () => { + cancelled = true; + }; + }, [path, kind, revokeUrl]); + + // Revoke any blob URL on unmount. + useEffect(() => revokeUrl, [revokeUrl]); + + if (!path) return null; + + const title = basename(path); + const showOpenAsText = kind === "text" && isSupportedTextPath(path) === false; + + return ( +
+
+
+ + + {t("preview.badge")} + + {t("preview.escHint")} +
+ {title} +
+ {showOpenAsText ? ( + + ) : null} + + +
+
+ +
+ {status === "loading" ? ( +
{t("preview.loading")}
+ ) : status === "error" ? ( +
+ {errMsg || t("preview.loadFailed")} +
+ ) : kind === "image" && url ? ( + + ) : kind === "video" && url ? ( +
+
+ ) : kind === "audio" && url ? ( +
+ {title} +
+ ) : kind === "pdf" && url ? ( +