refactor: migrate app to Alpine.js
This commit is contained in:
1 parent
766212f119
commit
bc236db952
9 files changed
+269
-216
No files matched your search
@@ -1,113 +1,12 @@
|
||||
import Alpine from './vendor/alpine.js';
|
||||
import {render, styles, resolveFont} from './renderer.js';
|
||||
import {translations} from './translations.js';
|
||||
import {setupPWA} from './pwa.js';
|
||||
|
||||
const text = document.getElementById('text');
|
||||
const style = document.getElementById('style');
|
||||
const output = document.getElementById('output');
|
||||
const status = document.getElementById('status');
|
||||
const language = document.getElementById('language');
|
||||
const theme = document.getElementById('theme');
|
||||
let lang = document.documentElement.lang;
|
||||
let statusKey = '';
|
||||
const t = key => translations[lang][key];
|
||||
const showStatus = key => {
|
||||
statusKey = key;
|
||||
status.textContent = key ? t(key) : '';
|
||||
};
|
||||
const savePreference = (key, value) => {
|
||||
function savePreference(key, value) {
|
||||
try { localStorage.setItem(`ascii-pwa-${key}`, value); } catch { /* Storage may be disabled. */ }
|
||||
};
|
||||
function translate() {
|
||||
document.documentElement.lang = lang;
|
||||
language.value = lang;
|
||||
document.querySelectorAll('[data-i18n]').forEach(element => {
|
||||
element.textContent = t(element.dataset.i18n);
|
||||
});
|
||||
output.setAttribute('aria-label', t('outputLabel'));
|
||||
document.querySelector('.logo').setAttribute('aria-label', t('homeLabel'));
|
||||
updateFontNavigation();
|
||||
showStatus(statusKey);
|
||||
}
|
||||
let drawVersion = 0;
|
||||
let drawTimer;
|
||||
function updateFontNavigation() {
|
||||
const next = styles[(styles.indexOf(style.value) + 1) % styles.length];
|
||||
document.getElementById('next-font').textContent = t('nextFont').replace('{name}', next);
|
||||
}
|
||||
async function draw() {
|
||||
const version = ++drawVersion;
|
||||
const copy = document.getElementById('copy');
|
||||
const download = document.getElementById('download');
|
||||
copy.disabled = true;
|
||||
download.disabled = true;
|
||||
try {
|
||||
const result = await render(text.value, style.value);
|
||||
if (version !== drawVersion) return;
|
||||
output.textContent = result;
|
||||
showStatus('');
|
||||
} catch {
|
||||
if (version !== drawVersion) return;
|
||||
output.textContent = '';
|
||||
showStatus('renderFailed');
|
||||
}
|
||||
document.getElementById('copy').disabled = !output.textContent.trim();
|
||||
document.getElementById('download').disabled = !output.textContent.trim();
|
||||
}
|
||||
|
||||
// Explicit shared URLs take precedence; a normal visit always opens the homepage.
|
||||
const params = new URL(window.location.href).searchParams;
|
||||
style.replaceChildren(...styles.map(font => {
|
||||
const option = document.createElement('option');
|
||||
option.value = font;
|
||||
option.textContent = font;
|
||||
return option;
|
||||
}));
|
||||
style.value = 'Standard';
|
||||
try {
|
||||
const savedFont = localStorage.getItem('ascii-pwa-font');
|
||||
if (savedFont) style.value = resolveFont(savedFont);
|
||||
} catch { /* Use the default when saved preferences are unavailable. */ }
|
||||
if (params.has('text')) text.value = params.get('text').slice(0, 500);
|
||||
if (params.has('font')) style.value = resolveFont(params.get('font'));
|
||||
savePreference('font', style.value);
|
||||
theme.value = document.documentElement.dataset.theme;
|
||||
translate();
|
||||
draw();
|
||||
|
||||
document.getElementById('render-form').addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
draw();
|
||||
});
|
||||
style.addEventListener('change', () => {
|
||||
clearTimeout(drawTimer);
|
||||
savePreference('font', style.value);
|
||||
updateFontNavigation();
|
||||
draw();
|
||||
});
|
||||
text.addEventListener('input', () => {
|
||||
clearTimeout(drawTimer);
|
||||
++drawVersion;
|
||||
document.getElementById('copy').disabled = true;
|
||||
document.getElementById('download').disabled = true;
|
||||
drawTimer = setTimeout(draw, 150);
|
||||
});
|
||||
for (const [id, direction] of [['prev-font', -1], ['next-font', 1]]) {
|
||||
document.getElementById(id).addEventListener('click', () => {
|
||||
const index = (styles.indexOf(style.value) + direction + styles.length) % styles.length;
|
||||
style.value = styles[index];
|
||||
style.dispatchEvent(new Event('change'));
|
||||
});
|
||||
}
|
||||
language.addEventListener('change', () => {
|
||||
lang = language.value;
|
||||
savePreference('language', lang);
|
||||
translate();
|
||||
});
|
||||
theme.addEventListener('change', () => {
|
||||
document.documentElement.dataset.theme = theme.value;
|
||||
savePreference('theme', theme.value);
|
||||
});
|
||||
function copyText(value) {
|
||||
const field = document.createElement('textarea');
|
||||
const focused = document.activeElement;
|
||||
@@ -126,42 +25,133 @@ function copyText(value) {
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('copy').addEventListener('click', () => {
|
||||
try {
|
||||
copyText(output.textContent);
|
||||
showStatus('copied');
|
||||
} catch {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(output);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
showStatus('copyFailed');
|
||||
}
|
||||
});
|
||||
document.getElementById('download').addEventListener('click', () => {
|
||||
const url = URL.createObjectURL(new Blob([output.textContent + '\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);
|
||||
showStatus('downloaded');
|
||||
});
|
||||
document.getElementById('share').addEventListener('click', () => {
|
||||
const url = new URL('./', window.location.href);
|
||||
url.searchParams.set('text', text.value);
|
||||
url.searchParams.set('font', style.value);
|
||||
try {
|
||||
copyText(url.href);
|
||||
showStatus('linkCopied');
|
||||
} catch {
|
||||
window.history.replaceState(null, '', url);
|
||||
showStatus('linkFailed');
|
||||
}
|
||||
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(); },
|
||||
};
|
||||
});
|
||||
|
||||
setupPWA({
|
||||
failed() { showStatus('offlineFailed'); },
|
||||
});
|
||||
Alpine.start();
|
||||
Reference in new issue
Block a user