fix things

This commit is contained in:
Chris Wanstrath 2026-01-30 20:30:07 -08:00
parent 4cbe5c2566
commit d1b7e973d3
9 changed files with 194 additions and 4 deletions

View File

@ -5,6 +5,7 @@
"": {
"name": "code",
"dependencies": {
"@because/forge": "*",
"@because/howl": "*",
"@because/hype": "*",
"@because/toes": "*",

View File

@ -127,6 +127,69 @@ const BreadcrumbCurrent = define('BreadcrumbCurrent', {
fontWeight: 500,
})
const MediaContainer = define('MediaContainer', {
margin: '20px 0',
border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-md'),
overflow: 'hidden',
backgroundColor: theme('colors-bgSubtle'),
})
const MediaHeader = define('MediaHeader', {
padding: '10px 15px',
backgroundColor: theme('colors-bgElement'),
borderBottom: `1px solid ${theme('colors-border')}`,
fontWeight: 'bold',
fontSize: '14px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
})
const MediaContent = define('MediaContent', {
padding: '20px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
})
const ImagePreview = define('ImagePreview', {
base: 'img',
maxWidth: '100%',
maxHeight: '600px',
objectFit: 'contain',
})
const AudioPlayer = define('AudioPlayer', {
base: 'audio',
width: '100%',
maxWidth: '500px',
})
const VideoPlayer = define('VideoPlayer', {
base: 'video',
maxWidth: '100%',
maxHeight: '600px',
})
const DownloadButton = define('DownloadButton', {
base: 'a',
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
padding: '8px 16px',
backgroundColor: theme('colors-primary'),
color: 'white',
textDecoration: 'none',
borderRadius: theme('radius-md'),
fontSize: '14px',
states: {
':hover': {
opacity: 0.9,
},
},
})
const FolderIcon = () => (
<FileIcon viewBox="0 0 24 24">
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
@ -145,6 +208,28 @@ interface LayoutProps {
highlight?: boolean
}
const fileMemoryScript = `
(function() {
var params = new URLSearchParams(window.location.search);
var app = params.get('app');
var file = params.get('file');
var version = params.get('version') || 'current';
if (!app) return;
var key = 'code-app:' + app + ':' + version + ':file';
if (file) {
localStorage.setItem(key, file);
} else {
var saved = localStorage.getItem(key);
if (saved) {
var url = '/?app=' + encodeURIComponent(app);
if (version !== 'current') url += '&version=' + encodeURIComponent(version);
url += '&file=' + encodeURIComponent(saved);
window.location.replace(url);
}
}
})();
`
function Layout({ title, children, highlight }: LayoutProps) {
return (
<html>
@ -162,6 +247,7 @@ function Layout({ title, children, highlight }: LayoutProps) {
)}
</head>
<body>
<script dangerouslySetInnerHTML={{ __html: fileMemoryScript }} />
<script dangerouslySetInnerHTML={{ __html: initScript }} />
<Container>
{children}
@ -176,6 +262,25 @@ app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, {
'Content-Type': 'text/css; charset=utf-8',
}))
app.get('/raw', async c => {
const appName = c.req.query('app')
const version = c.req.query('version') || 'current'
const filePath = c.req.query('file')
if (!appName || !filePath) {
return c.text('Missing app or file parameter', 400)
}
const fullPath = join(APPS_DIR, appName, version, filePath)
const file = Bun.file(fullPath)
if (!await file.exists()) {
return c.text(`File not found: ${fullPath}`, 404)
}
return new Response(file)
})
async function listFiles(appPath: string, subPath: string = '') {
const fullPath = join(appPath, subPath)
const entries = await readdir(fullPath, { withFileTypes: true })
@ -231,6 +336,22 @@ function PathBreadcrumb({ appName, filePath, versionParam }: BreadcrumbProps) {
)
}
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.bmp'])
const AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac'])
const VIDEO_EXTS = new Set(['.mp4', '.webm', '.mov', '.avi'])
const BINARY_EXTS = new Set(['.pdf', '.zip', '.tar', '.gz', '.exe', '.dmg', '.woff', '.woff2', '.ttf', '.otf', '.eot'])
type FileType = 'text' | 'image' | 'audio' | 'video' | 'binary'
function getFileType(filename: string): FileType {
const ext = extname(filename).toLowerCase()
if (IMAGE_EXTS.has(ext)) return 'image'
if (AUDIO_EXTS.has(ext)) return 'audio'
if (VIDEO_EXTS.has(ext)) return 'video'
if (BINARY_EXTS.has(ext)) return 'binary'
return 'text'
}
function getLanguage(filename: string): string {
const ext = extname(filename).toLowerCase()
const langMap: Record<string, string> = {
@ -249,6 +370,7 @@ function getLanguage(filename: string): string {
return langMap[ext] || 'plaintext'
}
app.get('/', async c => {
const appName = c.req.query('app')
const version = c.req.query('version') || 'current'
@ -290,14 +412,75 @@ app.get('/', async c => {
const versionParam = version !== 'current' ? `&version=${version}` : ''
if (fileStats.isFile()) {
const filename = basename(fullPath)
const fileType = getFileType(filename)
const rawUrl = `/raw?app=${appName}${versionParam}&file=${filePath}`
const downloadUrl = `${rawUrl}&download=1`
if (fileType === 'image') {
return c.html(
<Layout title={`${appName}/${filePath}`}>
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
<MediaContainer>
<MediaHeader>
<span>{filename}</span>
<DownloadButton href={downloadUrl}>Download</DownloadButton>
</MediaHeader>
<MediaContent>
<ImagePreview src={rawUrl} alt={filename} />
</MediaContent>
</MediaContainer>
</Layout>
)
}
if (fileType === 'audio') {
return c.html(
<Layout title={`${appName}/${filePath}`}>
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
<MediaContainer>
<MediaHeader>
<span>{filename}</span>
<DownloadButton href={downloadUrl}>Download</DownloadButton>
</MediaHeader>
<MediaContent>
<AudioPlayer controls src={rawUrl} />
</MediaContent>
</MediaContainer>
</Layout>
)
}
if (fileType === 'video') {
return c.html(
<Layout title={`${appName}/${filePath}`}>
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
<MediaContainer>
<MediaHeader>
<span>{filename}</span>
<DownloadButton href={downloadUrl}>Download</DownloadButton>
</MediaHeader>
<MediaContent>
<VideoPlayer controls src={rawUrl} />
</MediaContent>
</MediaContainer>
</Layout>
)
}
if (fileType === 'binary') {
return c.redirect(downloadUrl)
}
// Text file - show with syntax highlighting
const content = readFileSync(fullPath, 'utf-8')
const language = getLanguage(basename(fullPath))
const language = getLanguage(filename)
return c.html(
<Layout title={`${appName}/${filePath}`} highlight>
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
<CodeBlock>
<CodeHeader>{basename(fullPath)}</CodeHeader>
<CodeHeader>{filename}</CodeHeader>
<pre><code class={`language-${language}`}>{content}</code></pre>
</CodeBlock>
</Layout>

View File

@ -5,6 +5,7 @@
"": {
"name": "todo",
"dependencies": {
"@because/forge": "*",
"@because/hype": "*",
"@because/toes": "*",
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -5,6 +5,7 @@
"": {
"name": "versions",
"dependencies": {
"@because/forge": "*",
"@because/hype": "*",
"@because/toes": "*",
},

View File

@ -9,7 +9,7 @@ export let sidebarSection: 'apps' | 'tools' = (localStorage.getItem('sidebarSect
export let apps: App[] = []
// Tab state
export let selectedTab: string = 'overview'
export let selectedTab: string = localStorage.getItem('selectedTab') || 'overview'
// State setters
export function setSelectedApp(name: string | null) {
@ -37,4 +37,5 @@ export function setApps(newApps: App[]) {
export function setSelectedTab(tab: string) {
selectedTab = tab
localStorage.setItem('selectedTab', tab)
}

View File

@ -11,4 +11,7 @@ app.route('/api/sync', syncRouter)
console.log('🐾 Toes!')
initApps()
export default app.defaults
export default {
...app.defaults,
maxRequestBodySize: 1024 * 1024 * 50, // 50MB
}