timeline and readme
This commit is contained in:
188
ui/timeline/src/App.css
Normal file
188
ui/timeline/src/App.css
Normal file
@@ -0,0 +1,188 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #252525;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 0.875rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: #202020;
|
||||
border-right: 1px solid #333;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar h2 {
|
||||
padding: 1rem;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.asset-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.asset-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.asset-list li:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.asset-list li.selected {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.filename {
|
||||
font-size: 0.875rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: #f59e0b;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-badge.ready {
|
||||
background: #10b981;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.empty,
|
||||
.loading,
|
||||
.error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.video-container video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.timeline-container {
|
||||
height: 120px;
|
||||
background: #252525;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.timeline-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 1rem;
|
||||
background: #202020;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.info h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info dl {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.25rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.info dt {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.info dd {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
107
ui/timeline/src/App.tsx
Normal file
107
ui/timeline/src/App.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getAssets, getSystemStatus } from './api'
|
||||
import type { MediaAsset, SystemStatus } from './types'
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
const [assets, setAssets] = useState<MediaAsset[]>([])
|
||||
const [status, setStatus] = useState<SystemStatus | null>(null)
|
||||
const [selectedAsset, setSelectedAsset] = useState<MediaAsset | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
const [assetsData, statusData] = await Promise.all([
|
||||
getAssets(),
|
||||
getSystemStatus(),
|
||||
])
|
||||
setAssets(assetsData)
|
||||
setStatus(statusData)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to load')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Loading...</div>
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="error">Error: {error}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="header">
|
||||
<h1>MPR Timeline</h1>
|
||||
{status && (
|
||||
<span className="status">
|
||||
{status.status} v{status.version}
|
||||
</span>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="layout">
|
||||
<aside className="sidebar">
|
||||
<h2>Assets</h2>
|
||||
<ul className="asset-list">
|
||||
{assets.map((asset) => (
|
||||
<li
|
||||
key={asset.id}
|
||||
className={selectedAsset?.id === asset.id ? 'selected' : ''}
|
||||
onClick={() => setSelectedAsset(asset)}
|
||||
>
|
||||
<span className="filename">{asset.filename}</span>
|
||||
<span className={`status-badge ${asset.status}`}>
|
||||
{asset.status}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main className="main">
|
||||
{selectedAsset ? (
|
||||
<div className="editor">
|
||||
<div className="video-container">
|
||||
<video
|
||||
controls
|
||||
src={`/media/${selectedAsset.file_path}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="timeline-container">
|
||||
{/* Timeline component will go here */}
|
||||
<div className="timeline-placeholder">
|
||||
Timeline: {selectedAsset.duration?.toFixed(1)}s
|
||||
</div>
|
||||
</div>
|
||||
<div className="info">
|
||||
<h3>{selectedAsset.filename}</h3>
|
||||
<dl>
|
||||
<dt>Duration</dt>
|
||||
<dd>{selectedAsset.duration?.toFixed(2)}s</dd>
|
||||
<dt>Resolution</dt>
|
||||
<dd>{selectedAsset.width}x{selectedAsset.height}</dd>
|
||||
<dt>Video</dt>
|
||||
<dd>{selectedAsset.video_codec}</dd>
|
||||
<dt>Audio</dt>
|
||||
<dd>{selectedAsset.audio_codec}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty">Select an asset to begin</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
75
ui/timeline/src/api.ts
Normal file
75
ui/timeline/src/api.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* API client for FastAPI backend
|
||||
*/
|
||||
|
||||
import type {
|
||||
MediaAsset,
|
||||
TranscodePreset,
|
||||
TranscodeJob,
|
||||
CreateJobRequest,
|
||||
SystemStatus,
|
||||
WorkerStatus,
|
||||
} from "./types";
|
||||
|
||||
const API_BASE = "/api";
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Assets
|
||||
export async function getAssets(): Promise<MediaAsset[]> {
|
||||
return request("/assets/");
|
||||
}
|
||||
|
||||
export async function getAsset(id: string): Promise<MediaAsset> {
|
||||
return request(`/assets/${id}`);
|
||||
}
|
||||
|
||||
// Presets
|
||||
export async function getPresets(): Promise<TranscodePreset[]> {
|
||||
return request("/presets/");
|
||||
}
|
||||
|
||||
// Jobs
|
||||
export async function getJobs(): Promise<TranscodeJob[]> {
|
||||
return request("/jobs/");
|
||||
}
|
||||
|
||||
export async function getJob(id: string): Promise<TranscodeJob> {
|
||||
return request(`/jobs/${id}`);
|
||||
}
|
||||
|
||||
export async function createJob(data: CreateJobRequest): Promise<TranscodeJob> {
|
||||
return request("/jobs/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelJob(id: string): Promise<TranscodeJob> {
|
||||
return request(`/jobs/${id}/cancel`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// System
|
||||
export async function getSystemStatus(): Promise<SystemStatus> {
|
||||
return request("/system/status");
|
||||
}
|
||||
|
||||
export async function getWorkerStatus(): Promise<WorkerStatus> {
|
||||
return request("/system/worker");
|
||||
}
|
||||
9
ui/timeline/src/main.tsx
Normal file
9
ui/timeline/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('app')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -72,3 +72,25 @@ export interface TranscodeJob {
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
}
|
||||
|
||||
// API Request/Response Types
|
||||
|
||||
export interface CreateJobRequest {
|
||||
source_asset_id: string;
|
||||
preset_id: string | null;
|
||||
trim_start: number | null;
|
||||
trim_end: number | null;
|
||||
output_filename: string | null;
|
||||
}
|
||||
|
||||
export interface SystemStatus {
|
||||
status: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface WorkerStatus {
|
||||
available: boolean;
|
||||
active_jobs: number;
|
||||
supported_codecs: string[];
|
||||
gpu_available: boolean;
|
||||
}
|
||||
|
||||
1
ui/timeline/src/vite-env.d.ts
vendored
Normal file
1
ui/timeline/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user