mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
563 lines
23 KiB
HTML
563 lines
23 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Installed Font Detector — Preview installed fonts and generate ?font links</title>
|
||
<meta name="description" content="Detect installed fonts in your browser, preview live samples, and copy URL parameter links (?font=) compatible with VDO.Ninja, SocialStream.Ninja, and Caption.Ninja." />
|
||
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
|
||
<meta name="author" content="Installed Font Detector" />
|
||
<meta name="keywords" content="fonts, installed fonts, detect fonts, browser fonts, font preview, url parameter, vdo.ninja, socialstream.ninja, caption.ninja" />
|
||
<meta name="theme-color" content="#0f1221" />
|
||
|
||
<!-- Open Graph -->
|
||
<meta property="og:type" content="website" />
|
||
<meta property="og:title" content="Installed Font Detector" />
|
||
<meta property="og:description" content="Preview your system fonts and generate ?font= links compatible with VDO.Ninja, SocialStream.Ninja, and Caption.Ninja." />
|
||
<meta property="og:locale" content="en_US" />
|
||
|
||
<!-- Twitter Cards -->
|
||
<meta name="twitter:card" content="summary" />
|
||
<meta name="twitter:title" content="Installed Font Detector" />
|
||
<meta name="twitter:description" content="Preview installed fonts and copy ?font= links for Ninja apps." />
|
||
|
||
<!-- Structured data -->
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@type": "WebApplication",
|
||
"name": "Installed Font Detector",
|
||
"applicationCategory": "Utility",
|
||
"operatingSystem": "Windows, macOS, Linux, ChromeOS",
|
||
"isAccessibleForFree": true,
|
||
"inLanguage": "en",
|
||
"description": "Detects installed fonts in your browser, previews them, and generates ?font= URL parameters compatible with VDO.Ninja, SocialStream.Ninja, and Caption.Ninja.",
|
||
"featureList": [
|
||
"Detect installed fonts via text-metric comparison",
|
||
"Optional Local Fonts API for precise enumeration",
|
||
"Generate ?font=<family> links",
|
||
"Compatibility with VDO.Ninja, SocialStream.Ninja, Caption.Ninja"
|
||
],
|
||
"softwareRequirements": "Optional Local Fonts API requires Chromium-based browser on HTTPS/localhost and user permission.",
|
||
"sameAs": [
|
||
"https://vdo.ninja",
|
||
"https://socialstream.ninja",
|
||
"https://caption.ninja"
|
||
]
|
||
}
|
||
</script>
|
||
<style>
|
||
:root {
|
||
--font-family: Avenir Next, Sora, Roboto, Helvetica, Geneva, Verdana, Arial, sans-serif;
|
||
--bg: #0f1221;
|
||
--panel: #171a2b;
|
||
--text: #e7e9f4;
|
||
--muted: #a8aec9;
|
||
--accent: #7ab6ff;
|
||
--ok: #47d16a;
|
||
--warn: #ffb020;
|
||
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||
--radius: 10px;
|
||
}
|
||
|
||
html, body {
|
||
height: 100%;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: var(--font-family);
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
|
||
.wrap {
|
||
max-width: 1100px;
|
||
margin: 24px auto 80px;
|
||
padding: 0 16px;
|
||
}
|
||
|
||
header h1 {
|
||
margin: 0 0 6px 0;
|
||
font-size: 28px;
|
||
}
|
||
|
||
header p {
|
||
margin: 0 0 16px 0;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.controls {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto auto auto;
|
||
gap: 10px;
|
||
align-items: center;
|
||
background: var(--panel);
|
||
border-radius: var(--radius);
|
||
padding: 12px;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
}
|
||
|
||
.controls input[type="text"] {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid #2a2e44;
|
||
background: #0f1221;
|
||
color: var(--text);
|
||
}
|
||
|
||
.controls button, .controls a.button {
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid #2a2e44;
|
||
background: #0f1221;
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.controls .count {
|
||
color: var(--muted);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.results {
|
||
margin-top: 16px;
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
|
||
.row {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
gap: 12px;
|
||
align-items: center;
|
||
background: var(--panel);
|
||
border-radius: var(--radius);
|
||
padding: 12px;
|
||
border: 1px solid #20243a;
|
||
}
|
||
|
||
.sample {
|
||
font-size: 18px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.meta {
|
||
display: grid;
|
||
gap: 6px;
|
||
justify-items: end;
|
||
}
|
||
|
||
.fontname {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.link {
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
user-select: all;
|
||
}
|
||
|
||
.link a {
|
||
color: var(--accent);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.badge {
|
||
font-size: 11px;
|
||
padding: 2px 6px;
|
||
border-radius: 999px;
|
||
border: 1px solid #2a2e44;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.footer {
|
||
margin-top: 40px;
|
||
background: var(--panel);
|
||
border-radius: var(--radius);
|
||
padding: 24px 28px;
|
||
border: 1px solid #20243a;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.footer h2 {
|
||
margin: 0 0 12px 0;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.footer ul {
|
||
margin: 12px 0 0 24px;
|
||
padding: 0;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.footer li { margin: 6px 0; }
|
||
|
||
.footer code {
|
||
font-family: var(--mono);
|
||
background: #0f1221;
|
||
padding: 2px 6px;
|
||
border-radius: 6px;
|
||
border: 1px solid #2a2e44;
|
||
color: #cfe2ff;
|
||
}
|
||
|
||
.note {
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
@media (min-width: 900px) {
|
||
.results {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<header>
|
||
<h1>Installed Font Detector</h1>
|
||
<p>Lists fonts your browser can use, shows a live sample, and provides a URL parameter to apply the font site‑wide.</p>
|
||
</header>
|
||
|
||
<div class="controls">
|
||
<input id="filter" type="text" placeholder="Filter fonts (e.g. Segoe, Mono, Arabic)" />
|
||
<button id="tryLocalFonts" title="Requests permission (if supported) to list local fonts via the Local Fonts API">Try Local Fonts API</button>
|
||
<label class="count" style="display:flex;align-items:center;gap:6px;white-space:nowrap;">
|
||
<input id="facesToggle" type="checkbox" /> Show faces
|
||
</label>
|
||
<div class="count" id="count">Detecting…</div>
|
||
</div>
|
||
|
||
<div class="results" id="results" role="list"></div>
|
||
|
||
<div class="footer">
|
||
<h2>Using a specific font via URL</h2>
|
||
<ul>
|
||
<li><b>Param:</b> Append <code>?font=<Font%20Family%20Name></code> to the URL. Spaces and symbols must be URL‑encoded.</li>
|
||
<li><b>Applies to CSS var:</b> Your site uses <code>--font-family</code>; this page also respects it. Example: <code>?font=Segoe%20UI</code>.</li>
|
||
<li><b>Quotes not needed:</b> Do not include quotes in the parameter; they are not required in the CSS variable.</li>
|
||
<li><b>Installed only:</b> The parameter only works for fonts already installed on your system. It does not download webfonts.</li>
|
||
</ul>
|
||
|
||
<h2>Compatibility (Ninja apps)</h2>
|
||
<ul>
|
||
<li><b>VDO.Ninja:</b> Generated <code>?font=</code> links work with overlays/scenes that honor the <code>--font-family</code> variable.</li>
|
||
<li><b>SocialStream.Ninja:</b> Use the same <code>?font=</code> parameter to override the default font in supported widgets.</li>
|
||
<li><b>Caption.Ninja:</b> Apply <code>?font=</code> to caption overlays that support custom font families.</li>
|
||
</ul>
|
||
<p class="note">These apps accept a font-family override when the <code>?font=</code> parameter is supported in their URLs or embed links. This tool generates properly encoded values for drop‑in use.</p>
|
||
|
||
<h2>Troubleshooting (Windows)</h2>
|
||
<ul>
|
||
<li><b>Exact family name:</b> Use the font’s <i>family</i> name (not file name). Check Settings → Personalization → Fonts for the proper family label.</li>
|
||
<li><b>Right install scope:</b> If you installed a font for a different Windows user or via an admin account only, your browser session may not see it. Install “for all users” if needed.</li>
|
||
<li><b>Supported formats:</b> Browsers use TrueType/OpenType/WOFF. Legacy Type 1 and some bitmap fonts won’t work.</li>
|
||
<li><b>Weight/style availability:</b> If only Bold is installed, Normal text may fall back. Try a bold sample or install Regular as well.</li>
|
||
<li><b>Privacy limits:</b> Browsers don’t allow full font enumeration without permission. This page detects from a large known list and optional Local Fonts API.</li>
|
||
<li><b>Local Fonts API:</b> In Chromium on secure origins (https/localhost), clicking “Try Local Fonts API” can list your exact installed families after permission.</li>
|
||
</ul>
|
||
|
||
<h2>Manual usage tips</h2>
|
||
<ul>
|
||
<li><b>Force a font:</b> In your CSS: <code>font-family: "Your Family", Avenir Next, Sora, Roboto, Helvetica, Arial, sans-serif;</code></li>
|
||
<li><b>Test an arbitrary name:</b> Use the search box to try any family; if found, it appears in results with a sample.</li>
|
||
<li><b>Encoding:</b> Build links like: <code>?font=<span id="egFont">Segoe%20UI</span></code> (generated links below use proper encoding).</li>
|
||
<li><b>CJK and script fonts:</b> The sample includes Latin, numerals, and symbols. Replace with script‑specific text to verify shaping.</li>
|
||
</ul>
|
||
<p class="note">Note: This page does not install or download fonts; it only detects and previews families already available to your browser.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Respect the provided code for applying a font from URL params
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
if (urlParams.get("font")) {
|
||
const rawFont = urlParams.get("font").trim();
|
||
// Strip any surrounding quotes from the param and quote the family in CSS
|
||
const cleanFont = rawFont.replace(/^['"]|['"]$/g, "");
|
||
document.documentElement.style.setProperty(
|
||
"--font-family",
|
||
`"${cleanFont}", Sora, Roboto, Helvetica, Geneva, Verdana, Arial, sans-serif`
|
||
);
|
||
}
|
||
|
||
const resultsEl = document.getElementById('results');
|
||
const countEl = document.getElementById('count');
|
||
const filterEl = document.getElementById('filter');
|
||
const facesToggleEl = document.getElementById('facesToggle');
|
||
|
||
// A broad list of common fonts across Windows/macOS/Linux + popular developer/Google fonts.
|
||
// Detection works by comparing text metrics against generic base families.
|
||
const CANDIDATE_FONTS = Array.from(new Set([
|
||
// Windows core UI/text
|
||
'Segoe UI','Segoe UI Variable','Segoe UI Emoji','Segoe UI Historic','Segoe UI Symbol','Bahnschrift','Ebrima','Gadugi','Javanese Text','Leelawadee UI','Lucida Sans Unicode','Malgun Gothic','Meiryo','Microsoft Himalaya','Microsoft JhengHei','Microsoft New Tai Lue','Microsoft PhagsPa','Microsoft Sans Serif','Microsoft Tai Le','Microsoft Uighur','Microsoft YaHei','Microsoft Yi Baiti','MingLiU-ExtB','Mongolian Baiti','MS Gothic','MS PGothic','MS UI Gothic','NSimSun','PMingLiU-ExtB','SimSun','SimSun-ExtB','Yu Gothic','Yu Gothic UI',
|
||
// Windows Latin staples
|
||
'Arial','Arial Black','Calibri','Cambria','Candara','Comic Sans MS','Consolas','Constantia','Corbel','Courier New','Franklin Gothic Medium','Gabriola','Georgia','Impact','Palatino Linotype','Tahoma','Times New Roman','Trebuchet MS','Verdana','Symbol','Webdings','Wingdings','Wingdings 2','Wingdings 3','Sitka Banner','Sitka Display','Sitka Heading','Sitka Small','Sitka Subheading','Sitka Text','Lucida Console',
|
||
// Developer favorites / code
|
||
'Cascadia Code','Cascadia Mono','Fira Code','Fira Mono','JetBrains Mono','Source Code Pro','IBM Plex Mono','Ubuntu Mono','Inconsolata','Monaspace Neon','Monaspace Argon',
|
||
// Popular sans/serif families
|
||
'Inter','Roboto','Open Sans','Noto Sans','Noto Serif','Noto Sans JP','Noto Sans KR','Noto Sans SC','Noto Serif JP','Noto Naskh Arabic','Lato','Montserrat','Poppins','Oswald','Raleway','Nunito','Merriweather','Playfair Display','PT Sans','PT Serif','Source Sans 3','Source Serif 4','Source Sans Pro','Source Serif Pro','IBM Plex Sans','IBM Plex Serif','Ubuntu','Work Sans','Sora','Avenir','Avenir Next','SF Pro Text','SF Pro Display','Helvetica Neue','Helvetica','Gill Sans',
|
||
// Other common
|
||
'Book Antiqua','Century Gothic','Garamond','Didot','Bodoni MT','Perpetua','Rockwell','Goudy Old Style','Copperplate','Brush Script MT','Candara',
|
||
].sort((a,b)=>a.localeCompare(b))))
|
||
|
||
// Base families used for measuring vs installed font.
|
||
const BASES = ['serif','sans-serif','monospace'];
|
||
|
||
// Measurement string chosen to amplify dimension differences
|
||
const TEST_STRING = 'mmmmmmmmmmlliWWQy #@ 0123456789 The quick brown fox';
|
||
const TEST_SIZE = '72px';
|
||
|
||
function createMeasureSpan(base) {
|
||
const s = document.createElement('span');
|
||
s.textContent = TEST_STRING;
|
||
s.style.position = 'absolute';
|
||
s.style.left = '-9999px';
|
||
s.style.top = '-9999px';
|
||
s.style.fontSize = TEST_SIZE;
|
||
s.style.fontVariantLigatures = 'none';
|
||
s.style.whiteSpace = 'nowrap';
|
||
s.style.fontFamily = base;
|
||
document.body.appendChild(s);
|
||
return s;
|
||
}
|
||
|
||
function metricsFor(base) {
|
||
const el = createMeasureSpan(base);
|
||
const w = el.offsetWidth;
|
||
const h = el.offsetHeight;
|
||
el.remove();
|
||
return { w, h };
|
||
}
|
||
|
||
// Detect if a font is available by comparing width/height against all base families.
|
||
function isFontAvailable(fontName) {
|
||
try {
|
||
const baseline = BASES.map(b => metricsFor(b));
|
||
// If any base differs once we prepend the font, it's likely installed
|
||
return BASES.some((b, i) => {
|
||
const el = createMeasureSpan(`"${fontName}", ${b}`);
|
||
const different = el.offsetWidth !== baseline[i].w || el.offsetHeight !== baseline[i].h;
|
||
el.remove();
|
||
return different;
|
||
});
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function applyFilter(filter, items) {
|
||
if (!filter) return items;
|
||
const q = filter.trim().toLowerCase();
|
||
if (!q) return items;
|
||
return items.filter(item => {
|
||
if (typeof item === 'string') return item.toLowerCase().includes(q);
|
||
const hay = [item.family, item.fullName, item.style, item.weight, item.postscriptName].filter(Boolean).join(' ').toLowerCase();
|
||
return hay.includes(q);
|
||
});
|
||
}
|
||
|
||
function makeRow(item) {
|
||
const isFace = typeof item === 'object' && item && 'family' in item;
|
||
const name = isFace ? item.family : item;
|
||
const row = document.createElement('div');
|
||
row.className = 'row';
|
||
|
||
const sample = document.createElement('div');
|
||
sample.className = 'sample';
|
||
sample.style.fontFamily = `"${name}", Avenir Next, Sora, Roboto, Helvetica, Geneva, Verdana, Arial, sans-serif`;
|
||
if (isFace) {
|
||
if (item.weight) sample.style.fontWeight = String(item.weight);
|
||
if (item.style) sample.style.fontStyle = item.style;
|
||
if (item.stretch) sample.style.fontStretch = item.stretch;
|
||
}
|
||
const faceSuffix = isFace ? ` (${[item.style||'normal', item.weight||'400'].filter(Boolean).join(', ')})` : '';
|
||
sample.textContent = `${name}${faceSuffix} — The quick brown fox jumps over the lazy dog 0123456789`;
|
||
row.appendChild(sample);
|
||
|
||
const meta = document.createElement('div');
|
||
meta.className = 'meta';
|
||
|
||
const fontname = document.createElement('div');
|
||
fontname.className = 'fontname';
|
||
fontname.textContent = name + (isFace && item.fullName ? ` — ${item.fullName}` : '');
|
||
|
||
const link = document.createElement('div');
|
||
link.className = 'link';
|
||
const enc = encodeURIComponent(name);
|
||
const param = `?font=${enc}`;
|
||
const url = `${location.origin}${location.pathname}${param}`;
|
||
link.innerHTML = `<span class="badge">Param</span> <a href="${param}">${param}</a>`;
|
||
|
||
const copyBtn = document.createElement('button');
|
||
copyBtn.className = 'button';
|
||
copyBtn.textContent = 'Copy param';
|
||
copyBtn.addEventListener('click', async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(param);
|
||
copyBtn.textContent = 'Copied!';
|
||
setTimeout(() => (copyBtn.textContent = 'Copy param'), 1200);
|
||
} catch {
|
||
window.prompt('Copy param:', param);
|
||
}
|
||
});
|
||
|
||
meta.appendChild(fontname);
|
||
meta.appendChild(link);
|
||
meta.appendChild(copyBtn);
|
||
row.appendChild(meta);
|
||
return row;
|
||
}
|
||
|
||
function render(list) {
|
||
resultsEl.innerHTML = '';
|
||
const frag = document.createDocumentFragment();
|
||
list.forEach(item => frag.appendChild(makeRow(item)));
|
||
resultsEl.appendChild(frag);
|
||
countEl.textContent = `${list.length} item(s)`;
|
||
}
|
||
|
||
async function detectFromCandidates(candidates) {
|
||
const seen = new Set();
|
||
const found = [];
|
||
for (const name of candidates) {
|
||
if (seen.has(name)) continue;
|
||
seen.add(name);
|
||
if (isFontAvailable(name)) found.push(name);
|
||
}
|
||
found.sort((a,b)=>a.localeCompare(b));
|
||
return found;
|
||
}
|
||
|
||
// Optional: Use Local Fonts API if supported + permitted
|
||
async function tryLocalFontsAPI() {
|
||
if (!('queryLocalFonts' in window)) {
|
||
alert('Local Fonts API not supported in this browser. Use recent Chromium on HTTPS/localhost.');
|
||
return [];
|
||
}
|
||
try {
|
||
const families = new Set();
|
||
const fonts = await window.queryLocalFonts();
|
||
// Keep faces for optional detailed listing
|
||
window.__FACES__ = fonts.map(f => ({
|
||
family: f.family,
|
||
fullName: f.fullName,
|
||
postscriptName: f.postscriptName,
|
||
style: f.style,
|
||
weight: f.weight,
|
||
stretch: f.stretch,
|
||
}));
|
||
for (const f of fonts) {
|
||
if (f.family) families.add(f.family);
|
||
}
|
||
return Array.from(families).sort((a,b)=>a.localeCompare(b));
|
||
} catch (err) {
|
||
console.warn('Local Fonts API error:', err);
|
||
alert('Could not access local fonts. The browser may require HTTPS/localhost and user approval.');
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function getLocalFontsIfGranted() {
|
||
if (!('queryLocalFonts' in window) || !('permissions' in navigator)) return [];
|
||
try {
|
||
const status = await navigator.permissions.query({ name: 'local-fonts' });
|
||
if (status.state === 'granted') {
|
||
const families = await tryLocalFontsAPI();
|
||
return families;
|
||
}
|
||
} catch (e) {
|
||
// Ignore if permissions API doesn't recognize 'local-fonts'
|
||
}
|
||
return [];
|
||
}
|
||
|
||
// Initial detection
|
||
(async function init() {
|
||
// If a font is set via URL param, reflect it in example in the “Manual usage tips”
|
||
const eg = document.getElementById('egFont');
|
||
if (eg) eg.textContent = encodeURIComponent(urlParams.get('font') || 'Segoe UI');
|
||
|
||
countEl.textContent = 'Detecting…';
|
||
const detected = await detectFromCandidates(CANDIDATE_FONTS);
|
||
window.__ALL_DETECTED__ = detected; // families
|
||
|
||
// If permission already granted, auto-merge Local Fonts API
|
||
const grantedFamilies = await getLocalFontsIfGranted();
|
||
if (grantedFamilies.length) {
|
||
const merged = Array.from(new Set([...(window.__ALL_DETECTED__||[]), ...grantedFamilies]));
|
||
merged.sort((a,b)=>a.localeCompare(b));
|
||
window.__ALL_DETECTED__ = merged;
|
||
}
|
||
|
||
const initialList = (facesToggleEl && facesToggleEl.checked && window.__FACES__) ? window.__FACES__ : window.__ALL_DETECTED__;
|
||
render(initialList);
|
||
|
||
// Filter UI
|
||
filterEl.addEventListener('input', () => {
|
||
const facesOn = facesToggleEl && facesToggleEl.checked && window.__FACES__;
|
||
const base = facesOn ? window.__FACES__ : (window.__ALL_DETECTED__ || []);
|
||
const list = applyFilter(filterEl.value, base);
|
||
render(list);
|
||
});
|
||
|
||
// Allow trying arbitrary names by typing and pressing Enter
|
||
filterEl.addEventListener('keydown', async (e) => {
|
||
if (e.key === 'Enter') {
|
||
const name = filterEl.value.trim();
|
||
if (!name) return;
|
||
if (!window.__ALL_DETECTED__.includes(name) && isFontAvailable(name)) {
|
||
window.__ALL_DETECTED__ = [...window.__ALL_DETECTED__, name].sort((a,b)=>a.localeCompare(b));
|
||
}
|
||
const facesOn = facesToggleEl && facesToggleEl.checked && window.__FACES__;
|
||
const base = facesOn ? window.__FACES__ : window.__ALL_DETECTED__;
|
||
const list = applyFilter(filterEl.value, base);
|
||
render(list);
|
||
}
|
||
});
|
||
|
||
// Local Fonts API button
|
||
document.getElementById('tryLocalFonts').addEventListener('click', async () => {
|
||
const families = await tryLocalFontsAPI();
|
||
if (families.length) {
|
||
// Merge Local Fonts API results with detected list
|
||
const merged = Array.from(new Set([...(window.__ALL_DETECTED__||[]), ...families]));
|
||
merged.sort((a,b)=>a.localeCompare(b));
|
||
window.__ALL_DETECTED__ = merged;
|
||
const facesOn = facesToggleEl && facesToggleEl.checked && window.__FACES__;
|
||
const base = facesOn ? window.__FACES__ : merged;
|
||
const list = applyFilter(filterEl.value, base);
|
||
render(list);
|
||
}
|
||
});
|
||
|
||
// Faces toggle
|
||
if (facesToggleEl) {
|
||
facesToggleEl.addEventListener('change', () => {
|
||
const facesOn = facesToggleEl.checked && window.__FACES__;
|
||
const base = facesOn ? window.__FACES__ : (window.__ALL_DETECTED__ || []);
|
||
const list = applyFilter(filterEl.value, base);
|
||
render(list);
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|