Feat: Open in File Explorer

This commit is contained in:
Youwes09
2026-04-11 23:04:26 -05:00
parent 4a299f60ac
commit 49562c3f76
2 changed files with 77 additions and 2 deletions
+28
View File
@@ -587,6 +587,33 @@ fn restart_app(app: tauri::AppHandle) {
tauri::process::restart(&app.env()); tauri::process::restart(&app.env());
} }
#[tauri::command]
fn open_path(path: String) -> Result<(), String> {
let p = std::path::Path::new(path.trim());
#[cfg(target_os = "windows")]
{
std::process::Command::new("explorer")
.arg(p)
.spawn()
.map_err(|e| e.to_string())?;
}
#[cfg(target_os = "macos")]
{
std::process::Command::new("open")
.arg(p)
.spawn()
.map_err(|e| e.to_string())?;
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
std::process::Command::new("xdg-open")
.arg(p)
.spawn()
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
@@ -610,6 +637,7 @@ pub fn run() {
list_releases, list_releases,
download_and_install_update, download_and_install_update,
restart_app, restart_app,
open_path,
]) ])
.setup(|_app| Ok(())) .setup(|_app| Ok(()))
.on_window_event(|window, event| { .on_window_event(|window, event| {
+49 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, untrack } from "svelte"; import { onMount, untrack } from "svelte";
import { MagnifyingGlass, Books, DownloadSimple, Folder, FolderSimplePlus, Trash, Star, CheckSquare, X, ArrowSquareOut, SortAscending, Funnel, CaretUp, CaretDown, Check, ArrowsClockwise } from "phosphor-svelte"; import { MagnifyingGlass, Books, DownloadSimple, Folder, FolderSimple, FolderSimplePlus, Trash, Star, CheckSquare, X, ArrowSquareOut, SortAscending, Funnel, CaretUp, CaretDown, Check, ArrowsClockwise } from "phosphor-svelte";
import { invoke } from "@tauri-apps/api/core";
import { gql, thumbUrl } from "../../lib/client"; import { gql, thumbUrl } from "../../lib/client";
import { GET_CATEGORIES, GET_LIBRARY, UPDATE_MANGA, UPDATE_MANGAS, GET_CHAPTERS, DELETE_DOWNLOADED_CHAPTERS, DEQUEUE_DOWNLOAD, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES, UPDATE_CATEGORY_ORDER, UPDATE_LIBRARY, LIBRARY_UPDATE_STATUS } from "../../lib/queries"; import { GET_CATEGORIES, GET_LIBRARY, UPDATE_MANGA, UPDATE_MANGAS, GET_CHAPTERS, DELETE_DOWNLOADED_CHAPTERS, DEQUEUE_DOWNLOAD, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES, UPDATE_CATEGORY_ORDER, UPDATE_LIBRARY, LIBRARY_UPDATE_STATUS } from "../../lib/queries";
import { cache, CACHE_KEYS, CACHE_GROUPS, DEFAULT_TTL_MS } from "../../lib/cache"; import { cache, CACHE_KEYS, CACHE_GROUPS, DEFAULT_TTL_MS } from "../../lib/cache";
@@ -596,7 +597,45 @@
ctx = { x: e.clientX, y: e.clientY, manga: m }; ctx = { x: e.clientX, y: e.clientY, manga: m };
} }
function buildCtxItems(m: Manga): MenuEntry[] { function sanitize(s: string) { return s.replace(/[\/\\?%*:|"<>]/g, "_"); }
async function openMangaFolder(m: Manga) {
let base = store.settings.serverDownloadsPath?.trim();
if (!base) {
try { base = await invoke<string>("get_default_downloads_path"); } catch {}
}
if (!base) {
addToast({ kind: "error", title: "No downloads path set", body: "Configure it in Settings → Storage" });
return;
}
const source = m.source?.displayName ?? m.source?.name ?? "";
const path = source
? `${base}/mangas/${sanitize(source)}/${sanitize(m.title)}`
: `${base}/mangas/${sanitize(m.title)}`;
try {
await invoke("open_path", { path });
} catch (e: any) {
addToast({ kind: "error", title: "Could not open folder", body: e?.toString?.() ?? path });
}
}
async function openDownloadsFolder() {
let path = store.settings.serverDownloadsPath?.trim();
if (!path) {
try { path = await invoke<string>("get_default_downloads_path"); } catch {}
}
if (!path) {
addToast({ kind: "error", title: "No downloads path set", body: "Configure it in Settings → Storage" });
return;
}
try {
await invoke("open_path", { path });
} catch (e: any) {
addToast({ kind: "error", title: "Could not open folder", body: e?.toString?.() ?? path });
}
}
function buildCtxItems(m: Manga): MenuEntry[] {
const catEntries: MenuEntry[] = visibleCategories.map(cat => { const catEntries: MenuEntry[] = visibleCategories.map(cat => {
const inCat = (categoryMangaMap.get(cat.id) ?? []).some(x => x.id === m.id); const inCat = (categoryMangaMap.get(cat.id) ?? []).some(x => x.id === m.id);
return { return {
@@ -607,6 +646,7 @@
}); });
return [ return [
{ label: m.inLibrary ? "Remove from library" : "Add to library", icon: Books, onClick: () => m.inLibrary ? removeFromLibrary(m) : gql(UPDATE_MANGA, { id: m.id, inLibrary: true }).then(() => { allManga = allManga.map(x => x.id === m.id ? { ...x, inLibrary: true } : x); cache.clear(CACHE_KEYS.LIBRARY); }).catch(console.error) }, { label: m.inLibrary ? "Remove from library" : "Add to library", icon: Books, onClick: () => m.inLibrary ? removeFromLibrary(m) : gql(UPDATE_MANGA, { id: m.id, inLibrary: true }).then(() => { allManga = allManga.map(x => x.id === m.id ? { ...x, inLibrary: true } : x); cache.clear(CACHE_KEYS.LIBRARY); }).catch(console.error) },
{ label: "Open in file manager", icon: ArrowSquareOut, disabled: !(m.downloadCount && m.downloadCount > 0), onClick: () => openMangaFolder(m) },
{ label: "Delete all downloads", icon: Trash, danger: true, disabled: !(m.downloadCount && m.downloadCount > 0), onClick: () => deleteAllDownloads(m) }, { label: "Delete all downloads", icon: Trash, danger: true, disabled: !(m.downloadCount && m.downloadCount > 0), onClick: () => deleteAllDownloads(m) },
{ separator: true }, { separator: true },
{ label: "Select this manga", icon: CheckSquare, onClick: () => enterSelectMode(m.id) }, { label: "Select this manga", icon: CheckSquare, onClick: () => enterSelectMode(m.id) },
@@ -867,6 +907,13 @@
<span class="refresh-progress">{refreshProgress.finished}/{refreshProgress.total}</span> <span class="refresh-progress">{refreshProgress.finished}/{refreshProgress.total}</span>
{/if} {/if}
</button> </button>
<button
class="icon-btn"
title="Open downloads folder"
onclick={openDownloadsFolder}
>
<FolderSimple size={15} weight="bold" />
</button>
<div class="sort-panel-wrap"> <div class="sort-panel-wrap">
<button <button
class="icon-btn" class="icon-btn"