mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
[V1] Folder/Tag System
This commit is contained in:
@@ -371,4 +371,91 @@
|
||||
transition: background var(--t-base);
|
||||
}
|
||||
.dangerBtn:hover:not(:disabled) { background: var(--color-error-bg); }
|
||||
.dangerBtn:disabled { opacity: 0.3; cursor: default; }
|
||||
.dangerBtn:disabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
/* ── Folder management (Settings FoldersTab) ────────────────────────── */
|
||||
.folderCreateRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-1) var(--sp-3) var(--sp-3);
|
||||
}
|
||||
|
||||
.folderCreateBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-1);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
padding: 5px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-strong);
|
||||
background: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: color var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.folderCreateBtn:hover:not(:disabled) {
|
||||
color: var(--accent-fg);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.folderCreateBtn:disabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
.folderList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.folderRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: 9px var(--sp-3);
|
||||
border-radius: var(--radius-md);
|
||||
transition: background var(--t-fast);
|
||||
}
|
||||
.folderRow:hover { background: var(--bg-raised); }
|
||||
|
||||
.folderRowName {
|
||||
flex: 1;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.folderRowCount {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
margin-right: var(--sp-1);
|
||||
}
|
||||
|
||||
.folderTabToggle {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: none;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: color var(--t-base), border-color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.folderTabToggle:hover {
|
||||
color: var(--text-muted);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
.folderTabToggleOn {
|
||||
background: var(--accent-muted);
|
||||
border-color: var(--accent-dim);
|
||||
color: var(--accent-fg);
|
||||
}
|
||||
.folderTabToggleOn:hover {
|
||||
background: var(--accent-muted);
|
||||
color: var(--accent-fg);
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { X, Book, Image, Sliders, Info, Keyboard, Gear, HardDrives } from "@phosphor-icons/react";
|
||||
import { X, Book, Image, Sliders, Info, Keyboard, Gear, HardDrives, FolderSimple, Plus, Pencil, Trash } from "@phosphor-icons/react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { gql } from "../../lib/client";
|
||||
import { GET_DOWNLOADS_PATH } from "../../lib/queries";
|
||||
import { useStore } from "../../store";
|
||||
import type { Folder } from "../../store";
|
||||
import { KEYBIND_LABELS, DEFAULT_KEYBINDS, eventToKeybind, type Keybinds } from "../../lib/keybinds";
|
||||
import type { Settings, FitMode } from "../../store";
|
||||
import s from "./Settings.module.css";
|
||||
|
||||
type Tab = "general" | "reader" | "library" | "performance" | "keybinds" | "storage" | "about";
|
||||
type Tab = "general" | "reader" | "library" | "performance" | "keybinds" | "storage" | "folders" | "about";
|
||||
|
||||
const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: "general", label: "General", icon: <Gear size={14} weight="light" /> },
|
||||
@@ -17,6 +18,7 @@ const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: "performance", label: "Performance", icon: <Sliders size={14} weight="light" /> },
|
||||
{ id: "keybinds", label: "Keybinds", icon: <Keyboard size={14} weight="light" /> },
|
||||
{ id: "storage", label: "Storage", icon: <HardDrives size={14} weight="light" /> },
|
||||
{ id: "folders", label: "Folders", icon: <FolderSimple size={14} weight="light" /> },
|
||||
{ id: "about", label: "About", icon: <Info size={14} weight="light" /> },
|
||||
];
|
||||
|
||||
@@ -566,6 +568,127 @@ function StorageTab({ settings, update }: { settings: Settings; update: (p: Part
|
||||
);
|
||||
}
|
||||
|
||||
// ── Folders tab ───────────────────────────────────────────────────────────────
|
||||
|
||||
function FoldersTab() {
|
||||
const folders = useStore((s) => s.settings.folders);
|
||||
const addFolder = useStore((s) => s.addFolder);
|
||||
const removeFolder = useStore((s) => s.removeFolder);
|
||||
const renameFolder = useStore((s) => s.renameFolder);
|
||||
const toggleFolderTab = useStore((s) => s.toggleFolderTab);
|
||||
|
||||
const [newName, setNewName] = useState("");
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
|
||||
function handleCreate() {
|
||||
const name = newName.trim();
|
||||
if (!name) return;
|
||||
addFolder(name);
|
||||
setNewName("");
|
||||
}
|
||||
|
||||
function startEdit(folder: Folder) {
|
||||
setEditingId(folder.id);
|
||||
setEditingName(folder.name);
|
||||
}
|
||||
|
||||
function commitEdit() {
|
||||
if (editingId && editingName.trim()) {
|
||||
renameFolder(editingId, editingName.trim());
|
||||
}
|
||||
setEditingId(null);
|
||||
setEditingName("");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.panel}>
|
||||
<div className={s.section}>
|
||||
<p className={s.sectionTitle}>Manage Folders</p>
|
||||
<p className={s.toggleDesc} style={{ padding: "0 var(--sp-3) var(--sp-3)", display: "block" }}>
|
||||
Assign manga to folders from the series detail page. Toggle the tab icon to show a folder as a filter tab in the library.
|
||||
</p>
|
||||
|
||||
{/* Create new folder */}
|
||||
<div className={s.folderCreateRow}>
|
||||
<input
|
||||
className={s.textInput}
|
||||
placeholder="New folder name…"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); }}
|
||||
style={{ flex: 1, width: "auto" }}
|
||||
/>
|
||||
<button
|
||||
className={s.folderCreateBtn}
|
||||
onClick={handleCreate}
|
||||
disabled={!newName.trim()}
|
||||
>
|
||||
<Plus size={13} weight="bold" />
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Folder list */}
|
||||
{folders.length === 0 ? (
|
||||
<p className={s.storageLoading}>No folders yet. Create one above.</p>
|
||||
) : (
|
||||
<div className={s.folderList}>
|
||||
{folders.map((folder) => (
|
||||
<div key={folder.id} className={s.folderRow}>
|
||||
{editingId === folder.id ? (
|
||||
<>
|
||||
<input
|
||||
autoFocus
|
||||
className={s.textInput}
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") commitEdit();
|
||||
if (e.key === "Escape") { setEditingId(null); }
|
||||
}}
|
||||
onBlur={commitEdit}
|
||||
style={{ flex: 1, width: "auto" }}
|
||||
/>
|
||||
<button className={s.kbReset} onClick={commitEdit} title="Save">✓</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FolderSimple size={14} weight="light" style={{ color: "var(--text-faint)", flexShrink: 0 }} />
|
||||
<span className={s.folderRowName}>{folder.name}</span>
|
||||
<span className={s.folderRowCount}>{folder.mangaIds.length} manga</span>
|
||||
{/* Show as tab toggle */}
|
||||
<button
|
||||
className={[s.folderTabToggle, folder.showTab ? s.folderTabToggleOn : ""].join(" ")}
|
||||
onClick={() => toggleFolderTab(folder.id)}
|
||||
title={folder.showTab ? "Shown as library tab — click to hide" : "Click to show as library tab"}
|
||||
>
|
||||
{folder.showTab ? "Tab on" : "Tab off"}
|
||||
</button>
|
||||
<button
|
||||
className={s.kbReset}
|
||||
onClick={() => startEdit(folder)}
|
||||
title="Rename"
|
||||
>
|
||||
<Pencil size={12} weight="light" />
|
||||
</button>
|
||||
<button
|
||||
className={[s.kbReset, s.folderDeleteBtn].join(" ")}
|
||||
onClick={() => removeFolder(folder.id)}
|
||||
title="Delete folder"
|
||||
>
|
||||
<Trash size={12} weight="light" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AboutTab() {
|
||||
return (
|
||||
@@ -594,7 +717,6 @@ export default function SettingsModal() {
|
||||
const backdropRef = useRef<HTMLDivElement>(null);
|
||||
const contentBodyRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Scroll to top on every tab switch
|
||||
useEffect(() => {
|
||||
contentBodyRef.current?.scrollTo({ top: 0 });
|
||||
}, [tab]);
|
||||
@@ -638,6 +760,7 @@ export default function SettingsModal() {
|
||||
{tab === "performance" && <PerformanceTab settings={settings} update={updateSettings} />}
|
||||
{tab === "keybinds" && <KeybindsTab settings={settings} update={updateSettings} reset={resetKeybinds} />}
|
||||
{tab === "storage" && <StorageTab settings={settings} update={updateSettings} />}
|
||||
{tab === "folders" && <FoldersTab />}
|
||||
{tab === "about" && <AboutTab />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user