Compare commits
6 Commits
137f3e7988
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ed880fd57d | |||
| e5c6db95b7 | |||
| c1e5b48038 | |||
| 36c32feb01 | |||
| d13f895965 | |||
| bbce4c8eec |
@@ -5,3 +5,4 @@ VITE_BOOK=https://book.xuyue.cc
|
||||
VITE_HUABU=https://huabu.xuyue.cc
|
||||
VITE_PPT=https://ppt.xuyue.cc/py
|
||||
VITE_PY=https://python.xuyue.cc
|
||||
VITE_BLOCKLY=https://lego.xuyue.cc
|
||||
@@ -4,3 +4,4 @@ VITE_WEB=http://10.13.114.114:91
|
||||
VITE_BOOK=http://10.13.114.114:84
|
||||
VITE_HUABU=http://10.13.114.114:85
|
||||
VITE_SHUATI=http://10.13.114.114:86
|
||||
VITE_BLOCKLY=http://10.13.114.114:98
|
||||
23
.github/workflows/deploy.yml
vendored
23
.github/workflows/deploy.yml
vendored
@@ -5,23 +5,38 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: debian
|
||||
build_command: build
|
||||
remote_port: 22
|
||||
- name: school
|
||||
build_command: build:staging
|
||||
remote_port: 8822
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: CI=false npm run build
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run ${{ matrix.build_command }}
|
||||
env:
|
||||
CI: false
|
||||
|
||||
- uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.KEY }}
|
||||
REMOTE_HOST: ${{ secrets.HOST }}
|
||||
REMOTE_PORT: ${{ matrix.remote_port }}
|
||||
ARGS: "-avzr --delete"
|
||||
SOURCE: dist/
|
||||
REMOTE_USER: root
|
||||
|
||||
367
app.js
Normal file
367
app.js
Normal file
@@ -0,0 +1,367 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
189
data.js
Normal file
189
data.js
Normal file
@@ -0,0 +1,189 @@
|
||||
export const pins = [
|
||||
// {
|
||||
// url: "https://code.xuyue.cc?query=30",
|
||||
// description: "示例代码",
|
||||
// },
|
||||
]
|
||||
|
||||
export const sites = [
|
||||
{
|
||||
url: import.meta.env.VITE_OJ,
|
||||
title: {
|
||||
"zh-Hans": "判题狗",
|
||||
"zh-Hant": "判題狗",
|
||||
en: "Judge Dog",
|
||||
ja: "判定犬",
|
||||
ko: "판정개",
|
||||
meow: "喵喵喵",
|
||||
},
|
||||
description: {
|
||||
"zh-Hans": "在线判题网站",
|
||||
"zh-Hant": "在線判題網站",
|
||||
en: "Online judge platform",
|
||||
ja: "オンライン判定サイト",
|
||||
ko: "온라인 판정 사이트",
|
||||
meow: "喵喵喵喵喵喵",
|
||||
},
|
||||
icon: "noto--dog-face.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_CODE,
|
||||
title: {
|
||||
"zh-Hans": "自测猫",
|
||||
"zh-Hant": "自測貓",
|
||||
en: "Self Test Cat",
|
||||
ja: "自テスト猫",
|
||||
ko: "자가테스트猫",
|
||||
meow: "喵喵喵",
|
||||
},
|
||||
description: {
|
||||
"zh-Hans": "代码运行网站",
|
||||
"zh-Hant": "代碼運行網站",
|
||||
en: "Code runner",
|
||||
ja: "コード実行サイト",
|
||||
ko: "코드 실행 사이트",
|
||||
meow: "喵喵喵喵喵喵",
|
||||
},
|
||||
icon: "noto--cat-face.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_WEB,
|
||||
title: {
|
||||
"zh-Hans": "哈基米",
|
||||
"zh-Hant": "哈基米",
|
||||
en: "Hakimi",
|
||||
ja: "ハキミ",
|
||||
ko: "하키미",
|
||||
meow: "喵喵喵",
|
||||
},
|
||||
description: {
|
||||
"zh-Hans": "Web 前端开发",
|
||||
"zh-Hant": "Web 前端開發",
|
||||
en: "Web frontend development",
|
||||
ja: "Webフロントエンド開発",
|
||||
ko: "웹 프론트엔드 개발",
|
||||
meow: "喵喵喵喵喵喵喵喵",
|
||||
},
|
||||
icon: "noto--honeybee.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_SHUATI,
|
||||
title: {
|
||||
"zh-Hans": "刷题鸭",
|
||||
"zh-Hant": "刷題鴨",
|
||||
en: "Practice Duck",
|
||||
ja: "演習アヒル",
|
||||
ko: "문제풀이오리",
|
||||
meow: "喵喵喵",
|
||||
},
|
||||
description: {
|
||||
"zh-Hans": "梁老师的刷题网站",
|
||||
"zh-Hant": "梁老師的刷題網站",
|
||||
en: "Practice problems by Mr. Liang",
|
||||
ja: "梁先生の演習サイト",
|
||||
ko: "량 선생님의 문제풀이 사이트",
|
||||
meow: "喵喵喵喵喵喵喵喵",
|
||||
},
|
||||
icon: "noto--paintbrush.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_BOOK,
|
||||
title: {
|
||||
"zh-Hans": "编程书",
|
||||
"zh-Hant": "編程書",
|
||||
en: "Coding Books",
|
||||
ja: "プログラミング書",
|
||||
ko: "프로그래밍 책",
|
||||
meow: "喵喵喵",
|
||||
},
|
||||
description: {
|
||||
"zh-Hans": "编程和计算机相关知识汇总",
|
||||
"zh-Hant": "編程和計算機相關知識匯總",
|
||||
en: "CS knowledge summary",
|
||||
ja: "プログラミング/コンピュータ知識まとめ",
|
||||
ko: "프로그래밍/컴퓨터 지식 모음",
|
||||
meow: "喵喵喵喵喵喵喵喵喵喵喵喵",
|
||||
},
|
||||
icon: "noto--bookmark-tabs.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_BLOCKLY,
|
||||
title: {
|
||||
"zh-Hans": "小方块",
|
||||
"zh-Hant": "小方塊",
|
||||
en: "Little Blocks",
|
||||
ja: "小さなブロック",
|
||||
ko: "작은 블록",
|
||||
meow: "喵喵喵",
|
||||
},
|
||||
description: {
|
||||
"zh-Hans": "搭积木,学编程",
|
||||
"zh-Hant": "搭積木,學編程",
|
||||
en: "Learn coding with blocks",
|
||||
ja: "ブロックでプログラミング",
|
||||
ko: "블록으로 프로그래밍 배우기",
|
||||
meow: "喵喵喵喵喵喵喵",
|
||||
},
|
||||
icon: "twemoji--brick.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_HUABU,
|
||||
title: {
|
||||
"zh-Hans": "白板",
|
||||
"zh-Hant": "白板",
|
||||
en: "Whiteboard",
|
||||
ja: "ホワイトボード",
|
||||
ko: "화이트보드",
|
||||
meow: "喵喵",
|
||||
},
|
||||
description: {
|
||||
"zh-Hans": "在线板书",
|
||||
"zh-Hant": "在線板書",
|
||||
en: "Online whiteboard",
|
||||
ja: "オンライン板書",
|
||||
ko: "온라인 판서",
|
||||
meow: "喵喵喵喵",
|
||||
},
|
||||
icon: "noto--artist-palette.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_PPT,
|
||||
title: {
|
||||
"zh-Hans": "Python PPT",
|
||||
"zh-Hant": "Python PPT",
|
||||
en: "Python PPT",
|
||||
ja: "Python PPT",
|
||||
ko: "Python PPT",
|
||||
meow: "喵喵喵喵喵喵",
|
||||
},
|
||||
description: {
|
||||
"zh-Hans": "Python 第一学期上课用",
|
||||
"zh-Hant": "Python 第一學期上課用",
|
||||
en: "Python semester 1 materials",
|
||||
ja: "Python 1学期授業用",
|
||||
ko: "Python 1학기 수업용",
|
||||
meow: "喵喵喵喵喵喵喵喵喵喵喵喵喵喵",
|
||||
},
|
||||
icon: "material-icon-theme--python.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_PY,
|
||||
title: {
|
||||
"zh-Hans": "Python 项目",
|
||||
"zh-Hant": "Python 項目",
|
||||
en: "Python Projects",
|
||||
ja: "Python プロジェクト",
|
||||
ko: "Python 프로젝트",
|
||||
meow: "喵喵喵喵喵喵喵喵",
|
||||
},
|
||||
description: {
|
||||
"zh-Hans": "Python 第二学期上课用",
|
||||
"zh-Hant": "Python 第二學期上課用",
|
||||
en: "Python semester 2 materials",
|
||||
ja: "Python 2学期授業用",
|
||||
ko: "Python 2학기 수업용",
|
||||
meow: "喵喵喵喵喵喵喵",
|
||||
},
|
||||
icon: "material-icon-theme--folder-python-open.svg",
|
||||
},
|
||||
].filter((site) => !!site.url)
|
||||
158
i18n.js
Normal file
158
i18n.js
Normal file
@@ -0,0 +1,158 @@
|
||||
export const I18N = {
|
||||
"zh-Hans": {
|
||||
appTitle: "物联网专业在线学习平台",
|
||||
pinnedSubtitle: "置顶内容",
|
||||
designThemeLabel: "设计主题",
|
||||
themeToggleLabel: "切换主题",
|
||||
themeToggleTitle: "切换深色/浅色模式",
|
||||
moonAlt: "月亮",
|
||||
sunAlt: "太阳",
|
||||
languageLabel: "语言",
|
||||
beianIcp: "浙ICP备2023044109号",
|
||||
beianMps: "浙公网安备33100402331786号",
|
||||
},
|
||||
"zh-Hant": {
|
||||
appTitle: "物聯網專業在線學習平台",
|
||||
pinnedSubtitle: "置頂內容",
|
||||
designThemeLabel: "設計主題",
|
||||
themeToggleLabel: "切換主題",
|
||||
themeToggleTitle: "切換深色/淺色模式",
|
||||
moonAlt: "月亮",
|
||||
sunAlt: "太陽",
|
||||
languageLabel: "語言",
|
||||
beianIcp: "浙ICP備2023044109號",
|
||||
beianMps: "浙公網安備33100402331786號",
|
||||
},
|
||||
en: {
|
||||
appTitle: "IoT Program Online Learning Hub",
|
||||
pinnedSubtitle: "Pinned",
|
||||
designThemeLabel: "Design theme",
|
||||
themeToggleLabel: "Toggle theme",
|
||||
themeToggleTitle: "Toggle dark/light mode",
|
||||
moonAlt: "Moon",
|
||||
sunAlt: "Sun",
|
||||
languageLabel: "Language",
|
||||
beianIcp: "Zhejiang ICP 2023044109",
|
||||
beianMps: "Zhejiang Public Security 33100402331786",
|
||||
},
|
||||
ja: {
|
||||
appTitle: "IoT専攻オンライン学習プラットフォーム",
|
||||
pinnedSubtitle: "ピン留め",
|
||||
designThemeLabel: "デザインテーマ",
|
||||
themeToggleLabel: "テーマ切替",
|
||||
themeToggleTitle: "ダーク/ライト切替",
|
||||
moonAlt: "月",
|
||||
sunAlt: "太陽",
|
||||
languageLabel: "言語",
|
||||
beianIcp: "浙江ICP 2023044109",
|
||||
beianMps: "浙江公安 33100402331786",
|
||||
},
|
||||
ko: {
|
||||
appTitle: "IoT 전공 온라인 학습 플랫폼",
|
||||
pinnedSubtitle: "고정",
|
||||
designThemeLabel: "디자인 테마",
|
||||
themeToggleLabel: "테마 전환",
|
||||
themeToggleTitle: "다크/라이트 전환",
|
||||
moonAlt: "달",
|
||||
sunAlt: "태양",
|
||||
languageLabel: "언어",
|
||||
beianIcp: "저장 ICP 2023044109",
|
||||
beianMps: "저장 공안 33100402331786",
|
||||
},
|
||||
meow: {
|
||||
appTitle: "喵喵喵喵喵喵喵喵喵喵喵喵",
|
||||
pinnedSubtitle: "喵喵喵喵",
|
||||
designThemeLabel: "喵喵喵喵",
|
||||
themeToggleLabel: "喵喵喵喵",
|
||||
themeToggleTitle: "喵喵喵喵喵喵喵喵喵",
|
||||
moonAlt: "喵喵",
|
||||
sunAlt: "喵喵",
|
||||
languageLabel: "喵喵",
|
||||
beianIcp: "喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵",
|
||||
beianMps: "喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵",
|
||||
},
|
||||
}
|
||||
|
||||
export const DESIGN_THEME_LABELS = {
|
||||
"zh-Hans": {
|
||||
fluent: "Fluent",
|
||||
"material-you": "Material You",
|
||||
terminal: "终端",
|
||||
cyberpunk: "赛博朋克",
|
||||
},
|
||||
"zh-Hant": {
|
||||
fluent: "Fluent",
|
||||
"material-you": "Material You",
|
||||
terminal: "終端",
|
||||
cyberpunk: "賽博龐克",
|
||||
},
|
||||
en: {
|
||||
fluent: "Fluent",
|
||||
"material-you": "Material You",
|
||||
terminal: "Terminal",
|
||||
cyberpunk: "Cyberpunk",
|
||||
},
|
||||
ja: {
|
||||
fluent: "Fluent",
|
||||
"material-you": "Material You",
|
||||
terminal: "ターミナル",
|
||||
cyberpunk: "サイバーパンク",
|
||||
},
|
||||
ko: {
|
||||
fluent: "Fluent",
|
||||
"material-you": "Material You",
|
||||
terminal: "터미널",
|
||||
cyberpunk: "사이버펑크",
|
||||
},
|
||||
meow: {
|
||||
fluent: "喵喵",
|
||||
"material-you": "喵喵喵",
|
||||
terminal: "喵喵",
|
||||
cyberpunk: "喵喵喵喵",
|
||||
},
|
||||
}
|
||||
|
||||
export const LANGUAGE_NAMES = {
|
||||
"zh-Hans": "简体中文",
|
||||
"zh-Hant": "繁體中文",
|
||||
en: "English",
|
||||
ja: "日本語",
|
||||
ko: "한국어",
|
||||
meow: "喵喵喵",
|
||||
}
|
||||
|
||||
export const LANGUAGE_KEY = "language"
|
||||
export const SUPPORTED_LANGUAGES = Object.keys(LANGUAGE_NAMES)
|
||||
|
||||
export function getLocalizedText(value, language) {
|
||||
if (!value) return ""
|
||||
if (typeof value === "object") {
|
||||
return (
|
||||
value[language] || value["zh-Hans"]
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function getInitialLanguage() {
|
||||
const saved = localStorage.getItem(LANGUAGE_KEY)
|
||||
if (saved && SUPPORTED_LANGUAGES.includes(saved)) return saved
|
||||
const normalized = (navigator.language || "").toLowerCase()
|
||||
if (normalized.startsWith("zh")) {
|
||||
return normalized.includes("hant") || normalized.includes("tw")
|
||||
? "zh-Hant"
|
||||
: "zh-Hans"
|
||||
}
|
||||
if (normalized.startsWith("ja")) return "ja"
|
||||
if (normalized.startsWith("ko")) return "ko"
|
||||
return "zh-Hans"
|
||||
}
|
||||
|
||||
export function t(key, language) {
|
||||
return I18N[language]?.[key] || I18N["zh-Hans"][key] || ""
|
||||
}
|
||||
|
||||
export function getDesignThemeLabel(designTheme, language) {
|
||||
const labels = DESIGN_THEME_LABELS[language] || DESIGN_THEME_LABELS["zh-Hans"]
|
||||
return labels[designTheme] || labels.fluent
|
||||
}
|
||||
42
index.html
42
index.html
@@ -11,7 +11,9 @@
|
||||
<body>
|
||||
<div class="theme-controls">
|
||||
<label class="design-theme">
|
||||
<span class="visually-hidden">设计主题</span>
|
||||
<span class="visually-hidden" data-i18n="designThemeLabel"
|
||||
>设计主题</span
|
||||
>
|
||||
<button
|
||||
class="design-theme-button"
|
||||
id="designThemeButton"
|
||||
@@ -42,6 +44,38 @@
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
<label class="design-theme language-switch">
|
||||
<span class="visually-hidden" data-i18n="languageLabel">语言</span>
|
||||
<button
|
||||
class="design-theme-button"
|
||||
id="languageButton"
|
||||
type="button"
|
||||
aria-label="语言"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="false"
|
||||
aria-controls="languageList"
|
||||
>
|
||||
中文
|
||||
</button>
|
||||
<ul
|
||||
class="design-theme-list"
|
||||
id="languageList"
|
||||
role="listbox"
|
||||
tabindex="-1"
|
||||
hidden
|
||||
>
|
||||
<li role="option" data-value="zh-Hans" aria-selected="true">
|
||||
简体中文
|
||||
</li>
|
||||
<li role="option" data-value="zh-Hant" aria-selected="false">
|
||||
繁體中文
|
||||
</li>
|
||||
<li role="option" data-value="ja" aria-selected="false">日本語</li>
|
||||
<li role="option" data-value="ko" aria-selected="false">한국어</li>
|
||||
<li role="option" data-value="en" aria-selected="false">English</li>
|
||||
<li role="option" data-value="meow" aria-selected="false">喵喵喵</li>
|
||||
</ul>
|
||||
</label>
|
||||
<button
|
||||
class="theme-toggle"
|
||||
id="themeToggle"
|
||||
@@ -63,20 +97,20 @@
|
||||
<div id="app">
|
||||
<div class="container">
|
||||
<main class="main">
|
||||
<h1 class="title">物联网专业の在线学习平台</h1>
|
||||
<h1 class="title" data-i18n="appTitle">物联网专业の在线学习平台</h1>
|
||||
<h2 class="subtitle"></h2>
|
||||
<div class="grid" id="sites"></div>
|
||||
</main>
|
||||
<div class="beian">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank" rel="noreferrer">
|
||||
浙ICP备2023044109号
|
||||
<span data-i18n="beianIcp">浙ICP备2023044109号</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://beian.mps.gov.cn/#/query/webSearch?code=33100402331786"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
浙公网安备33100402331786号
|
||||
<span data-i18n="beianMps">浙公网安备33100402331786号</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
298
main.js
298
main.js
@@ -1,297 +1,3 @@
|
||||
const pins = [
|
||||
// {
|
||||
// url: "https://code.xuyue.cc?query=30",
|
||||
// description: "示例代码",
|
||||
// },
|
||||
]
|
||||
import { initApp } from "./app.js"
|
||||
|
||||
const sites = [
|
||||
{
|
||||
url: import.meta.env.VITE_OJ,
|
||||
title: "判题狗",
|
||||
description: "在线判题网站",
|
||||
icon: "noto--dog-face.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_CODE,
|
||||
title: "自测猫",
|
||||
description: "代码运行网站",
|
||||
icon: "noto--cat-face.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_WEB,
|
||||
title: "哈基米",
|
||||
description: "Web 前端开发",
|
||||
icon: "noto--honeybee.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_SHUATI,
|
||||
title: "刷题鸭",
|
||||
description: "梁老师的刷题网站",
|
||||
icon: "noto--paintbrush.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_BOOK,
|
||||
title: "编程书",
|
||||
description: "编程和计算机相关知识汇总",
|
||||
icon: "noto--bookmark-tabs.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_HUABU,
|
||||
title: "白板",
|
||||
description: "在线板书",
|
||||
icon: "noto--artist-palette.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_PPT,
|
||||
title: "Python PPT",
|
||||
description: "Python 第一学期上课用",
|
||||
icon: "material-icon-theme--python.svg",
|
||||
},
|
||||
{
|
||||
url: import.meta.env.VITE_PY,
|
||||
title: "Python 项目",
|
||||
description: "Python 第二学期上课用",
|
||||
icon: "material-icon-theme--folder-python-open.svg",
|
||||
},
|
||||
].filter((i) => !!i.url)
|
||||
|
||||
const item = (site) => `
|
||||
<a href="${site.url}" target="_blank" class="card">
|
||||
<div class="title-icon">
|
||||
${site.icon ? `<img src="/icons/${site.icon}" alt="${site.title}" class="icon" />` : ""}
|
||||
<h2>${site.title} →</h2>
|
||||
</div>
|
||||
<p>${site.description}</p>
|
||||
<p class="single">${site.url}</p>
|
||||
</a>
|
||||
`
|
||||
|
||||
const pin = (site) => `
|
||||
<a href="${site.url}" target="_blank" class="card pin">
|
||||
<p>${site.description}</p>
|
||||
</a>
|
||||
`
|
||||
|
||||
if (pins.length) {
|
||||
document.querySelector(".subtitle").innerHTML = "置顶内容"
|
||||
}
|
||||
|
||||
document.querySelector("#sites").innerHTML =
|
||||
pins.map(pin).join("") + sites.map(item).join("")
|
||||
|
||||
// 主题切换功能
|
||||
const themeToggle = document.getElementById("themeToggle")
|
||||
const designThemeButton = document.getElementById("designThemeButton")
|
||||
const designThemeList = document.getElementById("designThemeList")
|
||||
|
||||
const DESIGN_THEMES = ["fluent", "material-you", "terminal", "cyberpunk"]
|
||||
const FORCED_DARK_DESIGN_THEMES = new Set(["terminal", "cyberpunk"])
|
||||
const THEME_BEFORE_FORCED_KEY = "themeBeforeForcedDark"
|
||||
|
||||
// 获取保存的主题或系统偏好
|
||||
function getInitialTheme() {
|
||||
const savedTheme = localStorage.getItem("theme")
|
||||
if (savedTheme) {
|
||||
return savedTheme
|
||||
}
|
||||
// 如果没有保存的主题,使用系统偏好
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
}
|
||||
|
||||
// 应用主题
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute("data-theme", theme)
|
||||
localStorage.setItem("theme", theme)
|
||||
// 图标通过 CSS 自动切换显示
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
function toggleTheme() {
|
||||
const designTheme =
|
||||
document.documentElement.getAttribute("data-design-theme") || "fluent"
|
||||
if (FORCED_DARK_DESIGN_THEMES.has(designTheme)) return
|
||||
const currentTheme =
|
||||
document.documentElement.getAttribute("data-theme") || "light"
|
||||
const newTheme = currentTheme === "dark" ? "light" : "dark"
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
function getInitialDesignTheme() {
|
||||
const savedDesignTheme = localStorage.getItem("designTheme")
|
||||
if (savedDesignTheme && DESIGN_THEMES.includes(savedDesignTheme)) {
|
||||
return savedDesignTheme
|
||||
}
|
||||
return "fluent"
|
||||
}
|
||||
|
||||
function setDesignTheme(designTheme) {
|
||||
const safeDesignTheme = DESIGN_THEMES.includes(designTheme)
|
||||
? designTheme
|
||||
: "fluent"
|
||||
const previousDesignTheme =
|
||||
document.documentElement.getAttribute("data-design-theme") || "fluent"
|
||||
document.documentElement.setAttribute("data-design-theme", safeDesignTheme)
|
||||
localStorage.setItem("designTheme", safeDesignTheme)
|
||||
|
||||
const willForceDark = FORCED_DARK_DESIGN_THEMES.has(safeDesignTheme)
|
||||
const didForceDark = FORCED_DARK_DESIGN_THEMES.has(previousDesignTheme)
|
||||
|
||||
if (willForceDark) {
|
||||
if (!didForceDark) {
|
||||
const currentTheme =
|
||||
document.documentElement.getAttribute("data-theme") || "light"
|
||||
localStorage.setItem(THEME_BEFORE_FORCED_KEY, currentTheme)
|
||||
}
|
||||
setTheme("dark")
|
||||
} else if (didForceDark) {
|
||||
const restoreTheme =
|
||||
localStorage.getItem(THEME_BEFORE_FORCED_KEY) ||
|
||||
localStorage.getItem("themeBeforeTerminal")
|
||||
if (restoreTheme === "dark" || restoreTheme === "light") {
|
||||
setTheme(restoreTheme)
|
||||
}
|
||||
localStorage.removeItem(THEME_BEFORE_FORCED_KEY)
|
||||
localStorage.removeItem("themeBeforeTerminal")
|
||||
}
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.disabled = willForceDark
|
||||
themeToggle.setAttribute("aria-disabled", willForceDark ? "true" : "false")
|
||||
themeToggle.tabIndex = willForceDark ? -1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
function getDesignThemeLabel(designTheme) {
|
||||
const optionEl = designThemeList?.querySelector(
|
||||
`[role="option"][data-value="${designTheme}"]`,
|
||||
)
|
||||
if (optionEl) return optionEl.textContent?.trim() || "流光"
|
||||
const fallback = {
|
||||
fluent: "流光",
|
||||
"material-you": "Material You",
|
||||
terminal: "终端",
|
||||
cyberpunk: "Cyberpunk",
|
||||
}
|
||||
return fallback[designTheme] || "流光"
|
||||
}
|
||||
|
||||
function setSelectedDesignThemeUI(designTheme) {
|
||||
if (!designThemeList) return
|
||||
const options = [...designThemeList.querySelectorAll('[role="option"]')]
|
||||
options.forEach((el) => {
|
||||
el.setAttribute(
|
||||
"aria-selected",
|
||||
el.getAttribute("data-value") === designTheme ? "true" : "false",
|
||||
)
|
||||
})
|
||||
if (designThemeButton) {
|
||||
designThemeButton.textContent = getDesignThemeLabel(designTheme)
|
||||
}
|
||||
}
|
||||
|
||||
function setDesignThemeMenuOpen(open) {
|
||||
if (!designThemeButton || !designThemeList) return
|
||||
designThemeButton.setAttribute("aria-expanded", open ? "true" : "false")
|
||||
designThemeList.hidden = !open
|
||||
if (open) {
|
||||
designThemeList.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentDesignTheme() {
|
||||
return document.documentElement.getAttribute("data-design-theme") || "fluent"
|
||||
}
|
||||
|
||||
const titleEl = document.querySelector(".title")
|
||||
if (titleEl && !titleEl.dataset.text) {
|
||||
titleEl.dataset.text = titleEl.textContent?.trim() || ""
|
||||
}
|
||||
|
||||
const subtitleEl = document.querySelector(".subtitle")
|
||||
if (subtitleEl && !subtitleEl.dataset.text) {
|
||||
subtitleEl.dataset.text = subtitleEl.textContent?.trim() || ""
|
||||
}
|
||||
|
||||
// 初始化主题
|
||||
const initialTheme = getInitialTheme()
|
||||
setTheme(initialTheme)
|
||||
|
||||
const initialDesignTheme = getInitialDesignTheme()
|
||||
setDesignTheme(initialDesignTheme)
|
||||
setSelectedDesignThemeUI(initialDesignTheme)
|
||||
setDesignThemeMenuOpen(false)
|
||||
|
||||
if (designThemeButton && designThemeList) {
|
||||
designThemeButton.addEventListener("click", () => {
|
||||
const isOpen = designThemeButton.getAttribute("aria-expanded") === "true"
|
||||
setDesignThemeMenuOpen(!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)
|
||||
setSelectedDesignThemeUI(value)
|
||||
setDesignThemeMenuOpen(false)
|
||||
})
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!designThemeButton || !designThemeList) return
|
||||
const clickedInside =
|
||||
designThemeButton.contains(e.target) || designThemeList.contains(e.target)
|
||||
if (!clickedInside) setDesignThemeMenuOpen(false)
|
||||
})
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const isOpen = designThemeButton.getAttribute("aria-expanded") === "true"
|
||||
if (!isOpen) return
|
||||
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setDesignThemeMenuOpen(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)
|
||||
setSelectedDesignThemeUI(nextValue)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
setDesignThemeMenuOpen(false)
|
||||
designThemeButton.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听系统主题变化(仅在用户未手动设置时)
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", (e) => {
|
||||
// 如果用户没有手动设置过主题,则跟随系统
|
||||
if (!localStorage.getItem("theme")) {
|
||||
setTheme(e.matches ? "dark" : "light")
|
||||
}
|
||||
})
|
||||
|
||||
// 绑定点击事件
|
||||
themeToggle.addEventListener("click", toggleTheme)
|
||||
initApp()
|
||||
|
||||
1
public/icons/twemoji--brick.svg
Normal file
1
public/icons/twemoji--brick.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 36 36"><path fill="#c1694f" d="M35.627 10.826L.373 16.56v9.722h.004c-.012.149.129.295.443.473c.727.412 9.835 5.286 10.553 5.639c.895.439 1.441.493 2.206.112c.626-.312 19.236-10.173 20.932-11.074c.863-.459 1.146-.711 1.099-.919h.018z"/><path fill="#a0041e" d="M.373 16.56v9.722h.004c-.012.149.129.295.443.473c.727.412 9.835 5.286 10.553 5.639c.432.212.781.329 1.114.356V16.56z"/><path fill="#d99e82" d="M22.224 4.682c1.076-.558 1.113-.628 2.249-.024s9.846 5.248 10.487 5.589c.814.434 1.112.591-.449 1.421c-1.696.902-20.306 10.763-20.932 11.074c-.765.381-1.311.327-2.206-.112c-.718-.352-9.826-5.226-10.553-5.639c-.82-.465-.482-.706.769-1.37z"/><ellipse cx="11.189" cy="17.191" fill="#c1694f" rx="3.679" ry="1.84"/><path fill="#a0041e" d="M11.642 16.734c-1.569-.516-3.352-.369-4.108.339c-.005.04-.024.078-.024.118c0 1.016 1.647 1.84 3.679 1.84c1.063 0 2.013-.229 2.684-.589c-.333-.692-1.086-1.332-2.231-1.708"/><ellipse cx="18" cy="13.639" fill="#c1694f" rx="3.679" ry="1.84"/><path fill="#a0041e" d="M18.453 13.182c-1.569-.516-3.352-.369-4.108.339c-.005.04-.024.078-.024.118c0 1.016 1.647 1.84 3.679 1.84c1.063 0 2.013-.229 2.684-.589c-.333-.692-1.086-1.332-2.231-1.708"/><ellipse cx="24.811" cy="10.087" fill="#c1694f" rx="3.679" ry="1.84"/><path fill="#a0041e" d="M25.264 9.63c-1.569-.516-3.352-.369-4.108.339c-.005.04-.024.078-.024.118c0 1.016 1.647 1.84 3.679 1.84c1.063 0 2.012-.229 2.684-.589c-.333-.692-1.086-1.332-2.231-1.708"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
34
render.js
Normal file
34
render.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { getLocalizedText } from "./i18n.js"
|
||||
|
||||
const renderItem = (site, language) => {
|
||||
const title = getLocalizedText(site.title, language)
|
||||
const description = getLocalizedText(site.description, language)
|
||||
const iconName =
|
||||
language === "meow" ? "noto--cat-face.svg" : site.icon
|
||||
return `
|
||||
<a href="${site.url}" target="_blank" class="card">
|
||||
<div class="title-icon">
|
||||
${iconName ? `<img src="/icons/${iconName}" alt="${title}" class="icon" />` : ""}
|
||||
<h2>${title} →</h2>
|
||||
</div>
|
||||
<p>${description}</p>
|
||||
<p class="single">${site.url}</p>
|
||||
</a>
|
||||
`
|
||||
}
|
||||
|
||||
const renderPin = (site, language) => {
|
||||
const description = getLocalizedText(site.description, language)
|
||||
return `
|
||||
<a href="${site.url}" target="_blank" class="card pin">
|
||||
<p>${description}</p>
|
||||
</a>
|
||||
`
|
||||
}
|
||||
|
||||
export function renderSites({ container, sites, pins, language }) {
|
||||
if (!container) return
|
||||
container.innerHTML =
|
||||
pins.map((site) => renderPin(site, language)).join("") +
|
||||
sites.map((site) => renderItem(site, language)).join("")
|
||||
}
|
||||
27
style.css
27
style.css
@@ -217,9 +217,8 @@ a {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.design-theme-button {
|
||||
height: 44px;
|
||||
min-width: 118px;
|
||||
.design-theme-button,
|
||||
.theme-toggle {
|
||||
border: 1px solid var(--control-border);
|
||||
border-radius: 12px;
|
||||
background: var(--control-bg);
|
||||
@@ -229,10 +228,15 @@ a {
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 1px 2px rgba(0, 0, 0, 0.04),
|
||||
inset 0 1px 0 var(--control-inset);
|
||||
padding: 0 34px 0 14px;
|
||||
font: inherit;
|
||||
color: var(--control-fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.design-theme-button {
|
||||
height: 44px;
|
||||
min-width: 118px;
|
||||
padding: 0 34px 0 14px;
|
||||
font: inherit;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
@@ -266,8 +270,6 @@ a {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-width: 160px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
padding: 6px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
@@ -322,17 +324,6 @@ html[data-theme="dark"]:not([data-design-theme="terminal"]) .design-theme-list {
|
||||
.theme-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: var(--control-bg);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid var(--control-border);
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 1px 2px rgba(0, 0, 0, 0.04),
|
||||
inset 0 1px 0 var(--control-inset);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
122
theme.js
Normal file
122
theme.js
Normal file
@@ -0,0 +1,122 @@
|
||||
const DESIGN_THEMES = ["fluent", "material-you", "terminal", "cyberpunk"]
|
||||
const FORCED_DARK_DESIGN_THEMES = new Set(["terminal", "cyberpunk"])
|
||||
const THEME_BEFORE_FORCED_KEY = "themeBeforeForcedDark"
|
||||
|
||||
export function getInitialTheme() {
|
||||
const savedTheme = localStorage.getItem("theme")
|
||||
if (savedTheme) {
|
||||
return savedTheme
|
||||
}
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
}
|
||||
|
||||
export function setTheme(theme) {
|
||||
document.documentElement.setAttribute("data-theme", theme)
|
||||
localStorage.setItem("theme", theme)
|
||||
}
|
||||
|
||||
export function toggleTheme() {
|
||||
const designTheme =
|
||||
document.documentElement.getAttribute("data-design-theme") || "fluent"
|
||||
if (FORCED_DARK_DESIGN_THEMES.has(designTheme)) return
|
||||
const currentTheme =
|
||||
document.documentElement.getAttribute("data-theme") || "light"
|
||||
const newTheme = currentTheme === "dark" ? "light" : "dark"
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
export function getInitialDesignTheme() {
|
||||
const savedDesignTheme = localStorage.getItem("designTheme")
|
||||
if (savedDesignTheme && DESIGN_THEMES.includes(savedDesignTheme)) {
|
||||
return savedDesignTheme
|
||||
}
|
||||
return "fluent"
|
||||
}
|
||||
|
||||
export function setDesignTheme(designTheme, themeToggle) {
|
||||
const safeDesignTheme = DESIGN_THEMES.includes(designTheme)
|
||||
? designTheme
|
||||
: "fluent"
|
||||
const previousDesignTheme =
|
||||
document.documentElement.getAttribute("data-design-theme") || "fluent"
|
||||
document.documentElement.setAttribute("data-design-theme", safeDesignTheme)
|
||||
localStorage.setItem("designTheme", safeDesignTheme)
|
||||
|
||||
const willForceDark = FORCED_DARK_DESIGN_THEMES.has(safeDesignTheme)
|
||||
const didForceDark = FORCED_DARK_DESIGN_THEMES.has(previousDesignTheme)
|
||||
|
||||
if (willForceDark) {
|
||||
if (!didForceDark) {
|
||||
const currentTheme =
|
||||
document.documentElement.getAttribute("data-theme") || "light"
|
||||
localStorage.setItem(THEME_BEFORE_FORCED_KEY, currentTheme)
|
||||
}
|
||||
setTheme("dark")
|
||||
} else if (didForceDark) {
|
||||
const restoreTheme =
|
||||
localStorage.getItem(THEME_BEFORE_FORCED_KEY) ||
|
||||
localStorage.getItem("themeBeforeTerminal")
|
||||
if (restoreTheme === "dark" || restoreTheme === "light") {
|
||||
setTheme(restoreTheme)
|
||||
}
|
||||
localStorage.removeItem(THEME_BEFORE_FORCED_KEY)
|
||||
localStorage.removeItem("themeBeforeTerminal")
|
||||
}
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.disabled = willForceDark
|
||||
themeToggle.setAttribute("aria-disabled", willForceDark ? "true" : "false")
|
||||
themeToggle.tabIndex = willForceDark ? -1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
export function setSelectedDesignThemeUI({
|
||||
designThemeList,
|
||||
designThemeButton,
|
||||
designTheme,
|
||||
getLabel,
|
||||
}) {
|
||||
if (!designThemeList) return
|
||||
const options = [...designThemeList.querySelectorAll('[role="option"]')]
|
||||
options.forEach((el) => {
|
||||
el.setAttribute(
|
||||
"aria-selected",
|
||||
el.getAttribute("data-value") === designTheme ? "true" : "false",
|
||||
)
|
||||
})
|
||||
if (designThemeButton) {
|
||||
designThemeButton.textContent = getLabel(designTheme)
|
||||
}
|
||||
}
|
||||
|
||||
export function setDesignThemeMenuOpen({
|
||||
designThemeButton,
|
||||
designThemeList,
|
||||
open,
|
||||
}) {
|
||||
if (!designThemeButton || !designThemeList) return
|
||||
designThemeButton.setAttribute("aria-expanded", open ? "true" : "false")
|
||||
designThemeList.hidden = !open
|
||||
if (open) {
|
||||
designThemeList.focus()
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentDesignTheme() {
|
||||
return document.documentElement.getAttribute("data-design-theme") || "fluent"
|
||||
}
|
||||
|
||||
export function updateDesignThemeOptions({
|
||||
designThemeList,
|
||||
getLabel,
|
||||
language,
|
||||
}) {
|
||||
if (!designThemeList) return
|
||||
const options = [...designThemeList.querySelectorAll('[role="option"]')]
|
||||
options.forEach((el) => {
|
||||
const value = el.getAttribute("data-value")
|
||||
el.textContent = getLabel(value, language)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user