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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,30 +71,30 @@ bind = SUPER, E, exec, whale-cabinet
Needs Rust (via [rustup](https://rustup.rs)), Node ≥ 20, WebKitGTK 4.1 and the usual build tools.
Required at runtime (install.sh checks): zoxide (jump box, Frequent), fd (quick open), git (badges), gvfs with its
smb/dav/nfs backends (network locations) and polkit (administrator actions).
The optional tools light up extra features: poppler (PDF previews), ffmpegthumbnailer + ffmpeg (video thumbnails,
The optional tools light up extra features: poppler (PDF previews), libvips (HEIC and camera RAW/DNG photos), ffmpegthumbnailer + ffmpeg (video thumbnails,
media details), libarchive/zip/7-Zip (compress/extract), wl-clipboard (copy/paste with other apps), udisks2 (devices),
libsecret's secret-tool (remembering network passwords).

**Arch / CachyOS / Manjaro**
```sh
sudo pacman -S --needed base-devel webkit2gtk-4.1 curl wget file openssl librsvg libappindicator-gtk3 nodejs npm rustup
sudo pacman -S --needed zoxide fd git gvfs gvfs-smb gvfs-dnssd gvfs-nfs polkit
sudo pacman -S --needed poppler ffmpegthumbnailer ffmpeg libarchive zip 7zip wl-clipboard udisks2 # optional
sudo pacman -S --needed poppler libvips libheif libraw ffmpegthumbnailer ffmpeg libarchive zip 7zip wl-clipboard udisks2 # optional
```

**Fedora**
```sh
sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file libappindicator-gtk3-devel librsvg2-devel libxdo-devel nodejs npm
sudo dnf group install c-development
sudo dnf install zoxide fd-find git gvfs gvfs-smb gvfs-fuse polkit
sudo dnf install poppler-utils ffmpegthumbnailer ffmpeg-free bsdtar zip p7zip wl-clipboard udisks2 # optional
sudo dnf install poppler-utils vips-tools vips-heif ffmpegthumbnailer ffmpeg-free bsdtar zip p7zip wl-clipboard udisks2 # optional
```

**Debian / Ubuntu**
```sh
sudo apt install build-essential libwebkit2gtk-4.1-dev libssl-dev libxdo-dev libayatana-appindicator3-dev librsvg2-dev curl wget file nodejs npm
sudo apt install zoxide fd-find git gvfs gvfs-backends gvfs-fuse pkexec
sudo apt install poppler-utils ffmpegthumbnailer ffmpeg libarchive-tools zip 7zip wl-clipboard udisks2 # optional
sudo apt install poppler-utils libvips-tools ffmpegthumbnailer ffmpeg libarchive-tools zip 7zip wl-clipboard udisks2 # optional
```

Then:
Expand Down Expand Up @@ -287,7 +287,7 @@ selects the extra copies keeping the newest, the oldest or the one in a folder y

- Image, video and PDF thumbnails load lazily in the list and grid, using the shared freedesktop cache
(`~/.cache/thumbnails`, same as Dolphin/Nautilus). Missing ones are generated (videos need `ffmpegthumbnailer`,
PDFs `pdftoppm` from poppler).
PDFs `pdftoppm` from poppler, HEIC and RAW/DNG photos `vipsthumbnail` from libvips).
- The info panel (F11) previews the selection: images, video/audio players, the first page of PDFs,
syntax-highlighted text/code, and rendered Markdown (tables, task lists, code blocks, relative images and links;
"View source" toggles). Markdown is sanitized before it's shown.
Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "whale-cabinet",
"private": true,
"version": "1.0.0",
"version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "whale-cabinet"
version = "1.0.0"
version = "1.1.0"
edition = "2021"
description = "A Linux file manager with a flat filing-cabinet look"
license = "GPL-3.0-or-later"
Expand Down
22 changes: 19 additions & 3 deletions src-tauri/src/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ fn kind(path: &Path) -> &'static str {
"png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "tif" | "tiff" | "ico" => "image",
"mp4" | "mkv" | "webm" | "avi" | "mov" | "wmv" | "flv" | "m4v" | "mpg" | "mpeg" => "video",
"pdf" => "pdf",
// image-rs can't decode these; libvips can (libheif / libraw), and applies the EXIF rotation
"heic" | "heif" | "dng" | "raw" | "cr2" | "cr3" | "nef" | "arw" | "raf" | "orf" | "rw2" | "pef" | "srw" => "photo",
_ => match infer::get_from_path(path).ok().flatten().map(|t| t.matcher_type()) {
Some(infer::MatcherType::Image) => "image",
Some(infer::MatcherType::Video) => "video",
Expand All @@ -45,18 +47,20 @@ fn kind(path: &Path) -> &'static str {
fn render(src: &Path, size: u32) -> Result<image::DynamicImage, String> {
match kind(src) {
"image" => image::open(src).map(|i| i.thumbnail(size, size)).map_err(|e| e.to_string()),
k @ ("video" | "pdf") => {
k @ ("video" | "pdf" | "photo") => {
let tmp = tempfile_path();
let ok = if k == "video" {
Command::new("ffmpegthumbnailer").arg("-i").arg(src).arg("-o").arg(&tmp).args(["-s", &size.to_string(), "-c", "png"]).output()
} else if k == "photo" {
Command::new("vipsthumbnail").arg(src).arg("-o").arg(&tmp).args(["-s", &format!("{size}>")]).output() // ">": shrink only
} else {
// pdftoppm appends ".png" to the output root with -singlefile
Command::new("pdftoppm").args(["-png", "-singlefile", "-f", "1", "-l", "1", "-scale-to", &size.to_string()]).arg(src).arg(tmp.with_extension("")).output()
}
.map_err(|e| format!("{e} (is {} installed?)", if k == "video" { "ffmpegthumbnailer" } else { "poppler" }))?;
.map_err(|e| format!("{e} (is {} installed?)", match k { "video" => "ffmpegthumbnailer", "photo" => "libvips", _ => "poppler" }))?;
let img = image::open(&tmp).map_err(|e| format!("{}: {e}", String::from_utf8_lossy(&ok.stderr).trim()));
let _ = fs::remove_file(&tmp);
img.map(|i| i.thumbnail(size, size))
if k == "photo" { img } else { img.map(|i| i.thumbnail(size, size)) }
}
_ => Err("no thumbnailer for this type".into()),
}
Expand Down Expand Up @@ -264,6 +268,18 @@ trailer<</Root 1 0 R>>\n%%EOF\n").unwrap();
assert!(thumbnail(&root, &img, 800).unwrap().starts_with(root.join("whale-cabinet/previews/800")));
}

#[test]
fn heic_through_libvips() {
let t = tempfile::tempdir().unwrap();
let (png, heic) = (t.path().join("a.png"), t.path().join("IMG_0001.HEIC"));
image::DynamicImage::new_rgb8(600, 300).save(&png).unwrap();
if Command::new("vips").arg("copy").arg(&png).arg(&heic).status().map_or(true, |s| !s.success()) {
return eprintln!("skipped: no libvips with HEIF");
}
let th = thumbnail(&t.path().join("cache"), &heic, 1200).unwrap();
assert_eq!(image::image_dimensions(&th).unwrap(), (600, 300), "never upscaled");
}

#[test]
fn failures_are_remembered() {
let t = tempfile::tempdir().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"productName": "Whale Cabinet",
"mainBinaryName": "whale-cabinet",
"version": "1.0.0",
"version": "1.1.0",
"identifier": "whale-cabinet",
"build": {
"beforeDevCommand": "npm run dev",
Expand Down
11 changes: 8 additions & 3 deletions src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ export function copyButton(text: () => string, label = "Copy") {
}
const tools = (...bs: HTMLElement[]) => { const t = document.createElement("div"); t.className = "pv-tools"; t.append(...bs); return t; };

// WebKit can't show these itself (they still get thumbnails where a thumbnailer can)
const NO_WEB = new Set(["heic", "heif", "raw", "cr2", "nef", "tif", "tiff"]);
// WebKit can't show these itself: the viewer shows a big decoded PNG from the thumbnailer (libvips / image-rs)
const NO_WEB = new Set("heic heif raw dng cr2 cr3 nef arw raf orf rw2 pef srw tif tiff".split(" "));
const TEXTY = new Set(["pdf", "md", "markdown", "txt", "log", "csv", "conf", "ini", "desktop"]);
/** Can the viewer show this one properly? (decides "open in the viewer" for Enter / double-click) */
export const previewable = (e: Entry) => !e.dir && !e.special && !e.broken && !e.trashId &&
(!!e.preview || (["img", "vid", "aud", "code"].includes(kindOf(e)) && !NO_WEB.has(ext(e))) || TEXTY.has(ext(e)));
(!!e.preview || ["img", "vid", "aud", "code"].includes(kindOf(e)) || TEXTY.has(ext(e)));

/** Every page of a PDF, each rendered (poppler) as it scrolls near. */
async function pdfPages(e: Entry) {
Expand Down Expand Up @@ -178,6 +178,11 @@ async function previewOf(e: Entry, big: boolean): Promise<Big | null> {
const url = assetUrl(e.path);
if (e.preview) { const i = new Image(); i.src = e.preview; i.draggable = false; return { el: i }; }
if (e.dir || e.special || e.broken) return null;
if (k === "img" && NO_WEB.has(x)) {
const p = await invoke<string>("thumbnail", { path: e.path, size: big ? 2400 : 600 }).catch(() => "");
if (!p) return null;
const i = new Image(); i.src = assetUrl(p); i.draggable = false; return { el: i };
}
if (k === "img") { const i = new Image(); i.src = url; i.draggable = false; return { el: i }; }
if (k === "vid") { const v = document.createElement("video"); v.src = url; v.controls = true; v.preload = "metadata"; return { el: v }; }
if (k === "aud") { const a = document.createElement("audio"); a.src = url; a.controls = true; a.preload = "metadata"; return { el: a }; }
Expand Down
2 changes: 1 addition & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const fmtDate = (ms: number) => (ms ? dateFmt.format(ms) : "");

export const KINDS: Record<string, string> = {};
for (const [k, list] of Object.entries({
img: "png jpg jpeg gif webp svg bmp tiff tif avif heic ico jxl raw cr2 nef",
img: "png jpg jpeg gif webp svg bmp tiff tif avif heic heif ico jxl raw dng cr2 cr3 nef arw raf orf rw2 pef srw",
vid: "mp4 mkv webm avi mov wmv flv m4v mpg mpeg",
aud: "mp3 flac ogg opus wav m4a aac wma",
arc: "zip tar gz xz bz2 zst 7z rar tgz iso deb rpm appimage",
Expand Down
Loading