Files: play video and audio in the viewer, as the file is; reads answer Range requests
The viewer hands a video or audio file to the browser untouched — container, codec and all — and the browser plays what it can; when it can't, the viewer says so and offers the download. What the server adds is the part a player needs: /api/files/read now goes through http.ServeContent for a real file of known size, so Range requests (seeking) and HEAD work, and a download can resume. The root backend's helper takes an offset for that, and a Seeker opens the backend once, at the range asked for, instead of streaming from the top. Audio files are a preview kind too: their cover art is the thumbnail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c24f6f5cde
commit
a9bcb88212
+15
-3
@@ -17,6 +17,7 @@ function icon(e: FileEntry): string {
|
||||
if (e.type === 'other') return '⚙️'
|
||||
if (e.preview === 'image' || isImage(e.name)) return '🖼️'
|
||||
if (e.preview === 'video') return '🎞️'
|
||||
if (e.preview === 'audio') return '🎵'
|
||||
if (/\.(zip|gz|bz2|xz|zst|tar|tgz|7z|rar)$/i.test(e.name)) return '🗜️'
|
||||
if (/\.(sh|bash|zsh|py|go|js|ts|tsx|rb|pl|php|rs|c|h|cpp|java|lua)$/i.test(e.name) || e.mode.includes('x')) return '📜'
|
||||
if (/\.(log|txt|md|json|ya?ml|toml|ini|conf|cfg|env|service|timer)$/i.test(e.name)) return '📄'
|
||||
@@ -132,7 +133,7 @@ class Near {
|
||||
private forget(el: Element) { this.cbs.delete(el); this.obs.unobserve(el) }
|
||||
}
|
||||
|
||||
function Thumb({ url, kind, near, fallback }: { url: string; kind: 'image' | 'video'; near: Near | null; fallback: string }) {
|
||||
function Thumb({ url, kind, near, fallback }: { url: string; kind: 'image' | 'video' | 'audio'; near: Near | null; fallback: string }) {
|
||||
const ref = useRef<HTMLSpanElement>(null)
|
||||
const [show, setShow] = useState(false)
|
||||
const [failed, setFailed] = useState('')
|
||||
@@ -189,11 +190,12 @@ function Viewer({ path, sudo, onClose, onChanged }: { path: string; sudo: boolea
|
||||
const [orig, setOrig] = useState('')
|
||||
const [err, setErr] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [cantPlay, setCantPlay] = useState(false) // the browser tried the file as it is and gave up
|
||||
const name = path.split('/').pop() ?? path
|
||||
const dirty = text !== null && text !== orig
|
||||
|
||||
useEffect(() => {
|
||||
setInfo(null); setText(null); setErr('')
|
||||
setInfo(null); setText(null); setErr(''); setCantPlay(false)
|
||||
let stop = false
|
||||
;(async () => {
|
||||
try {
|
||||
@@ -251,7 +253,17 @@ function Viewer({ path, sudo, onClose, onChanged }: { path: string; sudo: boolea
|
||||
<textarea className="editor file-editor" value={text} spellCheck={false} onChange={(e) => setText(e.target.value)} />
|
||||
)}
|
||||
{info && text === null && isImage(name) && <div className="preview"><img src={api.fileReadURL(path, sudo)} alt={name} /></div>}
|
||||
{info && text === null && !isImage(name) && !err && (
|
||||
{/* Video and audio go to the browser as they are, codec and all, and it plays what it can. */}
|
||||
{info && text === null && !isImage(name) && info.preview === 'video' && !cantPlay && (
|
||||
<div className="preview"><video controls playsInline preload="metadata" src={api.fileReadURL(path, sudo)} onError={() => setCantPlay(true)} /></div>
|
||||
)}
|
||||
{info && text === null && !isImage(name) && info.preview === 'audio' && !cantPlay && (
|
||||
<div className="preview audio"><div className="audio-box"><div className="ficon">🎵</div><div className="mono small">{name}</div><audio controls preload="metadata" src={api.fileReadURL(path, sudo)} onError={() => setCantPlay(true)} /></div></div>
|
||||
)}
|
||||
{info && text === null && !isImage(name) && cantPlay && (
|
||||
<div className="notice">This browser can't play {name} as it is — the container or codec isn't one it decodes. Download it instead.</div>
|
||||
)}
|
||||
{info && text === null && !isImage(name) && !info.preview && !err && (
|
||||
<div className="notice">{info.type === 'dir' ? 'This is a directory.' : info.size > MAX_EDIT ? `Too large to edit here (${fmtBytes(info.size)}). Download it instead.` : 'Binary file. Download it instead.'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user