initial commit with full project
This commit is contained in:
53
src/lib/utils/format.ts
Normal file
53
src/lib/utils/format.ts
Normal 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
51
src/lib/utils/keyboard.ts
Normal 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
117
src/lib/utils/tauri.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user