ASCIInator/src/App.vue
dev e904cdb3cb feat: full GUI, test API, auto-convert, aspect-ratio sliders
- Auto-convert on image load (0ms) and flag change (400ms debounce)
- SizeControl: linked width/height sliders with aspect-ratio lock and 0.5 font correction factor
- Full flag exposure for all 4 tools (chafa, jp2a, ascii-image-converter, img2txt)
- ChafaOptions: symbols/fill dropdowns, dither controls, work/threshold/font-ratio sliders, format select, toggles
- Jp2aOptions: color-depth, RGB weight sliders, edge controls, 8 toggles
- ImgTxtOptions: dither select with valid libcaca values, gamma slider
- OutputDisplay: ansi-to-html rendering for colored chafa output
- ShellBridge: abort-previous pattern, conversion-start/end lifecycle events
- Test API (ENABLE_TEST_API=true): /test/health, /test/convert, /test/flags/:tool, /test/imagemagick
- buildArgs: space-separated args (not = format); full schemas in SCHEMAS export
- runChafa: width/height destructured and combined into --size WxH
- Port changed to 3050; Vite on 0.0.0.0 with allowedHosts for production domain
- 98 unit tests passing across 12 test files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 10:29:45 +00:00

106 lines
2.9 KiB
Vue

<script setup>
import { reactive, ref, watch } from 'vue'
import ImageInput from './components/ImageInput.vue'
import ToolSelector from './components/ToolSelector.vue'
import ShellBridge from './components/ShellBridge.vue'
import OutputDisplay from './components/OutputDisplay.vue'
import ErrorLog from './components/ErrorLog.vue'
const state = reactive({
image: null,
imageDims: null,
toolConfig: { tool: 'chafa', flags: {} },
result: '',
errors: [],
isConverting: false,
})
const bridge = ref(null)
let debounceTimer = null
function addError(message, source = 'error') {
state.errors.push({
timestamp: new Date().toLocaleTimeString(),
source,
message,
})
}
function scheduleConvert(delay = 400) {
if (!state.image) return
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => bridge.value?.convert(), delay)
}
function onImageReady(img) {
state.image = img
const el = new Image()
el.onload = () => { state.imageDims = { w: el.naturalWidth, h: el.naturalHeight } }
el.src = img.dataUrl
}
// New image — convert immediately
watch(() => state.image, (img) => {
if (img) scheduleConvert(0)
})
// Flag / tool changes — debounce for slider drag
watch(() => state.toolConfig, () => scheduleConvert(400), { deep: true })
function onResult(text) {
state.result = text
}
function onError(message) {
addError(message)
}
</script>
<template>
<div class="min-h-screen pb-20" style="background-color: var(--ascii-bg)">
<header class="px-6 py-4 border-b flex items-center gap-4" style="border-color: var(--ascii-border)">
<h1
class="text-lg font-semibold tracking-widest font-mono"
style="color: var(--ascii-green)"
>
ASCIInator
</h1>
<span
v-if="state.isConverting"
class="text-xs font-mono animate-pulse"
style="color: rgba(57,255,20,.5)"
>converting…</span>
</header>
<main class="max-w-5xl mx-auto px-6 py-8 flex flex-col gap-8">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<section>
<h2 class="text-xs font-medium uppercase tracking-wider mb-3"
style="color: rgba(224,224,224,.35)">Image</h2>
<ImageInput @image-ready="onImageReady" />
</section>
<section>
<h2 class="text-xs font-medium uppercase tracking-wider mb-3"
style="color: rgba(224,224,224,.35)">Tool &amp; Options</h2>
<ToolSelector :image-dims="state.imageDims" @tool-config="state.toolConfig = $event" />
</section>
</div>
<ShellBridge
ref="bridge"
:image="state.image"
:tool-config="state.toolConfig"
@conversion-result="onResult"
@conversion-error="onError"
@conversion-start="state.isConverting = true"
@conversion-end="state.isConverting = false"
/>
<OutputDisplay :result="state.result" />
</main>
<ErrorLog :errors="state.errors" @clear="state.errors = []" />
</div>
</template>