mirror of
https://github.com/D-Ogi/WatermarkRemover-AI.git
synced 2026-10-22 06:32:38 -05:00
1010 lines
56 KiB
HTML
1010 lines
56 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WM REMOVER - OHIO EDITION</title>
|
|
|
|
<!-- Tailwind + Alpine + Fonts -->
|
|
<link rel="stylesheet" href="vendor/tailwind.css">
|
|
<script defer src="vendor/alpine-3.13.3.min.js"></script>
|
|
<!-- Local base and theme fonts -->
|
|
<link rel="stylesheet" href="vendor/fonts.css">
|
|
|
|
<style>
|
|
[x-cloak] { display: none !important; }
|
|
body { background-color: #050505; color: white; }
|
|
|
|
/* CRT Scanline Effect */
|
|
.crt::before {
|
|
content: " ";
|
|
display: block;
|
|
position: absolute;
|
|
top: 0; left: 0; bottom: 0; right: 0;
|
|
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
|
|
z-index: 2;
|
|
background-size: 100% 2px, 3px 100%;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Custom Scrollbar */
|
|
::-webkit-scrollbar { width: 8px; }
|
|
::-webkit-scrollbar-track { background: #111; }
|
|
::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
|
|
::-webkit-scrollbar-thumb:hover { background: #00ff88; }
|
|
|
|
.glass-panel {
|
|
background: rgba(17, 17, 22, 0.7);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
@keyframes loadingBar {
|
|
0% { width: 0%; margin-left: 0%; }
|
|
50% { width: 60%; margin-left: 20%; }
|
|
100% { width: 0%; margin-left: 100%; }
|
|
}
|
|
|
|
.sidebar-header { flex-wrap: wrap; }
|
|
|
|
/* Custom Tooltips - Fast & Theme-aware */
|
|
[data-tooltip] {
|
|
position: relative;
|
|
}
|
|
[data-tooltip]::after {
|
|
content: attr(data-tooltip);
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 0;
|
|
transform: translateY(-4px);
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
font-weight: normal;
|
|
line-height: 1.4;
|
|
width: max-content;
|
|
max-width: 220px;
|
|
text-align: left;
|
|
z-index: 1000;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: opacity 0.15s ease, visibility 0.15s ease;
|
|
pointer-events: none;
|
|
/* Default theme (brainrot) */
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
color: #00ff88;
|
|
border: 1px solid #00ff88;
|
|
box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
|
|
}
|
|
/* Right-column hints must grow toward the inside of the sidebar. */
|
|
[data-tooltip-align="end"]::after { left: auto; right: 0; }
|
|
[data-tooltip]:hover::after {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
/* Hide empty tooltips */
|
|
[data-tooltip=""]::after {
|
|
display: none;
|
|
}
|
|
</style>
|
|
<!-- Theme styles -->
|
|
<link rel="stylesheet" href="themes.css">
|
|
</head>
|
|
<body class="h-screen w-screen flex crt selection:bg-neon-pink selection:text-white overflow-hidden"
|
|
x-data="brainrotApp()" x-init="init()">
|
|
|
|
<!-- LOADING OVERLAY -->
|
|
<div x-show="isLoading" x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
class="fixed inset-0 z-50 bg-void flex items-center justify-center">
|
|
<div class="text-center">
|
|
<div class="text-5xl font-glitch text-transparent bg-clip-text bg-gradient-to-r from-neon-green to-neon-cyan mb-6 animate-pulse">
|
|
LOADING
|
|
</div>
|
|
<div class="w-48 h-1.5 bg-gray-800 rounded-full overflow-hidden mx-auto">
|
|
<div class="h-full bg-gradient-to-r from-neon-green via-neon-cyan to-neon-pink rounded-full"
|
|
style="animation: loadingBar 1.5s ease-in-out infinite"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- BACKGROUND CHAOS -->
|
|
<div class="fixed inset-0 z-0 opacity-30 pointer-events-none">
|
|
<div class="absolute top-10 left-10 text-9xl animate-pulse text-neon-green">💀</div>
|
|
<div class="absolute bottom-20 right-10 text-8xl animate-bounce text-neon-pink">🔥</div>
|
|
<div class="absolute top-1/2 left-1/4 w-96 h-96 bg-neon-cyan blur-[150px] rounded-full mix-blend-screen opacity-30"></div>
|
|
</div>
|
|
|
|
<!-- MAIN UI CONTAINER - fills entire window -->
|
|
<main class="relative z-10 w-full h-full flex flex-col md:flex-row overflow-hidden">
|
|
|
|
<!-- LEFT SIDEBAR (Controls) -->
|
|
<aside class="w-96 flex-shrink-0 bg-black/60 backdrop-blur-md border-r border-white/10 p-4 flex flex-col gap-4 overflow-y-auto">
|
|
|
|
<!-- HEADER + THEME/LANG SELECTORS -->
|
|
<div class="sidebar-header flex items-start justify-between gap-2">
|
|
<!-- Logo -->
|
|
<div class="space-y-1">
|
|
<h1 class="text-4xl font-glitch text-transparent bg-clip-text bg-gradient-to-r from-neon-green to-neon-cyan leading-none filter drop-shadow-[2px_2px_0px_rgba(255,0,255,0.5)]">
|
|
WM<br>REMOVER
|
|
</h1>
|
|
<p class="text-[10px] font-mono text-gray-500 tracking-widest uppercase" x-text="'v' + appVersion"></p>
|
|
</div>
|
|
<!-- Theme & Language Selectors -->
|
|
<div class="flex flex-col gap-1">
|
|
<select x-show="availableThemes.length > 0" x-model="theme" @change="switchTheme(theme); saveConfigDebounced()" class="bg-black/50 border border-white/20 text-white text-[10px] rounded-lg px-2 py-1 focus:border-neon-pink outline-none font-mono">
|
|
<template x-for="th in availableThemes" :key="th.id">
|
|
<option :value="th.id" :selected="th.id === theme" x-text="th.name || th.id"></option>
|
|
</template>
|
|
</select>
|
|
<!-- Language selector -->
|
|
<select x-show="availableLanguages.length > 0" x-model="lang" @change="switchLang(lang); saveConfigDebounced()" class="bg-black/50 border border-white/20 text-white text-[10px] rounded-lg px-2 py-1 focus:border-neon-cyan outline-none font-mono">
|
|
<template x-for="ln in availableLanguages" :key="ln.id">
|
|
<option :value="ln.id" :selected="ln.id === lang" x-text="(ln.icon ? ln.icon + ' ' : '') + (ln.name || ln.id)"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODE SELECTOR -->
|
|
<div class="space-y-2">
|
|
<label class="text-xs font-bold text-neon-green uppercase tracking-wider" x-text="t.modes?.label || 'Mode'"></label>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<button @click="mode='single'; saveConfigDebounced()" :data-tooltip="t.modes?.single_tooltip || ''" :class="mode=='single' ? 'bg-neon-green text-black shadow-[0_0_15px_#00ff88]' : 'bg-white/5 hover:bg-white/10 text-gray-400'" class="p-3 rounded-xl font-black text-sm transition-all border border-white/10" x-text="t.modes?.single || 'Single'">
|
|
</button>
|
|
<button data-tooltip-align="end" @click="mode='batch'; saveConfigDebounced()" :data-tooltip="t.modes?.batch_tooltip || ''" :class="mode=='batch' ? 'bg-neon-cyan text-black shadow-[0_0_15px_#00f3ff]' : 'bg-white/5 hover:bg-white/10 text-gray-400'" class="p-3 rounded-xl font-black text-sm transition-all border border-white/10" x-text="t.modes?.batch || 'Batch'">
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SETTINGS -->
|
|
<div class="space-y-4 flex-1">
|
|
|
|
<!-- Toggle -->
|
|
<div class="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5">
|
|
<span class="text-xs font-bold text-gray-300 cursor-help" :data-tooltip="t.settings?.gaslight?.tooltip || ''"><span x-text="t.settings?.gaslight?.label || 'Overwrite'"></span> <br><span class="text-[10px] text-gray-500 font-mono" x-text="t.settings?.gaslight?.sublabel || ''"></span></span>
|
|
<button @click="overwrite = !overwrite; saveConfigDebounced()" class="w-12 h-6 rounded-full relative transition-colors duration-300" :class="overwrite ? 'bg-neon-pink' : 'bg-gray-700'">
|
|
<div class="w-4 h-4 bg-white rounded-full absolute top-1 transition-all duration-300" :class="overwrite ? 'left-7' : 'left-1'"></div>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Toggle -->
|
|
<div class="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5">
|
|
<span class="text-xs font-bold text-gray-300 cursor-help" :data-tooltip="t.settings?.ghost?.tooltip || ''"><span x-text="t.settings?.ghost?.label || 'Transparent'"></span> <br><span class="text-[10px] text-gray-500 font-mono" x-text="t.settings?.ghost?.sublabel || ''"></span></span>
|
|
<button @click="transparent = !transparent; saveConfigDebounced()" class="w-12 h-6 rounded-full relative transition-colors duration-300" :class="transparent ? 'bg-neon-green' : 'bg-gray-700'">
|
|
<div class="w-4 h-4 bg-white rounded-full absolute top-1 transition-all duration-300" :class="transparent ? 'left-7' : 'left-1'"></div>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Slider -->
|
|
<div class="space-y-2 pt-2">
|
|
<div class="flex justify-between text-xs font-mono">
|
|
<span class="text-neon-cyan cursor-help" :data-tooltip="t.settings?.sigma_detect?.tooltip || ''" x-text="t.settings?.sigma_detect?.label || 'Max Size'"></span>
|
|
<span x-text="detection + '%'">10%</span>
|
|
</div>
|
|
<input type="range" min="1" max="100" x-model="detection" @change="saveConfigDebounced()" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-neon-cyan">
|
|
</div>
|
|
|
|
<!-- Detection Prompt -->
|
|
<div class="space-y-1">
|
|
<label class="text-[10px] text-gray-500 uppercase cursor-help" :data-tooltip="t.settings?.detection_prompt?.tooltip || ''" x-text="t.settings?.detection_prompt?.label || 'Detection Prompt'"></label>
|
|
<input type="text" x-model="detectionPrompt" list="prompt-suggestions" @change="saveConfigDebounced()"
|
|
:placeholder="t.settings?.detection_prompt?.placeholder || 'watermark'"
|
|
class="w-full bg-black border border-white/20 text-white text-sm rounded-lg p-2.5 focus:border-neon-cyan outline-none font-mono">
|
|
<datalist id="prompt-suggestions">
|
|
<option value="watermark">
|
|
<option value="watermark logo">
|
|
<option value="watermark Sora logo">
|
|
<option value="watermark text">
|
|
<option value="Getty Images">
|
|
<option value="Shutterstock">
|
|
<option value="Adobe Stock">
|
|
<option value="iStock">
|
|
</datalist>
|
|
</div>
|
|
|
|
<!-- PREVIEW BUTTON -->
|
|
<button id="preview-processing" @click="runPreview()" :disabled="!inputPath || previewLoading"
|
|
:data-tooltip="t.buttons?.preview_tooltip || ''"
|
|
class="w-full py-3 bg-neon-cyan/20 hover:bg-neon-cyan/40 text-neon-cyan border border-neon-cyan/50 rounded-xl font-bold transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
:class="previewLoading ? 'animate-pulse' : ''">
|
|
<span x-text="previewLoading ? (t.buttons?.preview_loading || '🔄 Detecting...') : (t.buttons?.preview || '🔍 Preview')"></span>
|
|
</button>
|
|
|
|
<!-- Video Settings Section (only for video files) -->
|
|
<div x-show="isVideoFile(inputPath)" x-transition
|
|
class="space-y-3 p-3 bg-purple-900/20 rounded-xl border border-purple-500/30">
|
|
<div class="text-xs font-bold text-purple-400 uppercase flex items-center gap-2">
|
|
<span>🎬</span> <span x-text="t.video_settings?.title || 'Video Settings'"></span>
|
|
</div>
|
|
|
|
<!-- Detection Skip Slider -->
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between text-xs font-mono">
|
|
<span class="text-purple-300" x-text="t.video_settings?.detection_skip?.label || 'Detection Skip'"></span>
|
|
<span x-text="detectionSkip + ' ' + (t.video_settings?.detection_skip?.unit || 'frames')"></span>
|
|
</div>
|
|
<input type="range" min="1" max="10" x-model="detectionSkip" @change="saveConfigDebounced()"
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500">
|
|
<div class="text-[9px] text-gray-500" x-text="t.video_settings?.detection_skip?.hint || ''"></div>
|
|
</div>
|
|
|
|
<!-- Fade In Slider -->
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between text-xs font-mono">
|
|
<span class="text-purple-300" x-text="t.video_settings?.fade_in?.label || 'Fade In'"></span>
|
|
<span x-text="fadeIn + 's'"></span>
|
|
</div>
|
|
<input type="range" min="0" max="2" step="0.1" x-model="fadeIn" @change="saveConfigDebounced()"
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500">
|
|
<div class="text-[9px] text-gray-500" x-text="t.video_settings?.fade_in?.hint || ''"></div>
|
|
</div>
|
|
|
|
<!-- Fade Out Slider -->
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between text-xs font-mono">
|
|
<span class="text-purple-300" x-text="t.video_settings?.fade_out?.label || 'Fade Out'"></span>
|
|
<span x-text="fadeOut + 's'"></span>
|
|
</div>
|
|
<input type="range" min="0" max="2" step="0.1" x-model="fadeOut" @change="saveConfigDebounced()"
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500">
|
|
<div class="text-[9px] text-gray-500" x-text="t.video_settings?.fade_out?.hint || ''"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Format Dropdown -->
|
|
<div class="space-y-1">
|
|
<label class="text-[10px] text-gray-500 uppercase cursor-help" :data-tooltip="t.settings?.output_format?.tooltip || ''" x-text="t.settings?.output_format?.label || 'Output Format'"></label>
|
|
<select x-model="format" @change="saveConfigDebounced()" class="w-full bg-black border border-white/20 text-white text-sm rounded-lg p-2.5 focus:border-neon-green outline-none font-mono">
|
|
<option value="None" x-text="t.settings?.output_format?.auto || 'Auto'"></option>
|
|
<option value="PNG" x-text="t.settings?.output_format?.png || 'PNG'"></option>
|
|
<option value="JPG" x-text="t.settings?.output_format?.jpg || 'JPG'"></option>
|
|
<option value="WEBP" x-text="t.settings?.output_format?.webp || 'WEBP'"></option>
|
|
<option value="MP4" x-text="t.settings?.output_format?.mp4 || 'MP4'"></option>
|
|
<option value="AVI" x-text="t.settings?.output_format?.avi || 'AVI'"></option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- START BUTTON -->
|
|
<button id="start-processing" @click="startProcessing()" :disabled="processing || !inputPath"
|
|
class="relative group w-full py-5 bg-neon-green rounded-xl font-black text-black text-xl overflow-hidden hover:scale-[1.02] active:scale-95 transition-transform disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<span class="relative z-10 flex items-center justify-center gap-2" x-text="processing ? (t.buttons?.start_processing || 'Processing...') : (t.buttons?.start || 'Start')">
|
|
</span>
|
|
<!-- Button Glow -->
|
|
<div class="absolute inset-0 bg-white/30 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
|
|
</button>
|
|
|
|
</aside>
|
|
|
|
<!-- RIGHT CONTENT (Preview & Logs) -->
|
|
<div class="flex-1 min-w-0 flex flex-col relative bg-black/40 backdrop-blur-sm overflow-hidden">
|
|
|
|
<!-- MARQUEE BAR -->
|
|
<div class="bg-neon-pink text-black font-bold text-xs py-1 overflow-hidden whitespace-nowrap border-b border-neon-pink">
|
|
<div class="animate-marquee inline-block" x-text="(t.marquee || 'WATERMARK REMOVER AI') + ' ' + (t.marquee || '')">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DRAG & DROP ZONE -->
|
|
<div class="flex-1 p-6 flex flex-col gap-4">
|
|
|
|
<!-- Input Box -->
|
|
<div @click="browseInput()"
|
|
class="border-2 border-dashed border-white/20 rounded-2xl flex-1 flex flex-col items-center justify-center p-8 transition-all hover:border-neon-green hover:bg-neon-green/5 group cursor-pointer relative overflow-hidden">
|
|
<template x-if="!inputPath">
|
|
<div class="text-center">
|
|
<div class="text-5xl mb-4 group-hover:animate-bounce">📥</div>
|
|
<h3 class="text-xl font-bold text-white group-hover:text-neon-green" x-text="t.dropzone?.title || 'Drop Files Here'"></h3>
|
|
<p class="text-sm text-gray-500 mt-2 font-mono" x-text="t.dropzone?.subtitle || 'or click to browse'"></p>
|
|
</div>
|
|
</template>
|
|
<template x-if="inputPath">
|
|
<div class="text-center">
|
|
<div class="text-5xl mb-4" x-text="isVideoFile(inputPath) ? '🎬' : '✅'"></div>
|
|
<h3 class="text-lg font-bold text-neon-green break-all px-4" x-text="inputPath"></h3>
|
|
<p class="text-sm text-gray-500 mt-2 font-mono" x-text="isVideoFile(inputPath) ? (t.dropzone?.video_hint || 'video - click to change') : (t.dropzone?.image_hint || 'click to change')"></p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Background Grid Animation -->
|
|
<div class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwgMjU1LSAxMzYsIDAuMSkiLz48L3N2Zz4=')] opacity-20"></div>
|
|
</div>
|
|
|
|
<!-- Output Path Input -->
|
|
<div class="flex gap-2">
|
|
<div class="flex-1 min-w-0 bg-card border border-white/10 rounded-xl p-3 flex items-center gap-2">
|
|
<span class="text-gray-500 flex-shrink-0">📂</span>
|
|
<input type="text" x-model="outputPath" @change="saveConfigDebounced()" :placeholder="t.output?.placeholder || 'Output folder...'" class="bg-transparent text-sm text-gray-300 w-full outline-none font-mono truncate">
|
|
</div>
|
|
<button @click="browseOutput()" class="flex-shrink-0 bg-white/10 hover:bg-white/20 text-white px-3 py-3 rounded-xl border border-white/10 transition-colors text-sm" x-text="t.buttons?.browse || 'Browse'"></button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- LOGS / TERMINAL AREA -->
|
|
<div class="h-52 bg-black/70 backdrop-blur-md border-t border-white/10 p-4 font-mono text-xs overflow-y-auto relative select-text cursor-text" id="logs-container">
|
|
<div class="absolute top-2 right-2 flex gap-2">
|
|
<span class="w-3 h-3 rounded-full bg-red-500"></span>
|
|
<span class="w-3 h-3 rounded-full bg-yellow-500"></span>
|
|
<span class="w-3 h-3 rounded-full bg-green-500"></span>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<template x-for="log in logs" :key="log.id">
|
|
<div :class="log.color">
|
|
<span class="opacity-50" x-text="'[' + log.time + ']'"></span>
|
|
<span x-text="log.msg"></span>
|
|
</div>
|
|
</template>
|
|
<div x-show="processing" class="text-neon-green animate-pulse" x-text="t.logs?.cooking || '_ processing...'"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- STATUS BAR -->
|
|
<div class="status-bar bg-black/60 backdrop-blur-md border-t border-white/10 p-2 px-4 text-[10px] font-mono text-gray-500 uppercase">
|
|
<div class="status-details">
|
|
<span class="flex items-center gap-1">
|
|
<span class="w-2 h-2 rounded-full" :class="systemInfo.cuda ? 'bg-neon-green' : 'bg-red-500'"></span>
|
|
<span x-text="t.status_bar?.cuda || 'CUDA'"></span>: <span x-text="systemInfo.cuda ? (t.status_bar?.on || 'ON') : (t.status_bar?.off || 'OFF')"></span>
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<span class="w-2 h-2 rounded-full" :class="systemInfo.ffmpeg ? 'bg-neon-green' : 'bg-yellow-500'"></span>
|
|
<span x-text="t.status_bar?.ffmpeg || 'FFmpeg'"></span>: <span x-text="systemInfo.ffmpeg ? (t.status_bar?.on || 'ON') : (t.status_bar?.off || 'OFF')"></span>
|
|
</span>
|
|
<span><span x-text="t.status_bar?.ram || 'RAM'"></span>: <span x-text="systemInfo.ram + '%'"></span></span>
|
|
</div>
|
|
<div class="status-device" x-text="systemInfo.gpu || (t.status_bar?.cpu_mode || 'CPU Mode')"></div>
|
|
<button id="models-button" @click="modelsOpen = true" class="glass-panel px-3 py-2 text-xs text-neon-cyan rounded">
|
|
Models: <span x-text="models.status"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- PROGRESS OVERLAY (Visible when processing) -->
|
|
<div x-show="processing"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 backdrop-blur-none"
|
|
x-transition:enter-end="opacity-100 backdrop-blur-sm"
|
|
class="absolute inset-0 bg-black/60 backdrop-blur-sm z-50 flex flex-col items-center justify-center p-10">
|
|
|
|
<h2 class="text-4xl font-black italic text-white mb-6 animate-pulse" x-text="statusMsg">REMOVING CRINGE...</h2>
|
|
|
|
<!-- Custom Progress Bar -->
|
|
<div class="w-full max-w-md h-8 bg-gray-800 rounded-full border-2 border-white overflow-hidden relative shadow-[0_0_20px_#ff006e]">
|
|
<div class="h-full bg-gradient-to-r from-neon-pink via-purple-500 to-neon-cyan transition-all duration-300 relative" :style="'width: ' + progress + '%'">
|
|
<!-- Moving Stripe -->
|
|
<div class="absolute inset-0 bg-[linear-gradient(45deg,rgba(255,255,255,0.2)_25%,transparent_25%,transparent_50%,rgba(255,255,255,0.2)_50%,rgba(255,255,255,0.2)_75%,transparent_75%,transparent)] bg-[length:20px_20px] animate-[marquee_1s_linear_infinite]"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="mt-4 text-neon-cyan font-mono text-lg" x-text="progress + '%'"></p>
|
|
|
|
<button @click="stopProcessing()" class="mt-8 px-6 py-2 border border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-full font-bold transition-all uppercase text-xs tracking-widest" x-text="t.buttons?.stop || 'Stop'">
|
|
</button>
|
|
</div>
|
|
|
|
<!-- PREVIEW MODAL -->
|
|
<div x-show="previewMode"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
class="absolute inset-0 bg-black/90 backdrop-blur-md z-50 flex items-center justify-center p-4"
|
|
@click.self="previewMode = false">
|
|
|
|
<div class="bg-card border border-white/10 rounded-2xl p-6 max-w-3xl w-full max-h-full overflow-auto shadow-[0_0_50px_rgba(0,243,255,0.2)]">
|
|
<!-- Header -->
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-xl font-black text-neon-cyan" x-text="t.preview_modal?.title || 'Detection Preview'"></h2>
|
|
<button @click="previewMode = false" class="text-gray-500 hover:text-white text-2xl">×</button>
|
|
</div>
|
|
|
|
<!-- Preview Image -->
|
|
<div class="bg-black rounded-xl overflow-hidden mb-4 flex items-center justify-center min-h-[200px]">
|
|
<template x-if="previewImage">
|
|
<img :src="'data:image/png;base64,' + previewImage" class="max-w-full max-h-[400px] object-contain">
|
|
</template>
|
|
<template x-if="!previewImage && previewLoading">
|
|
<div class="text-center">
|
|
<div class="text-gray-500 animate-pulse mb-2" x-text="t.preview_modal?.loading || 'Loading...'"></div>
|
|
<div class="text-gray-600 text-xs" x-text="t.preview_modal?.loading_hint || ''"></div>
|
|
</div>
|
|
</template>
|
|
<template x-if="previewError">
|
|
<div class="p-4 flex flex-col items-center gap-3 max-w-full">
|
|
<div class="text-error text-sm break-all max-h-[150px] overflow-y-auto w-full" x-text="previewError"></div>
|
|
<button @click="navigator.clipboard.writeText(previewError); $el.textContent = 'Copied!'; setTimeout(() => $el.textContent = t.buttons?.copy_error || '📋 Copy Error', 1500)"
|
|
class="px-3 py-1.5 bg-white/10 hover:bg-white/20 text-gray-300 text-xs rounded-lg border border-white/10 transition-colors" x-text="t.buttons?.copy_error || '📋 Copy Error'">
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Detection Info -->
|
|
<div class="grid grid-cols-2 gap-4 text-sm font-mono mb-4">
|
|
<div class="bg-white/5 p-3 rounded-lg">
|
|
<span class="text-gray-500" x-text="t.preview_modal?.prompt || 'Prompt:'"></span>
|
|
<span class="text-neon-cyan ml-2" x-text="previewPromptUsed || '-'"></span>
|
|
</div>
|
|
<div class="bg-white/5 p-3 rounded-lg">
|
|
<span class="text-gray-500" x-text="t.preview_modal?.detected || 'Detected:'"></span>
|
|
<span class="text-neon-green ml-2" x-text="previewDetections.length + ' ' + (t.preview_modal?.regions || 'region(s)')"></span>
|
|
</div>
|
|
<div class="bg-white/5 p-3 rounded-lg col-span-2">
|
|
<span class="text-gray-500" x-text="t.preview_modal?.source || 'Source:'"></span>
|
|
<span class="text-white ml-2" x-text="previewSource || '-'"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detections List -->
|
|
<template x-if="previewDetections.length > 0">
|
|
<div class="space-y-2 mb-4">
|
|
<div class="text-xs text-gray-500 uppercase" x-text="t.preview_modal?.bounding_boxes || 'Bounding Boxes:'"></div>
|
|
<template x-for="(det, idx) in previewDetections" :key="idx">
|
|
<div class="flex items-center gap-2 text-xs font-mono bg-white/5 p-2 rounded">
|
|
<span class="w-3 h-3 rounded-full" :class="det.accepted ? 'bg-neon-green' : 'bg-error'"></span>
|
|
<span class="text-gray-400" x-text="'#' + (idx + 1)"></span>
|
|
<span class="text-white" x-text="det.area_percent + (t.preview_modal?.of_image || '% of image')"></span>
|
|
<span x-show="!det.accepted" class="text-error" x-text="t.preview_modal?.too_large || '(too large, skip)'"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex gap-3 justify-end">
|
|
<button @click="runPreview()" :disabled="previewLoading"
|
|
class="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg font-bold transition-all disabled:opacity-50">
|
|
<span x-text="previewLoading ? (t.preview_modal?.loading || 'Loading...') : (t.buttons?.try_again || '🔄 Try Again')"></span>
|
|
</button>
|
|
<button @click="previewMode = false"
|
|
class="px-4 py-2 bg-neon-cyan text-black rounded-lg font-bold hover:bg-neon-cyan/80 transition-all">
|
|
<span x-text="t.buttons?.close || 'Close'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<div x-show="bridgeError" class="fixed top-2 left-2 right-2 z-50 bg-card border border-error p-3 text-sm" x-text="bridgeError"></div>
|
|
<div x-show="modelsOpen" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-6">
|
|
<section class="glass-panel model-dialog w-full max-w-xl p-6 rounded-xl" role="dialog" aria-label="AI models">
|
|
<h2 class="text-xl font-bold text-neon-cyan">AI models</h2>
|
|
<p class="text-sm text-gray-400 mt-3">Download once, then process locally. About 1.8 GB is needed for Florence-2 and LaMA.</p>
|
|
<p class="mt-4" x-text="[models.model, models.file].filter(Boolean).join(' — ')"></p>
|
|
<p class="text-sm mt-2 break-words" x-text="models.message || models.status"></p>
|
|
<div x-show="models.status === 'downloading'" class="mt-4">
|
|
<progress class="w-full" max="100" :value="modelProgress()"></progress>
|
|
<p class="text-xs mt-1" x-text="`${modelProgress()}% — ${(models.current / 1048576).toFixed(1)} / ${(models.total / 1048576).toFixed(1)} MB`"></p>
|
|
</div>
|
|
<p x-show="models.status === 'error'" class="text-sm text-gray-400 mt-3">Check your connection and free disk space, then retry. Completed files are reused; partial Florence-2 downloads resume when supported.</p>
|
|
<div class="flex gap-3 mt-5">
|
|
<button id="download-models" @click="downloadModels()" :disabled="models.busy || processing" class="bg-neon-cyan text-black px-4 py-2 rounded disabled:opacity-50">Download / Retry</button>
|
|
<button id="close-models" @click="modelsOpen = false" class="border border-gray-500 px-4 py-2 rounded">Close</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script>
|
|
// Store reference to Alpine app instance for Python callbacks (Alpine 3.x compatible)
|
|
window.appInstance = null;
|
|
|
|
// Global functions for Python to call - dispatch events for Alpine to catch
|
|
function updateProgress(percent) {
|
|
window.dispatchEvent(new CustomEvent('pywebview-progress', {
|
|
detail: { percent }
|
|
}));
|
|
}
|
|
|
|
function addLog(msg, color = 'text-gray-400') {
|
|
window.dispatchEvent(new CustomEvent('pywebview-log', {
|
|
detail: { msg, color }
|
|
}));
|
|
}
|
|
|
|
function processingComplete(result = {success: false}) {
|
|
window.dispatchEvent(new CustomEvent('pywebview-complete', {detail: result}));
|
|
}
|
|
|
|
// Theme switcher function
|
|
function switchTheme(themeName) {
|
|
const themes = ['theme-slay', 'theme-sigma', 'theme-witch', 'theme-korpo', 'theme-coquette', 'theme-xp', 'theme-anime'];
|
|
document.body.classList.remove(...themes);
|
|
if (themeName !== 'brainrot') {
|
|
document.body.classList.add('theme-' + themeName);
|
|
}
|
|
}
|
|
|
|
// i18n - Load language file
|
|
async function loadLanguage(lang) {
|
|
try {
|
|
const response = await fetch(`lang/${lang}.json`);
|
|
if (!response.ok) throw new Error('Language file not found');
|
|
return await response.json();
|
|
} catch (e) {
|
|
console.error('Failed to load language:', lang, e);
|
|
// Fallback to brainrot
|
|
if (lang !== 'brainrot') {
|
|
return await loadLanguage('brainrot');
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function brainrotApp() {
|
|
return {
|
|
isLoading: true, // Show loading screen until config loads
|
|
mode: 'single',
|
|
overwrite: false,
|
|
transparent: false,
|
|
detection: 15,
|
|
format: 'None',
|
|
inputPath: '',
|
|
outputPath: '',
|
|
processing: false,
|
|
progress: 0,
|
|
statusMsg: '',
|
|
theme: 'brainrot', // Default theme for loading screen
|
|
lang: 'en',
|
|
t: {}, // translations
|
|
// Dynamic config loaded from config.json
|
|
appVersion: '',
|
|
availableThemes: [],
|
|
availableLanguages: [],
|
|
logSeq: Date.now(), // helps keep keys unique across reloads
|
|
logs: [],
|
|
systemInfo: {
|
|
cuda: false,
|
|
ram: 0,
|
|
gpu: 'Checking...',
|
|
ffmpeg: false
|
|
},
|
|
|
|
// Detection prompt
|
|
detectionPrompt: 'watermark',
|
|
|
|
// Preview modal state
|
|
previewMode: false,
|
|
previewLoading: false,
|
|
previewImage: null,
|
|
previewDetections: [],
|
|
previewPromptUsed: '',
|
|
previewSource: '',
|
|
previewError: null,
|
|
|
|
// Video settings (two-pass processing)
|
|
detectionSkip: 1,
|
|
fadeIn: 0,
|
|
fadeOut: 0,
|
|
|
|
|
|
async init() {
|
|
if (this._initialized) return;
|
|
this._initialized = true;
|
|
// Store reference for global Python callbacks
|
|
window.appInstance = this;
|
|
|
|
// Load UI config (themes, languages, version) from config.json
|
|
try {
|
|
const uiConfig = await fetch('config.json').then(r => r.json());
|
|
this.appVersion = uiConfig.version || '0.0';
|
|
this.availableThemes = uiConfig.themes || [];
|
|
this.availableLanguages = uiConfig.languages || [];
|
|
} catch (e) {
|
|
console.log('Failed to load config.json, using defaults');
|
|
this.appVersion = '0.0';
|
|
this.availableThemes = [{ id: 'brainrot' }];
|
|
this.availableLanguages = [{ id: 'brainrot', randomizable: true }, { id: 'en', randomizable: false }];
|
|
}
|
|
|
|
// Helper to pick random from array
|
|
const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
const randomizableThemes = this.availableThemes.map(t => t.id);
|
|
const randomizableLangs = this.availableLanguages.filter(l => l.randomizable).map(l => l.id);
|
|
|
|
// Load config when pywebview is ready
|
|
const loadConfig = async () => {
|
|
if (!window.pywebview?.api) return;
|
|
try {
|
|
const config = await pywebview.api.get_config() || {};
|
|
console.log('Config loaded:', config);
|
|
|
|
// Apply all settings
|
|
this.inputPath = config.input_path || '';
|
|
this.outputPath = config.output_path || '';
|
|
this.overwrite = config.overwrite || false;
|
|
this.transparent = config.transparent || false;
|
|
this.detection = config.max_bbox_percent || 15;
|
|
this.format = config.force_format || 'None';
|
|
this.mode = config.mode || 'single';
|
|
this.detectionPrompt = config.detection_prompt || 'watermark';
|
|
this.detectionSkip = config.detection_skip || 1;
|
|
this.fadeIn = config.fade_in || 0;
|
|
this.fadeOut = config.fade_out || 0;
|
|
|
|
// Theme & Language (always load, use saved or pick random)
|
|
this.theme = config.theme || pickRandom(randomizableThemes);
|
|
this.lang = config.lang || pickRandom(randomizableLangs);
|
|
switchTheme(this.theme);
|
|
await this.switchLang(this.lang);
|
|
|
|
this.bridgeError = '';
|
|
this._configLoaded = true; // Now allow saving
|
|
this.isLoading = false; // Hide loading screen
|
|
} catch (e) {
|
|
console.log('Config load failed:', e);
|
|
// Still need to load translations with defaults
|
|
this.theme = pickRandom(randomizableThemes);
|
|
this.lang = pickRandom(randomizableLangs);
|
|
switchTheme(this.theme);
|
|
await this.switchLang(this.lang);
|
|
this.isLoading = false;
|
|
}
|
|
};
|
|
|
|
// Try loading config now if pywebview ready, otherwise wait for event
|
|
if (window.pywebview?.api) {
|
|
await loadConfig();
|
|
} else {
|
|
window.addEventListener('pywebviewready', loadConfig);
|
|
// Fallback timeout in case pywebview event never fires
|
|
setTimeout(() => {
|
|
if (this.isLoading) {
|
|
this.bridgeError = "Connecting to the desktop engine is taking longer than expected. If controls remain unavailable, restart the app and check desktop.log.";
|
|
this.theme = pickRandom(randomizableThemes);
|
|
this.lang = pickRandom(randomizableLangs);
|
|
switchTheme(this.theme);
|
|
this.switchLang(this.lang);
|
|
this.isLoading = false;
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
// Listen for pywebview events (guard against duplicate listeners when window reloads)
|
|
if (window.__pywebviewLogHandler) {
|
|
window.removeEventListener('pywebview-log', window.__pywebviewLogHandler);
|
|
}
|
|
window.__pywebviewLogHandler = (e) => {
|
|
this.addLogEntry(e.detail.msg, e.detail.color);
|
|
};
|
|
window.addEventListener('pywebview-log', window.__pywebviewLogHandler);
|
|
|
|
if (window.__pywebviewProgressHandler) {
|
|
window.removeEventListener('pywebview-progress', window.__pywebviewProgressHandler);
|
|
}
|
|
window.__pywebviewProgressHandler = (e) => {
|
|
this.progress = e.detail.percent;
|
|
};
|
|
window.addEventListener('pywebview-progress', window.__pywebviewProgressHandler);
|
|
|
|
if (window.__pywebviewCompleteHandler) {
|
|
window.removeEventListener('pywebview-complete', window.__pywebviewCompleteHandler);
|
|
}
|
|
window.__pywebviewCompleteHandler = (event) => {
|
|
this.processing = false;
|
|
if (event.detail?.success) {
|
|
this.progress = 100;
|
|
this.addLogEntry(this.t.logs?.done || "Done! File processed successfully.", "text-neon-pink font-bold");
|
|
} else {
|
|
this.addLogEntry(event.detail?.cancelled ? "Processing stopped." : "Processing failed. See the error above and retry.", "text-error");
|
|
}
|
|
};
|
|
window.addEventListener('pywebview-complete', window.__pywebviewCompleteHandler);
|
|
|
|
// Get static info once (CUDA, FFmpeg, GPU)
|
|
this.loadStaticInfo();
|
|
this.pollModels();
|
|
// Poll dynamic info (RAM, CPU) every 3 seconds
|
|
setInterval(() => this.updateDynamicInfo(), 3000);
|
|
},
|
|
|
|
// Auto-save config (debounced)
|
|
_saveTimeout: null,
|
|
_configLoaded: false, // Block saves until config is loaded
|
|
saveConfigDebounced() {
|
|
if (!this._configLoaded) return; // Don't save until config loaded
|
|
clearTimeout(this._saveTimeout);
|
|
this._saveTimeout = setTimeout(() => this.saveConfig(), 500);
|
|
},
|
|
async saveConfig() {
|
|
if (window.pywebview?.api?.save_config) {
|
|
try {
|
|
await pywebview.api.save_config(this.getConfig());
|
|
} catch (e) {
|
|
console.log('Config save failed:', e);
|
|
}
|
|
}
|
|
},
|
|
|
|
async loadStaticInfo() {
|
|
if (window.pywebview) {
|
|
try {
|
|
const info = await pywebview.api.get_static_info();
|
|
this.systemInfo.cuda = info.cuda;
|
|
this.systemInfo.gpu = info.gpu_name || 'CPU MODE';
|
|
this.systemInfo.ffmpeg = info.ffmpeg;
|
|
} catch (e) {
|
|
// API not ready yet, retry
|
|
setTimeout(() => this.loadStaticInfo(), 500);
|
|
}
|
|
} else {
|
|
setTimeout(() => this.loadStaticInfo(), 500);
|
|
}
|
|
},
|
|
|
|
async updateDynamicInfo() {
|
|
if (window.pywebview) {
|
|
try {
|
|
const info = await pywebview.api.get_dynamic_info();
|
|
this.systemInfo.ram = Math.round(info.ram_percent);
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
},
|
|
|
|
isVideoFile(path) {
|
|
if (!path) return false;
|
|
const ext = path.toLowerCase().split('.').pop();
|
|
return ['mp4', 'avi', 'mov', 'mkv', 'flv', 'wmv', 'webm'].includes(ext);
|
|
},
|
|
|
|
async browseInput() {
|
|
const picker =
|
|
window.pywebview?.api?.browse_file ||
|
|
(this.mode === 'single'
|
|
? window.pywebview?.api?.browse_file
|
|
: window.pywebview?.api?.browse_folder);
|
|
|
|
if (!picker) {
|
|
this.addLogEntry(this.t.logs?.api_error || "API not available. Please run via run.bat/run.sh", "text-error");
|
|
return;
|
|
}
|
|
|
|
const path =
|
|
this.mode === 'single'
|
|
? await window.pywebview.api.browse_file()
|
|
: await window.pywebview.api.browse_folder();
|
|
|
|
if (path) {
|
|
this.inputPath = path;
|
|
this.saveConfigDebounced();
|
|
}
|
|
},
|
|
|
|
async browseOutput() {
|
|
if (!window.pywebview?.api?.browse_folder) {
|
|
this.addLogEntry(this.t.logs?.api_error || "API not available. Please run via run.bat/run.sh", "text-error");
|
|
return;
|
|
}
|
|
|
|
const path = await pywebview.api.browse_folder();
|
|
if (path) {
|
|
this.outputPath = path;
|
|
this.saveConfigDebounced();
|
|
}
|
|
},
|
|
|
|
modelsOpen: false,
|
|
models: {status: 'unchecked', busy: true},
|
|
bridgeError: '',
|
|
async pollModels() {
|
|
try {
|
|
if (window.pywebview?.api) {
|
|
const previousStatus = this.models.status;
|
|
this.models = await pywebview.api.get_model_status();
|
|
if (previousStatus !== this.models.status && (this.models.status === 'missing' || this.models.status === 'error')) this.modelsOpen = true;
|
|
}
|
|
} catch (error) { this.bridgeError = error.message; }
|
|
setTimeout(() => this.pollModels(), 500);
|
|
},
|
|
async downloadModels() {
|
|
const result = await pywebview.api.download_models();
|
|
if (result.error) this.bridgeError = result.error;
|
|
else { this.bridgeError = ''; this.models = {status: 'checking', busy: true}; }
|
|
},
|
|
modelProgress() {
|
|
return this.models.total ? Math.min(100, Math.round(100 * this.models.current / this.models.total)) : 0;
|
|
},
|
|
async startProcessing() {
|
|
if (this.models.status !== 'ready') { this.modelsOpen = true; return; }
|
|
if (!this.inputPath) {
|
|
this.addLogEntry(this.t.logs?.no_input || "No input selected. Please select a file first.", "text-error");
|
|
return;
|
|
}
|
|
|
|
// Check for video + transparency warning
|
|
if (this.isVideoFile(this.inputPath) && this.transparent) {
|
|
this.addLogEntry(this.t.logs?.ghost_video_warning || "Ghost mode doesn't work with videos. Turning it off.", "text-yellow-400");
|
|
this.transparent = false;
|
|
}
|
|
|
|
// Check for FFmpeg warning
|
|
if (this.isVideoFile(this.inputPath) && !this.systemInfo.ffmpeg) {
|
|
this.addLogEntry(this.t.logs?.no_ffmpeg || "FFmpeg not found! Video will have no audio.", "text-yellow-400");
|
|
}
|
|
|
|
this.processing = true;
|
|
this.progress = 0;
|
|
this.addLogEntry(this.t.logs?.starting || "Starting process...", "text-white");
|
|
this.addLogEntry(this.t.logs?.first_run_hint || "(First run may download AI models)", "text-gray-500");
|
|
|
|
// Update status message randomly
|
|
const statusInterval = setInterval(() => {
|
|
if (this.processing && this.t.processing_status?.length > 0) {
|
|
this.statusMsg = this.t.processing_status[Math.floor(Math.random() * this.t.processing_status.length)];
|
|
} else {
|
|
clearInterval(statusInterval);
|
|
}
|
|
}, 2000);
|
|
|
|
if (window.pywebview && window.pywebview.api) {
|
|
try {
|
|
const result = await pywebview.api.start_processing({
|
|
input: this.inputPath,
|
|
output: this.outputPath,
|
|
overwrite: this.overwrite,
|
|
transparent: this.transparent,
|
|
max_bbox: this.detection,
|
|
format: this.format,
|
|
mode: this.mode,
|
|
detection_prompt: this.detectionPrompt || 'watermark',
|
|
// Video settings
|
|
detection_skip: parseInt(this.detectionSkip) || 1,
|
|
fade_in: parseFloat(this.fadeIn) || 0,
|
|
fade_out: parseFloat(this.fadeOut) || 0,
|
|
// UI settings
|
|
theme: this.theme,
|
|
lang: this.lang
|
|
});
|
|
|
|
// Check for error response from backend
|
|
if (result && result.error) {
|
|
this.addLogEntry("Error: " + result.error, "text-error");
|
|
this.processing = false;
|
|
clearInterval(statusInterval);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
this.addLogEntry("Error: " + e.message, "text-error");
|
|
this.processing = false;
|
|
clearInterval(statusInterval);
|
|
}
|
|
} else {
|
|
this.addLogEntry(this.t.logs?.api_error || "API not available. Please run via run.bat/run.sh", "text-error");
|
|
this.processing = false;
|
|
clearInterval(statusInterval);
|
|
}
|
|
},
|
|
|
|
async stopProcessing() {
|
|
if (window.pywebview) {
|
|
await pywebview.api.stop_processing();
|
|
}
|
|
this.processing = false;
|
|
this.addLogEntry(this.t.logs?.stopped || "Process stopped by user.", "text-error");
|
|
},
|
|
|
|
addLogEntry(msg, color = 'text-gray-400') {
|
|
const now = new Date();
|
|
const time = now.toTimeString().split(' ')[0];
|
|
|
|
// Use push() - Alpine.js 3.x handles array mutations correctly
|
|
this.logs.push({
|
|
id: this.logSeq++,
|
|
time: time,
|
|
msg: msg,
|
|
color: color
|
|
});
|
|
|
|
// Keep logs limited - use slice() to create new array instead of shift()
|
|
if (this.logs.length > 100) {
|
|
this.logs = this.logs.slice(-100);
|
|
}
|
|
|
|
// Auto scroll logs
|
|
this.$nextTick(() => {
|
|
const container = document.getElementById('logs-container');
|
|
if (container) container.scrollTop = container.scrollHeight;
|
|
});
|
|
},
|
|
|
|
// Preview detection
|
|
async runPreview() {
|
|
if (!this.inputPath) {
|
|
this.addLogEntry(this.t.logs?.no_input || "No input selected. Please select a file first.", "text-error");
|
|
return;
|
|
}
|
|
|
|
this.previewLoading = true;
|
|
this.previewError = null;
|
|
this.previewImage = null; // Clear old preview immediately
|
|
this.previewDetections = [];
|
|
this.previewMode = true;
|
|
|
|
if (window.pywebview) {
|
|
try {
|
|
const result = await pywebview.api.preview_detection({
|
|
input: this.inputPath,
|
|
detection_prompt: this.detectionPrompt || 'watermark',
|
|
max_bbox: this.detection
|
|
});
|
|
|
|
if (result.error) {
|
|
this.previewError = result.error;
|
|
this.previewImage = null;
|
|
this.previewDetections = [];
|
|
this.addLogEntry((this.t.logs?.preview_failed || "Preview failed:") + " " + result.error, "text-error");
|
|
} else {
|
|
this.previewImage = result.image;
|
|
this.previewDetections = result.detections || [];
|
|
this.previewPromptUsed = result.prompt_used;
|
|
this.previewSource = result.source;
|
|
this.previewError = null;
|
|
|
|
const accepted = this.previewDetections.filter(d => d.accepted).length;
|
|
const total = this.previewDetections.length;
|
|
const msg = (this.t.logs?.preview_result || "Detected {total} regions, {accepted} will be removed.")
|
|
.replace('{total}', total)
|
|
.replace('{accepted}', accepted);
|
|
this.addLogEntry(msg, "text-neon-cyan");
|
|
}
|
|
} catch (e) {
|
|
this.previewError = "API error: " + e.message;
|
|
this.addLogEntry((this.t.logs?.preview_failed || "Preview failed:") + " " + e.message, "text-error");
|
|
}
|
|
}
|
|
|
|
this.previewLoading = false;
|
|
},
|
|
|
|
// Switch language
|
|
async switchLang(langCode) {
|
|
const translations = await loadLanguage(langCode);
|
|
if (translations) {
|
|
this.t = translations;
|
|
this.lang = langCode;
|
|
// Update page title
|
|
document.title = this.t.app?.title || 'WM Remover';
|
|
// Set initial status message
|
|
if (this.t.processing_status?.length > 0) {
|
|
this.statusMsg = this.t.processing_status[0];
|
|
}
|
|
// Add init log if logs are empty
|
|
if (this.logs.length === 0) {
|
|
this.addLogEntry(this.t.logs?.init || 'System initialized.', 'text-gray-400');
|
|
}
|
|
// Auto-save config
|
|
this.saveConfigDebounced();
|
|
}
|
|
},
|
|
|
|
// Save config on window close (called from Python)
|
|
getConfig() {
|
|
return {
|
|
input_path: this.inputPath,
|
|
output_path: this.outputPath,
|
|
overwrite: this.overwrite,
|
|
transparent: this.transparent,
|
|
max_bbox_percent: this.detection,
|
|
force_format: this.format,
|
|
mode: this.mode,
|
|
detection_prompt: this.detectionPrompt,
|
|
// Video settings
|
|
detection_skip: parseInt(this.detectionSkip) || 1,
|
|
fade_in: parseFloat(this.fadeIn) || 0,
|
|
fade_out: parseFloat(this.fadeOut) || 0,
|
|
// Theme & Language
|
|
theme: this.theme,
|
|
lang: this.lang
|
|
};
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|