feat: asciinema in dashboard rendering

This commit is contained in:
diced
2025-08-20 20:40:24 -07:00
parent b48e9ba1e4
commit 6758fe1037
7 changed files with 113 additions and 7 deletions

View File

@@ -122,6 +122,7 @@
["calx", ["application/vnd.ms-office.calx"]],
["cap", ["application/vnd.tcpdump.pcap"]],
["car", ["application/vnd.curl.car"]],
["cast", ["application/x-asciicast"]],
["cat", ["application/vnd.ms-pki.seccat"]],
["cb7", ["application/x-cbr"]],
["cba", ["application/x-cbr"]],

View File

@@ -56,6 +56,7 @@
"@smithy/node-http-handler": "^4.1.0",
"@tabler/icons-react": "^3.34.1",
"argon2": "^0.43.1",
"asciinema-player": "^3.10.0",
"bytes": "^3.1.2",
"clsx": "^2.1.1",
"colorette": "^2.0.20",

36
pnpm-lock.yaml generated
View File

@@ -83,6 +83,9 @@ importers:
argon2:
specifier: ^0.43.1
version: 0.43.1
asciinema-player:
specifier: ^3.10.0
version: 3.10.0
bytes:
specifier: ^3.1.2
version: 3.1.2
@@ -2164,6 +2167,9 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
asciinema-player@3.10.0:
resolution: {integrity: sha512-shoOK6F606nDKZxDVM7JuGSCAyWLePoGRFNlV+FqiP5Sqvyn0BlE7wlbjZyd2X4P1iRhv/HKfVNtnQIxmgphRA==}
ast-types-flow@0.0.8:
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
@@ -4248,6 +4254,16 @@ packages:
engines: {node: '>=10'}
hasBin: true
seroval-plugins@1.3.2:
resolution: {integrity: sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==}
engines: {node: '>=10'}
peerDependencies:
seroval: ^1.0
seroval@1.3.2:
resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==}
engines: {node: '>=10'}
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -4311,6 +4327,9 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
solid-js@1.9.9:
resolution: {integrity: sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==}
sonic-boom@4.2.0:
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
@@ -7177,6 +7196,11 @@ snapshots:
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
asciinema-player@3.10.0:
dependencies:
'@babel/runtime': 7.28.2
solid-js: 1.9.9
ast-types-flow@0.0.8: {}
async-function@1.0.0: {}
@@ -9699,6 +9723,12 @@ snapshots:
semver@7.7.2: {}
seroval-plugins@1.3.2(seroval@1.3.2):
dependencies:
seroval: 1.3.2
seroval@1.3.2: {}
set-blocking@2.0.0: {}
set-cookie-parser@2.7.1: {}
@@ -9800,6 +9830,12 @@ snapshots:
slash@3.0.0: {}
solid-js@1.9.9:
dependencies:
csstype: 3.1.3
seroval: 1.3.2
seroval-plugins: 1.3.2(seroval@1.3.2)
sonic-boom@4.2.0:
dependencies:
atomic-sleep: 1.0.0

View File

@@ -15,6 +15,7 @@ import { useEffect, useState, useCallback, useMemo } from 'react';
import { renderMode } from '../pages/upload/renderMode';
import Render from '../render/Render';
import fileIcon from './fileIcon';
import Asciinema from '../render/Asciinema';
function PlaceholderContent({ text, Icon }: { text: string; Icon: Icon }) {
return (
@@ -83,7 +84,7 @@ export default function DashboardFileType({
const renderIn = useMemo(() => renderMode(file.name.split('.').pop() || ''), [file.name]);
const [fileContent, setFileContent] = useState('');
const [type, setType] = useState<string>(file.type.split('/')[0]);
const [type, setType] = useState(file.type.split('/')[0]);
const [open, setOpen] = useState(false);
@@ -164,8 +165,10 @@ export default function DashboardFileType({
</Paper>
);
switch (type) {
case 'video':
const isAsciicast = file.type === 'application/x-asciicast' || file.name.endsWith('.cast');
switch (true) {
case type === 'video':
return show ? (
<video
width='100%'
@@ -201,7 +204,7 @@ export default function DashboardFileType({
) : (
<Placeholder text={`Click to play video ${file.name}`} Icon={fileIcon(file.type)} />
);
case 'image':
case type === 'image':
return show ? (
<Center>
<MantineImage
@@ -240,7 +243,7 @@ export default function DashboardFileType({
alt={file.name || 'Image'}
/>
);
case 'audio':
case type === 'audio':
return show ? (
<audio
autoPlay
@@ -252,7 +255,7 @@ export default function DashboardFileType({
) : (
<Placeholder text={`Click to play audio ${file.name}`} Icon={fileIcon(file.type)} />
);
case 'text':
case type === 'text':
return show ? (
fileContent.trim() === '' ? (
<LoadingOverlay
@@ -276,6 +279,15 @@ export default function DashboardFileType({
) : (
<Placeholder text={`Click to view text ${file.name}`} Icon={fileIcon(file.type)} />
);
case isAsciicast === true:
return show && dbFile ? (
<Asciinema src={`/raw/${file.name}`} />
) : (
<Placeholder
text={`Click to download asciinema cast ${file.name}`}
Icon={fileIcon('application/x-asciicast')}
/>
);
default:
if (dbFile && !show)
return <Placeholder text={`Click to view file ${file.name}`} Icon={fileIcon(file.type)} />;

View File

@@ -16,6 +16,7 @@ import {
IconFileTypeHtml,
IconFileTypeJs,
IconFileTypeJsx,
IconFileTypePdf,
IconFileTypePhp,
IconFileTypePpt,
IconFileTypeRs,
@@ -49,7 +50,7 @@ const icons: Record<string, Icon> = {
'application/x-gzip': IconFileZip,
// common text/document files that are not detected by the 'text' type
'application/pdf': IconFileText,
'application/pdf': IconFileTypePdf,
'application/msword': IconFileTypeDocx,
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': IconFileTypeDocx,
'application/vnd.ms-excel': IconFileTypeXls,
@@ -67,6 +68,7 @@ const icons: Record<string, Icon> = {
'text/javascript': IconFileTypeJs,
'application/json': IconBracketsContain,
'text/xml': IconFileTypeXml,
'application/x-asciicast': IconTerminal2,
// zipline text uploads
'text/x-zipline-html': IconFileTypeHtml,

View File

@@ -0,0 +1,7 @@
declare module 'asciinema-player' {
export function create(
src: string,
container: HTMLElement,
options?: { autoplay?: boolean; cols?: number; rows?: number },
): void;
}

View File

@@ -0,0 +1,47 @@
import { Box, LoadingOverlay } from '@mantine/core';
import { useEffect, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
export default function Asciinema({ src }: { src: string }) {
const ref = useRef<HTMLDivElement>(null);
const [loaded, setLoaded] = useState(false);
const location = useLocation();
useEffect(() => {
let cancelled = false;
const loadPlayer = async () => {
const AsciinemaPlayer = await import('asciinema-player');
await import('asciinema-player/dist/bundle/asciinema-player.css');
if (ref.current && !cancelled) {
ref.current.innerHTML = '';
AsciinemaPlayer.create(src, ref.current);
setLoaded(true);
}
};
loadPlayer();
return () => {
cancelled = true;
if (ref.current) {
ref.current.innerHTML = '';
}
};
}, [src]);
return (
<div>
{!loaded && (
<Box pos='relative' h={400}>
<LoadingOverlay visible />
</Box>
)}
<div style={location.pathname.startsWith('/view') ? { width: '70vw' } : undefined} ref={ref} />
</div>
);
}