import { pins, sites } from "./data.js" import { getDesignThemeLabel, getInitialLanguage, LANGUAGE_KEY, LANGUAGE_NAMES, SUPPORTED_LANGUAGES, t, } from "./i18n.js" import { renderSites } from "./render.js" import { getCurrentDesignTheme, getInitialDesignTheme, getInitialTheme, setDesignTheme, setDesignThemeMenuOpen, setSelectedDesignThemeUI, setTheme, toggleTheme, updateDesignThemeOptions, } from "./theme.js" export function initApp() { const CAT_ICON = "/icons/noto--cat-face.svg" const themeToggle = document.getElementById("themeToggle") const designThemeButton = document.getElementById("designThemeButton") const designThemeList = document.getElementById("designThemeList") const languageButton = document.getElementById("languageButton") const languageList = document.getElementById("languageList") const titleEl = document.querySelector(".title") const subtitleEl = document.querySelector(".subtitle") const designThemeLabelEl = document.querySelector( '[data-i18n="designThemeLabel"]', ) const languageLabelEl = document.querySelector('[data-i18n="languageLabel"]') const moonIcon = document.querySelector(".theme-icon-moon") const sunIcon = document.querySelector(".theme-icon-sun") const sitesContainer = document.querySelector("#sites") const faviconEl = document.querySelector('link[rel~="icon"]') const beianIcpEl = document.querySelector('[data-i18n="beianIcp"]') const beianMpsEl = document.querySelector('[data-i18n="beianMps"]') let currentLanguage = getInitialLanguage() const getThemeLabel = (designTheme, language = currentLanguage) => getDesignThemeLabel(designTheme, language) function setSelectedLanguageUI(language) { if (!languageList) return const options = [...languageList.querySelectorAll('[role="option"]')] options.forEach((el) => { const value = el.getAttribute("data-value") const label = LANGUAGE_NAMES[value] || value || "" el.setAttribute("aria-selected", value === language ? "true" : "false") el.textContent = label }) if (languageButton) { languageButton.textContent = LANGUAGE_NAMES[language] || language } } function setLanguageMenuOpen(open) { if (!languageButton || !languageList) return languageButton.setAttribute("aria-expanded", open ? "true" : "false") languageList.hidden = !open if (open) { languageList.focus() } } function updateSubtitle(language = currentLanguage) { if (!subtitleEl) return if (pins.length) { subtitleEl.textContent = t("pinnedSubtitle", language) } else { subtitleEl.textContent = subtitleEl.dataset.text || "" } } function getDocumentLang(language) { if (language === "zh-Hant") return "zh-Hant" if (language === "zh-Hans") return "zh-Hans" if (language === "ja") return "ja" if (language === "ko") return "ko" return "en" } function setSwappableIcon(element, language) { if (!element) return if (!element.dataset.defaultSrc) { element.dataset.defaultSrc = element.getAttribute("src") || "" } element.setAttribute( "src", language === "meow" ? CAT_ICON : element.dataset.defaultSrc, ) } function setFavicon(language) { if (!faviconEl) return if (!faviconEl.dataset.defaultHref) { faviconEl.dataset.defaultHref = faviconEl.getAttribute("href") || "" } faviconEl.setAttribute( "href", language === "meow" ? CAT_ICON : faviconEl.dataset.defaultHref, ) } function applyTranslations() { const language = currentLanguage document.documentElement.setAttribute("lang", getDocumentLang(language)) document.title = t("appTitle", language) if (titleEl) { const titleText = t("appTitle", language) titleEl.textContent = titleText titleEl.dataset.text = titleText } if (designThemeLabelEl) { designThemeLabelEl.textContent = t("designThemeLabel", language) } if (languageLabelEl) { languageLabelEl.textContent = t("languageLabel", language) } if (beianIcpEl) { beianIcpEl.textContent = t("beianIcp", language) } if (beianMpsEl) { beianMpsEl.textContent = t("beianMps", language) } if (designThemeButton) { designThemeButton.setAttribute( "aria-label", t("designThemeLabel", language), ) } if (languageButton) { languageButton.setAttribute("aria-label", t("languageLabel", language)) } if (themeToggle) { themeToggle.setAttribute("aria-label", t("themeToggleLabel", language)) themeToggle.setAttribute("title", t("themeToggleTitle", language)) } if (moonIcon) { moonIcon.setAttribute("alt", t("moonAlt", language)) } if (sunIcon) { sunIcon.setAttribute("alt", t("sunAlt", language)) } setSwappableIcon(moonIcon, language) setSwappableIcon(sunIcon, language) setFavicon(language) updateDesignThemeOptions({ designThemeList, getLabel: getThemeLabel, language, }) setSelectedDesignThemeUI({ designThemeList, designThemeButton, designTheme: getCurrentDesignTheme(), getLabel: getThemeLabel, }) setSelectedLanguageUI(language) updateSubtitle(language) renderSites({ container: sitesContainer, sites, pins, language }) } function setLanguage(language) { const safeLanguage = SUPPORTED_LANGUAGES.includes(language) ? language : "zh-Hans" currentLanguage = safeLanguage localStorage.setItem(LANGUAGE_KEY, safeLanguage) applyTranslations() } if (titleEl && !titleEl.dataset.text) { titleEl.dataset.text = titleEl.textContent?.trim() || "" } if (subtitleEl && !subtitleEl.dataset.text) { subtitleEl.dataset.text = subtitleEl.textContent?.trim() || "" } const initialTheme = getInitialTheme() setTheme(initialTheme) const initialDesignTheme = getInitialDesignTheme() setDesignTheme(initialDesignTheme, themeToggle) setSelectedDesignThemeUI({ designThemeList, designThemeButton, designTheme: initialDesignTheme, getLabel: getThemeLabel, }) setDesignThemeMenuOpen({ designThemeButton, designThemeList, open: false, }) setLanguage(currentLanguage) if (designThemeButton && designThemeList) { designThemeButton.addEventListener("click", () => { const isOpen = designThemeButton.getAttribute("aria-expanded") === "true" setDesignThemeMenuOpen({ designThemeButton, designThemeList, open: !isOpen, }) }) designThemeList.addEventListener("click", (e) => { const option = e.target.closest?.('[role="option"][data-value]') if (!option) return const value = option.getAttribute("data-value") setDesignTheme(value, themeToggle) setSelectedDesignThemeUI({ designThemeList, designThemeButton, designTheme: value, getLabel: getThemeLabel, }) setDesignThemeMenuOpen({ designThemeButton, designThemeList, open: false, }) }) document.addEventListener("click", (e) => { if (!designThemeButton || !designThemeList) return const clickedInside = designThemeButton.contains(e.target) || designThemeList.contains(e.target) if (!clickedInside) { setDesignThemeMenuOpen({ designThemeButton, designThemeList, open: false, }) } }) document.addEventListener("keydown", (e) => { const isOpen = designThemeButton.getAttribute("aria-expanded") === "true" if (!isOpen) return if (e.key === "Escape") { e.preventDefault() setDesignThemeMenuOpen({ designThemeButton, designThemeList, open: false, }) designThemeButton.focus() return } const options = [...designThemeList.querySelectorAll('[role="option"]')] if (!options.length) return const current = getCurrentDesignTheme() const currentIndex = Math.max( 0, options.findIndex((el) => el.getAttribute("data-value") === current), ) if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault() const delta = e.key === "ArrowDown" ? 1 : -1 const nextIndex = (currentIndex + delta + options.length) % options.length const nextValue = options[nextIndex].getAttribute("data-value") setDesignTheme(nextValue, themeToggle) setSelectedDesignThemeUI({ designThemeList, designThemeButton, designTheme: nextValue, getLabel: getThemeLabel, }) return } if (e.key === "Enter" || e.key === " ") { e.preventDefault() setDesignThemeMenuOpen({ designThemeButton, designThemeList, open: false, }) designThemeButton.focus() } }) } if (languageButton && languageList) { languageButton.addEventListener("click", () => { const isOpen = languageButton.getAttribute("aria-expanded") === "true" setLanguageMenuOpen(!isOpen) }) languageList.addEventListener("click", (e) => { const option = e.target.closest?.('[role="option"][data-value]') if (!option) return const value = option.getAttribute("data-value") setLanguage(value) setLanguageMenuOpen(false) }) document.addEventListener("click", (e) => { if (!languageButton || !languageList) return const clickedInside = languageButton.contains(e.target) || languageList.contains(e.target) if (!clickedInside) setLanguageMenuOpen(false) }) document.addEventListener("keydown", (e) => { const isOpen = languageButton.getAttribute("aria-expanded") === "true" if (!isOpen) return if (e.key === "Escape") { e.preventDefault() setLanguageMenuOpen(false) languageButton.focus() return } const options = [...languageList.querySelectorAll('[role="option"]')] if (!options.length) return const currentIndex = Math.max( 0, options.findIndex( (el) => el.getAttribute("data-value") === currentLanguage, ), ) if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault() const delta = e.key === "ArrowDown" ? 1 : -1 const nextIndex = (currentIndex + delta + options.length) % options.length const nextValue = options[nextIndex].getAttribute("data-value") setLanguage(nextValue) return } if (e.key === "Enter" || e.key === " ") { e.preventDefault() setLanguageMenuOpen(false) languageButton.focus() } }) } window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", (e) => { if (!localStorage.getItem("theme")) { setTheme(e.matches ? "dark" : "light") } }) if (themeToggle) { themeToggle.addEventListener("click", toggleTheme) } }