Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/vidl/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@
"footer": {
"copyright": "Copyright © 2025-PRESENT |"
},
"console": {
"callsign": "Signal Capture Deck",
"privacy": "Browser-side · Zero upload",
"inputTitle": "Input Signal",
"inputHint": "Paste a stream URL — capture runs entirely on this device",
"channelSingle": "Single",
"channelBatch": "Batch",
"caps": {
"formats": "M3U8 / HLS / MP4",
"decrypt": "AES-128 decrypt",
"memory": "Near-zero memory"
}
},
"tool": {
"title": "Video Downloader",
"description": "Supports M3U8/HLS, MP4, and other video formats with range download, streaming, AES decryption, MP4 conversion"
Expand Down
13 changes: 13 additions & 0 deletions apps/vidl/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@
"footer": {
"copyright": "Copyright © 2025-PRESENT |"
},
"console": {
"callsign": "信号捕获终端",
"privacy": "纯浏览器处理 · 零上传",
"inputTitle": "输入信号",
"inputHint": "粘贴流媒体链接 — 全程在本机捕获",
"channelSingle": "单条",
"channelBatch": "批量",
"caps": {
"formats": "M3U8 / HLS / MP4",
"decrypt": "AES-128 解密",
"memory": "近零内存占用"
}
},
"tool": {
"title": "视频在线下载工具",
"description": "支持 M3U8/HLS、MP4 等视频格式,范围下载、流式下载、AES 解密、转 MP4"
Expand Down
1 change: 1 addition & 0 deletions apps/vidl/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const nextConfig: NextConfig = {
env: {
BUILD_TIME: new Date().toLocaleString(),
},
allowedDevOrigins: ['vidl.a.wd.ds.cc'],
images: {
unoptimized: true,
formats: ['image/avif', 'image/webp'],
Expand Down
39 changes: 21 additions & 18 deletions apps/vidl/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,37 @@

import { Toaster } from '@cdlab996/ui/components/sonner'
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import {
Bricolage_Grotesque,
IBM_Plex_Mono,
IBM_Plex_Sans,
} from 'next/font/google'
import { notFound } from 'next/navigation'
import type { Locale } from 'next-intl'
import { hasLocale, NextIntlClientProvider } from 'next-intl'
import { getMessages, setRequestLocale } from 'next-intl/server'

import '@cdlab996/ui/globals.css'
import { IKHeader } from '@cdlab996/ui/IK'
import {
ClientProviders,
LanguageSelector,
ThemeToggle,
} from '@/components/layout'
import '../theme.css'
import { ClientProviders, SiteHeader } from '@/components/layout'
import { routing } from '@/i18n/routing'

const geistSans = Geist({
variable: '--font-geist-sans',
const plexSans = IBM_Plex_Sans({
variable: '--font-plex-sans',
weight: ['400', '500', '600', '700'],
subsets: ['latin'],
})

const geistMono = Geist_Mono({
// keep the `--font-geist-mono` name so existing `font-mono` utilities resolve
const plexMono = IBM_Plex_Mono({
variable: '--font-geist-mono',
weight: ['400', '500', '600'],
subsets: ['latin'],
})

const bricolage = Bricolage_Grotesque({
variable: '--font-bricolage',
weight: ['600', '700', '800'],
subsets: ['latin'],
})

Expand Down Expand Up @@ -324,17 +333,11 @@ export default async function LocaleLayout({
/>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${plexSans.variable} ${plexMono.variable} ${bricolage.variable} antialiased`}
>
<NextIntlClientProvider messages={messages}>
<ClientProviders>
<IKHeader
brand="Video Downloader"
githubHref="https://github.com/WuChenDi/projects/tree/main/apps/vidl"
>
<LanguageSelector />
<ThemeToggle />
</IKHeader>
<SiteHeader />
{children}
<Toaster richColors position="top-center" duration={3000} />
</ClientProviders>
Expand Down
93 changes: 68 additions & 25 deletions apps/vidl/src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
TabsTrigger,
} from '@cdlab996/ui/components/tabs'
import { IKPageContainer } from '@cdlab996/ui/IK'
import { ListPlus, Video } from 'lucide-react'
import { ListPlus, Lock, Radio, Video, Zap } from 'lucide-react'
import Script from 'next/script'
import { useTranslations } from 'next-intl'
import { useEffect, useState } from 'react'
Expand Down Expand Up @@ -45,49 +45,77 @@ export default function VideoDownloaderPage() {

const busy = isDownloading || batch.isBatchRunning

const cardAction = (
<div className="flex items-center gap-2">
<TabsList>
<TabsTrigger
value="single"
disabled={batch.isBatchRunning || isDownloading}
>
<Video className="size-4" />
</TabsTrigger>
<TabsTrigger
value="batch"
disabled={isDownloading && !batch.isBatchRunning}
>
<ListPlus className="size-4" />
</TabsTrigger>
</TabsList>
<SettingsDialog disabled={busy} />
</div>
)

return (
<IKPageContainer scrollable={false}>
<Script
src="/static/StreamSaver.js"
strategy="afterInteractive"
onLoad={actions.onStreamSaverReady}
/>
<div className="w-full h-full flex flex-col gap-4">

<div className="mx-auto flex h-full w-full max-w-5xl flex-col gap-5">
{/* ── Hero band ───────────────────────────────────────────── */}
<section className="deck-rise flex flex-col gap-4 pt-2">
<h1 className="font-display text-4xl font-extrabold leading-[0.95] tracking-tight text-foreground sm:text-5xl">
{t('tool.title')}
</h1>

<p className="max-w-2xl text-sm leading-relaxed text-muted-foreground">
{t('tool.description')}
</p>

{/* Capability call-signs */}
<div className="flex flex-wrap gap-2">
<Capability icon={Radio} label={t('console.caps.formats')} />
<Capability icon={Lock} label={t('console.caps.decrypt')} />
<Capability icon={Zap} label={t('console.caps.memory')} />
</div>
</section>

{/* ── Console ─────────────────────────────────────────────── */}
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex flex-col gap-3 flex-1 min-h-0"
className="deck-rise flex min-h-0 flex-1 flex-col gap-4"
style={{ animationDelay: '0.08s' }}
>
{/* Channel toolbar */}
<div className="flex items-center justify-between gap-3 border-y border-border/70 py-2">
<div className="flex items-center gap-3">
<span className="deck-label hidden sm:inline">CH</span>
<TabsList>
<TabsTrigger
value="single"
disabled={batch.isBatchRunning || isDownloading}
>
<Video className="size-4" />
<span className="hidden sm:inline">
{t('console.channelSingle')}
</span>
</TabsTrigger>
<TabsTrigger
value="batch"
disabled={isDownloading && !batch.isBatchRunning}
>
<ListPlus className="size-4" />
<span className="hidden sm:inline">
{t('console.channelBatch')}
</span>
</TabsTrigger>
</TabsList>
</div>
<SettingsDialog disabled={busy} />
</div>

{/* Single */}
<TabsContent value="single" className="space-y-4">
<SourceCard headerAction={cardAction} actions={actions} />
<SourceCard actions={actions} />
<ProgressCard actions={actions} />
</TabsContent>

{/* Batch */}
<TabsContent value="batch" className="space-y-4">
<BatchInputCard
headerAction={cardAction}
batchText={batch.batchText}
onBatchTextChange={batch.setBatchText}
disabled={batch.isBatchRunning || batch.isBatchParsing}
Expand All @@ -102,3 +130,18 @@ export default function VideoDownloaderPage() {
</IKPageContainer>
)
}

function Capability({
icon: Icon,
label,
}: {
icon: React.ComponentType<{ className?: string }>
label: string
}) {
return (
<span className="inline-flex items-center gap-1.5 rounded-sm border border-border bg-card/50 px-2.5 py-1.5 font-mono text-xs text-muted-foreground">
<Icon className="size-3.5 text-primary" />
{label}
</span>
)
}
Loading