initial commit with full project

This commit is contained in:
2026-04-26 17:50:04 +03:00
commit 53044e7d40
68 changed files with 34115 additions and 0 deletions

53
src/lib/utils/format.ts Normal file
View File

@@ -0,0 +1,53 @@
export function formatFileSize(bytes: number): string {
if (bytes >= 1_073_741_824) {
const gb = bytes / 1_073_741_824;
return gb >= 10 ? `${Math.round(gb)} GB` : `${gb.toFixed(1)} GB`;
}
const mb = bytes / 1_048_576;
if (mb >= 10) return `${Math.round(mb)} MB`;
if (mb >= 0.1) return `${mb.toFixed(1)} MB`;
const kb = bytes / 1024;
return `${Math.round(kb)} KB`;
}
export function formatDuration(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
return `${m}:${String(s).padStart(2, '0')}`;
}
export function formatTimecode(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
const ms = Math.round((seconds % 1) * 100);
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(ms).padStart(2, '0')}`;
}
export function formatTimecodeShort(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
return `${m}:${String(s).padStart(2, '0')}`;
}
export function formatBitrate(bps: number): string {
const kbps = Math.round(bps / 1000);
return `${kbps} kbps`;
}
export function formatPercent(value: number): string {
if (value >= 100) return '100%';
if (value >= 10) return `${value.toFixed(1)}%`;
return `${value.toFixed(1)}%`;
}
export function formatEta(seconds: number): string {
if (seconds <= 0) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${String(s).padStart(2, '0')}`;
}

51
src/lib/utils/keyboard.ts Normal file
View File

@@ -0,0 +1,51 @@
type ShortcutHandler = {
key: string;
ctrl?: boolean;
shift?: boolean;
handler: () => void;
when?: () => boolean;
};
let registered = false;
let shortcuts: ShortcutHandler[] = [];
let cleanup: (() => void) | null = null;
export function setShortcuts(handlers: ShortcutHandler[]) {
shortcuts = handlers;
}
export function registerShortcuts() {
if (registered) return;
registered = true;
const onKeyDown = (e: KeyboardEvent) => {
// skip when typing in inputs
const tag = (e.target as HTMLElement)?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
for (const s of shortcuts) {
const ctrlMatch = s.ctrl ? (e.ctrlKey || e.metaKey) : !(e.ctrlKey || e.metaKey);
const shiftMatch = s.shift ? e.shiftKey : !e.shiftKey;
const keyMatch = e.key === s.key || e.code === s.key;
if (keyMatch && ctrlMatch && shiftMatch) {
if (s.when && !s.when()) continue;
e.preventDefault();
s.handler();
return;
}
}
};
window.addEventListener('keydown', onKeyDown);
cleanup = () => {
window.removeEventListener('keydown', onKeyDown);
registered = false;
};
}
export function unregisterShortcuts() {
cleanup?.();
cleanup = null;
shortcuts = [];
}

117
src/lib/utils/tauri.ts Normal file
View File

@@ -0,0 +1,117 @@
import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import type {
VideoInfo,
CompressSettings,
TrimRange,
OutputInfo,
HardwareInfo,
FFmpegStatus,
ProgressEvent,
AppConfig
} from '$lib/types';
export function analyzeVideo(path: string): Promise<VideoInfo> {
return invoke('analyze_video', { path });
}
export function extractKeyframes(path: string): Promise<number[]> {
return invoke('extract_keyframes', { path });
}
export function generateThumbnails(path: string, count: number, duration?: number): Promise<string[]> {
return invoke('generate_thumbnails', { path, count, duration: duration ?? null });
}
export function generatePreview(path: string, codec?: string): Promise<string> {
return invoke('generate_preview', { path, codec: codec ?? null });
}
export function detectHardware(): Promise<HardwareInfo> {
return invoke('detect_hardware');
}
export function compress(
input: string,
output: string,
settings: CompressSettings,
trim?: TrimRange
): Promise<OutputInfo> {
return invoke('compress', { input, output, settings, trim: trim ?? null });
}
export function trim(
input: string,
output: string,
range: TrimRange,
smartCut: boolean,
stripAudio: boolean = false
): Promise<OutputInfo> {
return invoke('trim', { input, output, range, smartCut, stripAudio: stripAudio ? true : null });
}
export function cancelJob(jobId: string): Promise<void> {
return invoke('cancel_job', { jobId });
}
export function checkFfmpeg(): Promise<FFmpegStatus> {
return invoke('check_ffmpeg');
}
export function openInExplorer(path: string): Promise<void> {
return invoke('open_in_explorer', { path });
}
export function getConfig(): Promise<AppConfig> {
return invoke('get_config');
}
export function saveConfig(cfg: AppConfig): Promise<void> {
return invoke('save_config_cmd', { newConfig: cfg });
}
export function getOutputPath(input: string, mode: string, container: string = 'mp4'): Promise<string> {
return invoke('get_output_path', { input, mode, container });
}
export function initApp(): Promise<FFmpegStatus> {
return invoke('init_app');
}
export function downloadFfmpeg(): Promise<FFmpegStatus> {
return invoke('download_ffmpeg');
}
export interface InterruptedJob {
input_path: string;
output_path: string;
mode: string;
settings_json: string;
}
export function checkRecovery(): Promise<InterruptedJob | null> {
return invoke('check_recovery');
}
export function cleanupRecovery(): Promise<void> {
return invoke('cleanup_recovery');
}
export function listenProgress(callback: (event: ProgressEvent) => void): Promise<UnlistenFn> {
return listen<ProgressEvent>('progress', (ev) => callback(ev.payload));
}
export function getStreamPort(): Promise<number> {
return invoke('get_stream_port_cmd');
}
let streamBase = '';
export function setStreamBase(base: string) {
streamBase = base;
}
export function streamUrl(path: string): string {
if (!streamBase) return '';
return streamBase + encodeURIComponent(path);
}