import Alpine from './vendor/alpine.js'; import {render, styles, resolveFont} from './renderer.js'; import {translations} from './translations.js'; import {setupPWA} from './pwa.js'; function savePreference(key, value) { try { localStorage.setItem(`ascii-pwa-${key}`, value); } catch { /* Storage may be disabled. */ } } function copyText(value) { const field = document.createElement('textarea'); const focused = document.activeElement; field.value = value; field.readOnly = true; field.style.cssText = 'position:fixed;left:-9999px;top:0;font-size:16px'; document.body.append(field); try { field.focus({preventScroll: true}); field.select(); field.setSelectionRange(0, field.value.length); if (!document.execCommand('copy')) throw new Error('Copy failed'); } finally { field.remove(); focused?.focus({preventScroll: true}); } } Alpine.data('asciiApp', () => { let drawVersion = 0; let drawTimer; let pwa; return { styles, text: 'hello world', font: 'Standard', ascii: '', busy: true, statusKey: '', language: document.documentElement.lang, theme: document.documentElement.dataset.theme, installAvailable: false, updateAvailable: false, installed: false, get nextFont() { return styles[(styles.indexOf(this.font) + 1) % styles.length]; }, get canCopy() { return !this.busy && !!this.ascii.trim(); }, t(key) { return translations[this.language][key]; }, init() { try { const saved = localStorage.getItem('ascii-pwa-font'); if (saved) this.font = resolveFont(saved); } catch { /* Use the default when saved preferences are unavailable. */ } const params = new URL(window.location.href).searchParams; if (params.has('text')) this.text = params.get('text').slice(0, 500); if (params.has('font')) this.font = resolveFont(params.get('font')); savePreference('font', this.font); this.$watch('text', () => this.scheduleDraw()); this.$watch('font', () => { savePreference('font', this.font); this.draw(); }); this.$watch('language', () => { document.documentElement.lang = this.language; savePreference('language', this.language); }); this.$watch('theme', () => { document.documentElement.dataset.theme = this.theme; savePreference('theme', this.theme); }); pwa = setupPWA({ installAvailable: value => { this.installAvailable = value; }, updateAvailable: value => { this.updateAvailable = value; }, installed: value => { this.installed = value; }, failed: () => { this.statusKey = 'offlineFailed'; }, }); this.draw(); }, destroy() { clearTimeout(drawTimer); ++drawVersion; pwa?.destroy(); }, scheduleDraw() { clearTimeout(drawTimer); ++drawVersion; this.busy = true; drawTimer = setTimeout(() => this.draw(), 150); }, async draw() { clearTimeout(drawTimer); const version = ++drawVersion; this.busy = true; try { const result = await render(this.text, this.font); if (version !== drawVersion) return; this.ascii = result; this.statusKey = ''; } catch { if (version !== drawVersion) return; this.ascii = ''; this.statusKey = 'renderFailed'; } finally { if (version === drawVersion) this.busy = false; } }, cycleFont(direction) { this.font = styles[(styles.indexOf(this.font) + direction + styles.length) % styles.length]; }, copy() { try { copyText(this.ascii); this.statusKey = 'copied'; } catch { const range = document.createRange(); range.selectNodeContents(this.$refs.output); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); this.statusKey = 'copyFailed'; } }, download() { const url = URL.createObjectURL(new Blob([this.ascii + '\n'], {type: 'text/plain;charset=utf-8'})); const link = document.createElement('a'); link.href = url; link.download = 'ascii-art.txt'; document.body.append(link); link.click(); link.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); this.statusKey = 'downloaded'; }, share() { const url = new URL('./', window.location.href); url.searchParams.set('text', this.text); url.searchParams.set('font', this.font); try { copyText(url.href); this.statusKey = 'linkCopied'; } catch { window.history.replaceState(null, '', url); this.statusKey = 'linkFailed'; } }, install() { return pwa.install(); }, update() { pwa.update(); }, }; }); Alpine.start();