mirror of
https://github.com/imputnet/cobalt.git
synced 2026-10-24 02:52:18 +00:00
Add media folder opener to client and server
This commit is contained in:
parent
bc65ef8630
commit
66d07e9e23
4 changed files with 56 additions and 2 deletions
|
|
@ -7,6 +7,7 @@ import {
|
|||
Clock3,
|
||||
Download,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
History,
|
||||
Image as ImageIcon,
|
||||
LoaderCircle,
|
||||
|
|
@ -19,7 +20,7 @@ import {
|
|||
X,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { cancelJob, clearHistory, createJobs, listJobs, retryJob } from './api';
|
||||
import { cancelJob, clearHistory, createJobs, listJobs, openMediaFolder, retryJob } from './api';
|
||||
import type { DownloadJob, JobStatus, MediaItem } from './types';
|
||||
|
||||
const STATUS: Record<JobStatus, { label: string; className: string }> = {
|
||||
|
|
@ -146,6 +147,7 @@ export default function App() {
|
|||
const [submitting, setSubmitting] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [notice, setNotice] = useState('');
|
||||
const [openingFolder, setOpeningFolder] = useState(false);
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const urls = useMemo(() => extractUrls(input), [input]);
|
||||
|
||||
|
|
@ -206,6 +208,17 @@ export default function App() {
|
|||
setNotice(`已清除 ${removed} 条历史记录`);
|
||||
}
|
||||
|
||||
async function showMediaFolder() {
|
||||
setOpeningFolder(true);
|
||||
try {
|
||||
await openMediaFolder();
|
||||
} catch (error) {
|
||||
setNotice(error instanceof Error ? error.message : '无法打开媒体文件夹');
|
||||
} finally {
|
||||
setOpeningFolder(false);
|
||||
}
|
||||
}
|
||||
|
||||
const activeJobs = jobs.filter((job) => ['queued', 'resolving', 'downloading'].includes(job.status));
|
||||
const historyJobs = jobs.filter((job) => ['completed', 'failed', 'canceled'].includes(job.status));
|
||||
const visibleJobs = tab === 'queue' ? activeJobs : historyJobs;
|
||||
|
|
@ -215,7 +228,19 @@ export default function App() {
|
|||
<div className="app-shell">
|
||||
<header className="topbar">
|
||||
<a className="brand" href="#top"><span className="brand-mark">𝕏</span><span>X 媒体存档<small>PUBLIC MEDIA ARCHIVE</small></span></a>
|
||||
<div className="service-state"><span /> Cobalt 本地解析</div>
|
||||
<div className="topbar-actions">
|
||||
<button
|
||||
className="folder-button"
|
||||
type="button"
|
||||
title="打开 C:\\Users\\70918\\Desktop\\veo downloader\\data\\media"
|
||||
onClick={() => void showMediaFolder()}
|
||||
disabled={openingFolder}
|
||||
>
|
||||
{openingFolder ? <LoaderCircle className="spin" size={15} /> : <FolderOpen size={15} />}
|
||||
打开媒体文件夹
|
||||
</button>
|
||||
<div className="service-state"><span /> Cobalt 本地解析</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top">
|
||||
|
|
|
|||
|
|
@ -33,3 +33,7 @@ export function retryJob(id: string) {
|
|||
export function clearHistory() {
|
||||
return request<{ removed: number }>('/api/history', { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function openMediaFolder() {
|
||||
return request<{ path: string }>('/api/open-media-folder', { method: 'POST' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ a { color: inherit; text-decoration: none; }
|
|||
.brand-mark { width: 37px; height: 37px; border-radius: 9px; display: grid; place-items: center; background: var(--ink); color: white; font-size: 19px; }
|
||||
.brand > span:last-child { display: grid; line-height: 1.05; }
|
||||
.brand small { font: 500 8px/1.7 Consolas, monospace; letter-spacing: .14em; color: var(--muted); }
|
||||
.topbar-actions { display: flex; align-items: center; gap: 20px; }
|
||||
.folder-button { border: 1px solid var(--line); background: var(--card); min-height: 32px; padding: 0 11px; display: flex; align-items: center; gap: 7px; cursor: pointer; color: var(--ink); font-size: 11px; }
|
||||
.folder-button:hover:not(:disabled) { border-color: var(--ink); color: var(--accent); }
|
||||
.folder-button:disabled { opacity: .6; cursor: wait; }
|
||||
.service-state { font: 500 11px Consolas, monospace; letter-spacing: .04em; color: var(--muted); display: flex; gap: 8px; align-items: center; }
|
||||
.service-state span { width: 7px; height: 7px; background: #3c9c64; border-radius: 50%; box-shadow: 0 0 0 4px rgba(60,156,100,.12); }
|
||||
main { width: min(1120px, calc(100% - 56px)); margin: 0 auto; }
|
||||
|
|
@ -120,6 +124,7 @@ main { width: min(1120px, calc(100% - 56px)); margin: 0 auto; }
|
|||
|
||||
@media (max-width: 760px) {
|
||||
.topbar { padding: 0 20px; }
|
||||
.topbar-actions { gap: 0; }
|
||||
.service-state { display: none; }
|
||||
main { width: calc(100% - 28px); }
|
||||
.hero { padding: 52px 0 36px; display: block; }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dotenv/config';
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import fsp from 'node:fs/promises';
|
||||
|
|
@ -107,6 +108,25 @@ app.post('/api/jobs/:id/retry', (req, res) => {
|
|||
|
||||
app.delete('/api/history', async (_req, res) => res.json({ removed: await store.clearCompleted() }));
|
||||
|
||||
app.post('/api/open-media-folder', async (_req, res) => {
|
||||
const platform = process.platform;
|
||||
const command = platform === 'win32' ? 'explorer.exe' : platform === 'darwin' ? 'open' : 'xdg-open';
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const opener = spawn(command, [store.mediaDir], { detached: true, stdio: 'ignore' });
|
||||
opener.once('error', reject);
|
||||
opener.once('spawn', () => {
|
||||
opener.unref();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return res.json({ path: store.mediaDir });
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: error instanceof Error ? error.message : '无法打开媒体文件夹' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/jobs/:jobId/media/:mediaId', async (req, res) => {
|
||||
const job = store.get(req.params.jobId);
|
||||
const media = job?.media.find((item) => item.id === req.params.mediaId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue