added routing for loading into specific pages. updated translations to appear near the top again.

This commit is contained in:
ph1r3754r73r
2026-06-13 14:02:07 -07:00
parent b90397f0c0
commit 384876a83b
5 changed files with 261 additions and 111 deletions
+1
View File
@@ -423,6 +423,7 @@
<script src="js/utils/history.js"></script>
<script src="js/utils/clipboard.js"></script>
<script src="js/utils/theme.js"></script>
<script src="js/utils/routing.js"></script>
<script src="js/utils/emoji.js"></script>
<!-- Core modules (feature libraries) -->
+81 -1
View File
@@ -137,7 +137,59 @@ window.app = new Vue({
setTimeout(()=>section && section.classList.remove('shake-once','randomizer-glow'), 600);
} catch(_) {}
},
switchToTab(tabName) {
getValidToolIds() {
if (window.toolRegistry && typeof window.toolRegistry.getAll === 'function') {
return window.toolRegistry.getAll().map(function(tool) { return tool.id; });
}
return [];
},
getRouteSubState(tabName) {
if (tabName === 'codes' && this.codesMode === 'decode') {
return 'decode';
}
return '';
},
applyRouteSubState(route) {
if (!route || !route.sub) {
return;
}
if (route.tab === 'codes' && (route.sub === 'generate' || route.sub === 'decode')) {
this.codesMode = route.sub;
}
},
applyRouteFromHash() {
if (!window.TabRouting) {
return;
}
var route = window.TabRouting.parse();
var validIds = this.getValidToolIds();
var defaultTab = 'transforms';
if (!route || !route.tab) {
return;
}
if (!validIds.includes(route.tab)) {
this.switchToTab(defaultTab, { fromRoute: true, updateUrl: true, replaceUrl: true });
return;
}
if (route.tab !== this.activeTab) {
this.switchToTab(route.tab, { fromRoute: true, updateUrl: false, route: route });
return;
}
this.applyRouteSubState(route);
},
switchToTab(tabName, options) {
options = options || {};
if (this.activeTab && window.toolRegistry) {
window.toolRegistry.deactivateTool(this.activeTab, this);
}
@@ -149,6 +201,14 @@ window.app = new Vue({
if (window.toolRegistry) {
window.toolRegistry.activateTool(tabName, this);
}
if (options.fromRoute && options.route) {
this.applyRouteSubState(options.route);
}
if (options.updateUrl !== false && !options.fromRoute && window.TabRouting) {
window.TabRouting.setHash(tabName, this.getRouteSubState(tabName), !!options.replaceUrl);
}
},
toggleTheme() {
@@ -392,6 +452,21 @@ window.app = new Vue({
if (window.toolRegistry && typeof window.toolRegistry.getAll === 'function') {
this.registeredTools = window.toolRegistry.getAll();
}
var initialRoute = window.TabRouting && window.TabRouting.parse();
if (initialRoute && initialRoute.tab && this.getValidToolIds().includes(initialRoute.tab)) {
this.applyRouteFromHash();
} else if (window.toolRegistry) {
window.toolRegistry.activateTool(this.activeTab, this);
}
this._onHashChange = () => {
if (window.TabRouting && window.TabRouting.shouldIgnoreHashChange()) {
return;
}
this.applyRouteFromHash();
};
window.addEventListener('hashchange', this._onHashChange);
this.$nextTick(() => {
const closeButton = document.querySelector('#unicode-options-panel .close-button');
@@ -508,6 +583,11 @@ window.app = new Vue({
},
beforeDestroy() {
if (this._onHashChange) {
window.removeEventListener('hashchange', this._onHashChange);
this._onHashChange = null;
}
if (this._emojiGridInitializer) {
clearInterval(this._emojiGridInitializer);
this._emojiGridInitializer = null;
+4 -1
View File
@@ -241,8 +241,11 @@ class CodesTool extends Tool {
getVueWatchers() {
return {
codesMode: function() {
codesMode: function(mode) {
this.codesError = '';
if (this.activeTab === 'codes' && window.TabRouting) {
window.TabRouting.setHash('codes', mode === 'decode' ? 'decode' : '');
}
},
codesFormat: function() {
this.codesClearOutput();
+67
View File
@@ -0,0 +1,67 @@
/**
* Hash-based tab routing for static hosting (GitHub Pages friendly).
* Examples: #decoder, #/transforms, #codes/decode
*/
(function(global) {
var suppressHashChange = false;
function normalizeSegment(value) {
return String(value || '').trim().toLowerCase();
}
function parseHash() {
var raw = (global.location.hash || '').replace(/^#\/?/, '');
if (!raw) {
return null;
}
var slashIndex = raw.indexOf('/');
if (slashIndex === -1) {
return {
tab: normalizeSegment(decodeURIComponent(raw)),
sub: ''
};
}
return {
tab: normalizeSegment(decodeURIComponent(raw.slice(0, slashIndex))),
sub: normalizeSegment(decodeURIComponent(raw.slice(slashIndex + 1)))
};
}
function buildHash(tab, sub) {
var hash = '#' + encodeURIComponent(String(tab || '').trim());
if (sub) {
hash += '/' + encodeURIComponent(String(sub).trim());
}
return hash;
}
function setHash(tab, sub, replace) {
var next = buildHash(tab, sub);
if (global.location.hash === next) {
return;
}
suppressHashChange = true;
if (replace && global.history && typeof global.history.replaceState === 'function') {
global.history.replaceState(null, '', next);
suppressHashChange = false;
return;
}
global.location.hash = next;
global.setTimeout(function() {
suppressHashChange = false;
}, 0);
}
global.TabRouting = {
parse: parseHash,
buildHash: buildHash,
setHash: setHash,
shouldIgnoreHashChange: function() {
return suppressHashChange;
}
};
})(typeof window !== 'undefined' ? window : globalThis);
+108 -109
View File
@@ -190,126 +190,125 @@
</template>
</div>
</div>
<template v-for="(category, categoryIndex) in categories" :key="category">
<!-- AI Translation Section — renders above "symbol" (scripts & alphabets) -->
<div v-if="category === 'symbol'" id="category-translate" class="transform-category-section translate-inline-section">
<h4 class="category-title transform-category-translate">
<i class="fas fa-language"></i> Translation (AI)
<small class="translate-powered-by">TranslateGemma</small>
</h4>
<div v-if="translateError" class="pc-error">
<i class="fas fa-exclamation-triangle"></i> {{ translateError }}
</div>
<!-- AI Translation — pinned after Favorites / Last Used, before transform categories -->
<div id="category-translate" class="transform-category-section translate-inline-section">
<h4 class="category-title transform-category-translate">
<i class="fas fa-language"></i> Translation (AI)
<small class="translate-powered-by">TranslateGemma</small>
</h4>
<div v-if="translateLoading" class="translate-loading">
<i class="fas fa-spinner fa-spin"></i> Translating to {{ translateActiveLang }}...
</div>
<div v-if="translateError" class="pc-error">
<i class="fas fa-exclamation-triangle"></i> {{ translateError }}
</div>
<div class="translate-model-picker">
<select v-model="translateModel" class="translate-model-select">
<option v-for="m in translateModels" :key="m.id" :value="m.id">{{ m.name }} — {{ m.note }}</option>
</select>
</div>
<div v-if="translateLoading" class="translate-loading">
<i class="fas fa-spinner fa-spin"></i> Translating to {{ translateActiveLang }}...
</div>
<div class="translate-subsection">
<div class="translate-subsection-label"><i class="fas fa-globe"></i> Major</div>
<div class="translate-lang-grid translate-lang-grid-inline">
<button
v-for="lang in translateMainLangs"
:key="lang.code"
class="translate-lang-btn translate-lang-main"
@click="translateTo(lang.name)"
:disabled="translateLoading"
:title="'Translate to ' + lang.name"
>
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
<span class="translate-name">{{ lang.name }}</span>
<i
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
class="fas fa-star favorite-icon translate-lang-favorite"
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
></i>
</button>
</div>
</div>
<div class="translate-model-picker">
<select v-model="translateModel" class="translate-model-select">
<option v-for="m in translateModels" :key="m.id" :value="m.id">{{ m.name }} — {{ m.note }}</option>
</select>
</div>
<div class="translate-subsection">
<div class="translate-subsection-label"><i class="fas fa-scroll"></i> Dead &amp; Exotic</div>
<div class="translate-lang-grid translate-lang-grid-inline">
<button
v-for="lang in translateExoticLangs"
:key="lang.code"
class="translate-lang-btn translate-lang-exotic"
@click="translateTo(lang.name)"
:disabled="translateLoading"
:title="'Translate to ' + lang.name"
>
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
<span class="translate-name">{{ lang.name }}</span>
<i
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
class="fas fa-star favorite-icon translate-lang-favorite"
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
></i>
</button>
</div>
</div>
<div class="translate-subsection">
<div class="translate-subsection">
<div class="translate-subsection-label"><i class="fas fa-globe"></i> Major</div>
<div class="translate-lang-grid translate-lang-grid-inline">
<button
type="button"
class="translate-subsection-label translate-custom-label-btn"
@click="translateAddingLang = !translateAddingLang"
title="Add a language"
v-for="lang in translateMainLangs"
:key="lang.code"
class="translate-lang-btn translate-lang-main"
@click="translateTo(lang.name)"
:disabled="translateLoading"
:title="'Translate to ' + lang.name"
>
<i class="fas fa-plus-circle"></i>
<span>Custom</span>
<span class="translate-custom-toggle-end" aria-hidden="true">
<i :class="translateAddingLang ? 'fas fa-minus' : 'fas fa-plus'"></i>
</span>
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
<span class="translate-name">{{ lang.name }}</span>
<i
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
class="fas fa-star favorite-icon translate-lang-favorite"
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
></i>
</button>
<div v-if="translateAddingLang" class="translate-add-form">
<input
type="text"
v-model="translateNewLangName"
placeholder="e.g., Tagalog, Esperanto, Nahuatl..."
@keyup.enter="translateAddCustomLang"
/>
<button class="translate-add-btn" @click="translateAddCustomLang" :disabled="!translateNewLangName.trim()">
<i class="fas fa-plus"></i> Add
</button>
</div>
<div class="translate-lang-grid translate-lang-grid-inline" v-if="translateCustomLangs.length">
<button
v-for="(lang, idx) in translateCustomLangs"
:key="'custom-'+idx"
class="translate-lang-btn translate-lang-custom"
@click="translateTo(lang.name)"
:disabled="translateLoading"
:title="'Translate to ' + lang.name"
>
<span class="translate-flag translate-custom-flag" aria-hidden="true"><i class="fas fa-globe"></i></span>
<span class="translate-name">{{ lang.name }}</span>
<span class="translate-remove" @click.stop="translateRemoveCustomLang(idx)" title="Remove">&times;</span>
<i
@click.stop="toggleTranslateFavorite(lang.name, true, $event)"
class="fas fa-star favorite-icon translate-lang-favorite"
:class="{ favorited: isTranslateFavorite(lang.name, true) }"
:title="isTranslateFavorite(lang.name, true) ? 'Remove from favorites' : 'Add to favorites'"
></i>
</button>
</div>
<div v-if="!translateCustomLangs.length && !translateAddingLang" class="translate-empty-custom">
<small>Click <strong>Custom</strong> above to add any language.</small>
</div>
</div>
</div>
<!-- Special handling for randomizer category -->
<div class="translate-subsection">
<div class="translate-subsection-label"><i class="fas fa-scroll"></i> Dead &amp; Exotic</div>
<div class="translate-lang-grid translate-lang-grid-inline">
<button
v-for="lang in translateExoticLangs"
:key="lang.code"
class="translate-lang-btn translate-lang-exotic"
@click="translateTo(lang.name)"
:disabled="translateLoading"
:title="'Translate to ' + lang.name"
>
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
<span class="translate-name">{{ lang.name }}</span>
<i
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
class="fas fa-star favorite-icon translate-lang-favorite"
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
></i>
</button>
</div>
</div>
<div class="translate-subsection">
<button
type="button"
class="translate-subsection-label translate-custom-label-btn"
@click="translateAddingLang = !translateAddingLang"
title="Add a language"
>
<i class="fas fa-plus-circle"></i>
<span>Custom</span>
<span class="translate-custom-toggle-end" aria-hidden="true">
<i :class="translateAddingLang ? 'fas fa-minus' : 'fas fa-plus'"></i>
</span>
</button>
<div v-if="translateAddingLang" class="translate-add-form">
<input
type="text"
v-model="translateNewLangName"
placeholder="e.g., Tagalog, Esperanto, Nahuatl..."
@keyup.enter="translateAddCustomLang"
/>
<button class="translate-add-btn" @click="translateAddCustomLang" :disabled="!translateNewLangName.trim()">
<i class="fas fa-plus"></i> Add
</button>
</div>
<div class="translate-lang-grid translate-lang-grid-inline" v-if="translateCustomLangs.length">
<button
v-for="(lang, idx) in translateCustomLangs"
:key="'custom-'+idx"
class="translate-lang-btn translate-lang-custom"
@click="translateTo(lang.name)"
:disabled="translateLoading"
:title="'Translate to ' + lang.name"
>
<span class="translate-flag translate-custom-flag" aria-hidden="true"><i class="fas fa-globe"></i></span>
<span class="translate-name">{{ lang.name }}</span>
<span class="translate-remove" @click.stop="translateRemoveCustomLang(idx)" title="Remove">&times;</span>
<i
@click.stop="toggleTranslateFavorite(lang.name, true, $event)"
class="fas fa-star favorite-icon translate-lang-favorite"
:class="{ favorited: isTranslateFavorite(lang.name, true) }"
:title="isTranslateFavorite(lang.name, true) ? 'Remove from favorites' : 'Add to favorites'"
></i>
</button>
</div>
<div v-if="!translateCustomLangs.length && !translateAddingLang" class="translate-empty-custom">
<small>Click <strong>Custom</strong> above to add any language.</small>
</div>
</div>
</div>
<template v-for="(category, categoryIndex) in categories" :key="category">
<div
v-if="isSpecialCategory(category)"
:id="'category-' + category"