(() => {
"use strict";
const state = {
sessionId: null,
fontInfo: null,
chars: {}, // char -> { status, label, codepoint, recipe, originalRecipe, warnings, include, open }
};
const STATUS_LABEL = {
present: "Zaten Mevcut",
existing_unmapped: "Fontta Var (Eşlenecek)",
composable: "Otomatik Oluşturulacak",
composable_synthetic: "Oluşturulacak (Sentetik Aksan)",
needs_attention: "Dikkat Gerekiyor",
};
const STATUS_DOT_COLOR = {
present: "#2f7d4f",
existing_unmapped: "#3a5ba0",
composable: "#3a5ba0",
composable_synthetic: "#a5680c",
needs_attention: "#b23a3a",
};
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
function debounce(fn, ms) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
}
// ---------------------------------------------------------------- upload
const dropzone = $("#dropzone");
const fileInput = $("#fileInput");
const browseBtn = $("#browseBtn");
const uploadError = $("#uploadError");
browseBtn.addEventListener("click", () => fileInput.click());
dropzone.addEventListener("click", (e) => {
if (e.target === browseBtn) return;
fileInput.click();
});
fileInput.addEventListener("change", () => {
if (fileInput.files[0]) handleFile(fileInput.files[0]);
});
["dragenter", "dragover"].forEach((evt) =>
dropzone.addEventListener(evt, (e) => {
e.preventDefault();
dropzone.classList.add("dragover");
})
);
["dragleave", "drop"].forEach((evt) =>
dropzone.addEventListener(evt, (e) => {
e.preventDefault();
dropzone.classList.remove("dragover");
})
);
dropzone.addEventListener("drop", (e) => {
const f = e.dataTransfer.files[0];
if (f) handleFile(f);
});
$("#newFontBtn").addEventListener("click", () => {
if (state.sessionId) {
fetch(`/api/session/${state.sessionId}`, { method: "DELETE" }).catch(() => {});
}
state.sessionId = null;
state.chars = {};
$("#workspace").classList.add("hidden");
$("#uploadSection").classList.remove("hidden");
$("#newFontBtn").classList.add("hidden");
$("#fontInfoBadge").classList.add("hidden");
fileInput.value = "";
});
async function handleFile(file) {
uploadError.classList.add("hidden");
if (!/\.(ttf|otf)$/i.test(file.name)) {
showUploadError("Lütfen .ttf ya da .otf uzantılı bir dosya seçin.");
return;
}
const fd = new FormData();
fd.append("font", file);
dropzone.classList.add("dragover");
dropzone.querySelector(".dropzone-title").textContent = "Yükleniyor...";
try {
const res = await fetch("/api/upload", { method: "POST", body: fd });
const data = await res.json();
if (!res.ok) throw new Error(data.detail || "Yükleme başarısız.");
onUploaded(data);
} catch (err) {
showUploadError(err.message);
} finally {
dropzone.classList.remove("dragover");
dropzone.querySelector(".dropzone-title").textContent = "TTF veya OTF font dosyanızı buraya sürükleyin";
}
}
function showUploadError(msg) {
uploadError.textContent = msg;
uploadError.classList.remove("hidden");
}
function onUploaded(data) {
state.sessionId = data.session_id;
state.fontInfo = data.font_info;
state.chars = {};
for (const c of data.chars) {
state.chars[c.char] = {
status: c.status,
label: c.label,
codepoint: c.codepoint,
recipe: c.recipe,
originalRecipe: JSON.parse(JSON.stringify(c.recipe)),
warnings: c.warnings,
include: c.status !== "needs_attention" || !!c.recipe.base_glyph,
open: false,
};
}
$("#uploadSection").classList.add("hidden");
$("#workspace").classList.remove("hidden");
$("#newFontBtn").classList.remove("hidden");
$("#subsetSuccessBanner").classList.add("hidden");
const badge = $("#fontInfoBadge");
badge.textContent = `${data.font_info.family_name} · ${data.font_info.format}`;
badge.classList.remove("hidden");
renderFontWarningBanner();
renderSidebarInfo();
renderCharList();
prefetchAllPreviews();
}
function renderFontWarningBanner() {
const info = state.fontInfo;
const banner = $("#fontWarningBanner");
if (!info.has_basic_latin) {
banner.innerHTML =
"Bu font temel Latin harfleri içermiyor" +
"Muhtemelen bir ikon/sembol fontu (ör. Font Awesome). Türkçe karakterler mevcut harf " +
"tasarımlarından türetildiği için, hiç harf içermeyen bir fontta otomatik oluşturma yapılamaz " +
"— aşağıdaki karakterler bu yüzden \"Dikkat Gerekiyor\" olarak işaretli.";
banner.classList.remove("hidden");
} else if (info.glyphs_remaining < info.chars_needing_new_glyphs) {
const needed = info.chars_needing_new_glyphs;
const remaining = info.glyphs_remaining;
let msg;
if (remaining === 0) {
msg =
"Font glyph kapasitesi dolu" +
`Bu font zaten OpenType formatının izin verdiği azami 65.535 glyph'in tamamını kullanıyor ` +
`(${info.num_glyphs.toLocaleString("tr-TR")}/65.535), hiç boş yer yok. Yeni çizim gerektiren ` +
`${needed} karakterin hiçbiri eklenemeyecek (fontta zaten bulunanlar bundan etkilenmez).`;
} else {
const missing = needed - remaining;
msg =
"Font glyph kapasitesi yetersiz" +
`Yeni glyph için yalnızca ${remaining} yer kaldı, ama ${needed} karakter yeni çizim gerektiriyor ` +
`— ${missing} tanesi sığmayacak.`;
}
banner.innerHTML = msg + renderSubsetToolHtml();
banner.classList.remove("hidden");
wireSubsetTool(banner);
} else {
banner.classList.add("hidden");
}
}
function renderSubsetToolHtml() {
return `
Yer açmak için fontu subset edebilirsiniz: kullanılmayan glyph'leri kaldırır,
Türkçe kompozisyon için gereken temel Latin harfler/aksanlar her zaman korunur.