Files
home/app.js
yuetsh ed880fd57d
Some checks failed
Deploy / deploy (build, debian, 22) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822) (push) Has been cancelled
add i18n
2026-01-15 11:13:41 +08:00

368 lines
11 KiB
JavaScript

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)
}
}