File size: 20,100 Bytes
96dd062 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 | import {
DARK_MODE,
DEFAULT_THEME,
LIGHT_MODE,
SYSTEM_MODE,
WALLPAPER_BANNER,
WALLPAPER_NONE,
WALLPAPER_OVERLAY,
} from "@constants/constants";
import type { LIGHT_DARK_MODE, WALLPAPER_MODE } from "@/types/config";
import {
backgroundWallpaper,
expressiveCodeConfig,
siteConfig,
} from "../config";
import { isHomePage as checkIsHomePage } from "./layout-utils";
// Declare global functions
declare global {
interface Window {
initSemifullScrollDetection?: () => void;
semifullScrollHandler?: () => void;
}
}
export function getDefaultHue(): number {
const fallback = "250";
// 检查是否在浏览器环境中
if (typeof document === "undefined") {
return Number.parseInt(fallback, 10);
}
const configCarrier = document.getElementById("config-carrier");
return Number.parseInt(configCarrier?.dataset.hue || fallback, 10);
}
export function getDefaultTheme(): LIGHT_DARK_MODE {
// 如果配置文件中设置了 defaultMode,使用配置的值
// 否则使用 DEFAULT_THEME(向后兼容)
return siteConfig.themeColor.defaultMode ?? DEFAULT_THEME;
}
// 获取系统主题
export function getSystemTheme(): LIGHT_DARK_MODE {
if (typeof window === "undefined") {
return LIGHT_MODE;
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? DARK_MODE
: LIGHT_MODE;
}
// 解析主题(如果是system模式,则获取系统主题)
export function resolveTheme(theme: LIGHT_DARK_MODE): LIGHT_DARK_MODE {
if (theme === SYSTEM_MODE) {
return getSystemTheme();
}
return theme;
}
export function getHue(): number {
// 先检查全局对象
if (typeof window === "undefined" || !window.localStorage) {
return getDefaultHue();
}
const stored = localStorage.getItem("hue");
return stored ? Number.parseInt(stored, 10) : getDefaultHue();
}
export function setHue(hue: number): void {
// 先检查是否在浏览器环境
if (
typeof window === "undefined" ||
!window.localStorage ||
typeof document === "undefined"
) {
return;
}
localStorage.setItem("hue", String(hue));
const r = document.querySelector(":root") as HTMLElement;
if (!r) {
return;
}
r.style.setProperty("--hue", String(hue));
}
export function applyThemeToDocument(theme: LIGHT_DARK_MODE) {
// 检查是否在浏览器环境中
if (typeof document === "undefined") {
return;
}
// 解析主题
const resolvedTheme = resolveTheme(theme);
// 获取当前主题状态的完整信息
const currentIsDark = document.documentElement.classList.contains("dark");
const currentTheme = document.documentElement.getAttribute("data-theme");
// 计算目标主题状态
let targetIsDark = false; // 初始化默认值
switch (resolvedTheme) {
case LIGHT_MODE:
targetIsDark = false;
break;
case DARK_MODE:
targetIsDark = true;
break;
default:
// 处理默认情况,使用当前主题状态
targetIsDark = currentIsDark;
break;
}
// 检测是否真的需要主题切换:
// 1. dark类状态是否改变
// 2. expressiveCode主题是否需要更新
const needsThemeChange = currentIsDark !== targetIsDark;
const expectedTheme = targetIsDark
? expressiveCodeConfig.darkTheme
: expressiveCodeConfig.lightTheme;
const needsCodeThemeUpdate = currentTheme !== expectedTheme;
// 如果既不需要主题切换也不需要代码主题更新,直接返回
if (!needsThemeChange && !needsCodeThemeUpdate) {
return;
}
// 批量 DOM 操作,减少重绘
if (needsThemeChange) {
// 添加过渡保护类(但会导致大量重绘,所以使用更轻量的方式)
// document.documentElement.classList.add("is-theme-transitioning");
// 直接切换主题,利用 CSS 变量的特性让浏览器优化过渡
if (targetIsDark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
// Set the theme for Expressive Code based on current mode
if (needsCodeThemeUpdate) {
document.documentElement.setAttribute("data-theme", expectedTheme);
}
}
// 系统主题监听器引用
let systemThemeListener:
| ((e: MediaQueryListEvent | MediaQueryList) => void)
| null = null;
export function setTheme(theme: LIGHT_DARK_MODE): void {
// 检查是否在浏览器环境中
if (
typeof localStorage === "undefined" ||
typeof localStorage.setItem !== "function"
) {
return;
}
// 先应用主题
applyThemeToDocument(theme);
// 保存到localStorage
localStorage.setItem("theme", theme);
// 如果切换到 system 模式,需要监听系统主题变化
if (theme === SYSTEM_MODE) {
setupSystemThemeListener();
} else {
// 如果切换其他模式,移除系统主题监听
cleanupSystemThemeListener();
}
}
// 设置系统主题监听器
export function setupSystemThemeListener() {
// 先清理之前的监听器
cleanupSystemThemeListener();
if (typeof window === "undefined") {
return;
}
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
// 处理系统主题变化的回调
const handleSystemThemeChange = (e: MediaQueryListEvent | MediaQueryList) => {
const isDark = e.matches;
const currentIsDark = document.documentElement.classList.contains("dark");
// 如果主题状态没有变化,直接返回
if (currentIsDark === isDark) {
return;
}
// 直接应用系统主题,不使用过渡保护类以避免大量重绘
if (isDark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
// Set the theme for Expressive Code
const expressiveTheme = isDark
? expressiveCodeConfig.darkTheme
: expressiveCodeConfig.lightTheme;
document.documentElement.setAttribute("data-theme", expressiveTheme);
// 触发自定义事件通知其他组件(仅在真正切换时触发)
window.dispatchEvent(new CustomEvent("theme-change"));
};
// 立即调用一次以设置初始状态
handleSystemThemeChange(mediaQuery);
// 监听系统主题变化(现代浏览器)
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener("change", handleSystemThemeChange);
} else {
// 兼容旧浏览器
mediaQuery.addListener(handleSystemThemeChange);
}
systemThemeListener = handleSystemThemeChange;
}
// 清理系统主题监听器
function cleanupSystemThemeListener() {
if (typeof window === "undefined" || !systemThemeListener) {
return;
}
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
if (mediaQuery.removeEventListener) {
mediaQuery.removeEventListener("change", systemThemeListener);
} else {
// 兼容旧浏览器
mediaQuery.removeListener(systemThemeListener);
}
systemThemeListener = null;
}
export function getStoredTheme(): LIGHT_DARK_MODE {
// 检查是否在浏览器环境中
if (
typeof localStorage === "undefined" ||
typeof localStorage.getItem !== "function"
) {
return getDefaultTheme();
}
return (
(localStorage.getItem("theme") as LIGHT_DARK_MODE) || getDefaultTheme()
);
}
// 初始化主题监听器(用于页面加载后)
export function initThemeListener() {
if (
typeof localStorage === "undefined" ||
typeof localStorage.getItem !== "function"
) {
return;
}
const theme = getStoredTheme();
// 如果主题是 system 模式,需要监听系统主题变化
if (theme === SYSTEM_MODE) {
setupSystemThemeListener();
}
}
// Wallpaper mode functions
export function applyWallpaperModeToDocument(mode: WALLPAPER_MODE) {
// 检查是否允许切换壁纸模式
const isSwitchable = backgroundWallpaper.switchable ?? true;
if (!isSwitchable) {
// 如果不允许切换,直接返回,不执行任何操作
return;
}
// 获取当前的壁纸模式
const currentMode =
(document.documentElement.getAttribute(
"data-wallpaper-mode",
) as WALLPAPER_MODE) || backgroundWallpaper.mode;
// 如果模式没有变化,直接返回
if (currentMode === mode) {
// 即使是相同模式,也要确保UI状态正确
ensureWallpaperState(mode);
return;
}
// 添加过渡保护类
document.documentElement.classList.add("is-wallpaper-transitioning");
// 更新数据属性
document.documentElement.setAttribute("data-wallpaper-mode", mode);
// 使用 requestAnimationFrame 确保在下一帧执行,避免闪屏
requestAnimationFrame(() => {
const body = document.body;
// 移除所有壁纸相关的CSS类
body.classList.remove("enable-banner", "wallpaper-transparent");
// 根据模式添加相应的CSS类
switch (mode) {
case WALLPAPER_BANNER:
body.classList.add("enable-banner");
showBannerMode();
break;
case WALLPAPER_OVERLAY:
body.classList.add("wallpaper-transparent");
showOverlayMode();
break;
case WALLPAPER_NONE:
hideAllWallpapers();
break;
default:
hideAllWallpapers();
break;
}
// 更新导航栏透明模式
updateNavbarTransparency(mode);
// 在下一帧移除过渡保护类
requestAnimationFrame(() => {
document.documentElement.classList.remove("is-wallpaper-transitioning");
});
});
}
// 确保壁纸状态正确
function ensureWallpaperState(mode: WALLPAPER_MODE) {
const body = document.body;
// 移除所有壁纸相关的CSS类
body.classList.remove("enable-banner", "wallpaper-transparent");
// 根据模式添加相应的CSS类
switch (mode) {
case WALLPAPER_BANNER:
body.classList.add("enable-banner");
showBannerMode();
break;
case WALLPAPER_OVERLAY:
body.classList.add("wallpaper-transparent");
showOverlayMode();
break;
case WALLPAPER_NONE:
hideAllWallpapers();
break;
}
// 更新导航栏透明模式
updateNavbarTransparency(mode);
}
function showBannerMode() {
// 隐藏全屏壁纸(通过CSS类和display控制)
const overlayContainer = document.querySelector(
"[data-overlay-wallpaper]",
) as HTMLElement;
if (overlayContainer) {
overlayContainer.style.display = "none";
overlayContainer.classList.add("hidden");
overlayContainer.classList.add("opacity-0");
overlayContainer.classList.remove("opacity-100");
}
// 显示banner壁纸(通过CSS类和display控制)
const bannerWrapper = document.getElementById("banner-wrapper");
if (bannerWrapper) {
// 检查当前是否为首页
const isHomePage = checkIsHomePage(window.location.pathname);
const isMobile = window.innerWidth < 1024;
// 移动端非首页时,不显示banner;桌面端始终显示
if (isMobile && !isHomePage) {
bannerWrapper.style.display = "none";
bannerWrapper.classList.add("mobile-hide-banner");
} else {
// 首页或桌面端:先设置display,然后使用requestAnimationFrame确保渲染
bannerWrapper.style.display = "block";
bannerWrapper.style.setProperty("display", "block", "important");
requestAnimationFrame(() => {
bannerWrapper.classList.remove("hidden");
bannerWrapper.classList.remove("opacity-0");
bannerWrapper.classList.add("opacity-100");
bannerWrapper.classList.remove("mobile-hide-banner");
});
}
}
// 显示横幅图片来源文本
const creditDesktop = document.getElementById("banner-credit-desktop");
const creditMobile = document.getElementById("banner-credit-mobile");
if (creditDesktop) creditDesktop.style.display = "";
if (creditMobile) creditMobile.style.display = "";
// 显示横幅首页文本(如果启用且是首页)
const bannerTextOverlay = document.querySelector(".banner-text-overlay");
if (bannerTextOverlay) {
// 检查是否启用 homeText
const homeTextEnabled = backgroundWallpaper.banner?.homeText?.enable;
// 检查当前是否为首页
const isHomePage = checkIsHomePage(window.location.pathname);
// 只有在启用且在首页时才显示
if (homeTextEnabled && isHomePage) {
bannerTextOverlay.classList.remove("hidden");
} else {
bannerTextOverlay.classList.add("hidden");
}
}
// 调整主内容位置
adjustMainContentPosition("banner");
// 处理移动端非首页主内容区域位置
const mainContentWrapper = document.querySelector(".absolute.w-full.z-30");
if (mainContentWrapper) {
const isHomePage = checkIsHomePage(window.location.pathname);
const isMobile = window.innerWidth < 1024;
// 只在移动端非首页时调整主内容位置
if (isMobile && !isHomePage) {
mainContentWrapper.classList.add("mobile-main-no-banner");
} else {
mainContentWrapper.classList.remove("mobile-main-no-banner");
}
}
// 移除透明效果(横幅模式不使用半透明)
adjustMainContentTransparency(false);
// 调整导航栏透明度
const navbar = document.getElementById("navbar");
if (navbar) {
// 获取导航栏透明模式配置(banner模式)
const transparentMode =
backgroundWallpaper.banner?.navbar?.transparentMode || "semi";
navbar.setAttribute("data-transparent-mode", transparentMode);
// 重新初始化半透明模式滚动检测(如果需要)
if (
transparentMode === "semifull" &&
typeof window.initSemifullScrollDetection === "function"
) {
window.initSemifullScrollDetection();
}
}
}
function showOverlayMode() {
// 显示全屏壁纸(通过CSS类和display控制)
const overlayContainer = document.querySelector(
"[data-overlay-wallpaper]",
) as HTMLElement;
if (overlayContainer) {
// 先设置display,然后使用requestAnimationFrame确保渲染
overlayContainer.style.display = "block";
overlayContainer.style.setProperty("display", "block", "important");
requestAnimationFrame(() => {
overlayContainer.classList.remove("hidden");
overlayContainer.classList.remove("opacity-0");
overlayContainer.classList.add("opacity-100");
});
}
// 隐藏banner壁纸(通过CSS类和display控制)
const bannerWrapper = document.getElementById("banner-wrapper");
if (bannerWrapper) {
bannerWrapper.style.display = "none";
bannerWrapper.classList.add("hidden");
bannerWrapper.classList.add("opacity-0");
bannerWrapper.classList.remove("opacity-100");
}
// 隐藏横幅图片来源文本
const creditDesktop = document.getElementById("banner-credit-desktop");
const creditMobile = document.getElementById("banner-credit-mobile");
if (creditDesktop) creditDesktop.style.display = "none";
if (creditMobile) creditMobile.style.display = "none";
// 隐藏横幅首页文本
const bannerTextOverlay = document.querySelector(".banner-text-overlay");
if (bannerTextOverlay) {
bannerTextOverlay.classList.add("hidden");
}
// 调整主内容透明度
adjustMainContentTransparency(true);
// 调整布局为紧凑模式
adjustMainContentPosition("overlay");
}
function hideAllWallpapers() {
// 隐藏所有壁纸(通过CSS类和display控制)
const bannerWrapper = document.getElementById("banner-wrapper");
const overlayContainer = document.querySelector(
"[data-overlay-wallpaper]",
) as HTMLElement;
if (bannerWrapper) {
bannerWrapper.style.display = "none";
bannerWrapper.classList.add("hidden");
bannerWrapper.classList.add("opacity-0");
}
if (overlayContainer) {
overlayContainer.style.display = "none";
overlayContainer.classList.add("hidden");
overlayContainer.classList.add("opacity-0");
overlayContainer.classList.remove("opacity-100");
}
// 隐藏横幅图片来源文本
const creditDesktop = document.getElementById("banner-credit-desktop");
const creditMobile = document.getElementById("banner-credit-mobile");
if (creditDesktop) creditDesktop.style.display = "none";
if (creditMobile) creditMobile.style.display = "none";
// 隐藏横幅首页文本
const bannerTextOverlay = document.querySelector(".banner-text-overlay");
if (bannerTextOverlay) {
bannerTextOverlay.classList.add("hidden");
}
// 调整主内容位置和透明度
adjustMainContentPosition("none");
adjustMainContentTransparency(false);
}
function updateNavbarTransparency(mode: WALLPAPER_MODE) {
const navbar = document.getElementById("navbar");
if (!navbar) return;
let transparentMode: string;
let enableBlur: boolean;
// 根据当前壁纸模式设置导航栏透明模式和模糊效果
if (mode === WALLPAPER_OVERLAY) {
// 全屏壁纸模式
transparentMode = "none";
enableBlur = false;
} else if (mode === WALLPAPER_NONE) {
// 纯色背景模式
transparentMode = "none";
enableBlur = false;
} else {
// Banner模式:使用配置的透明模式和模糊效果
transparentMode =
backgroundWallpaper.banner?.navbar?.transparentMode || "semi";
enableBlur = backgroundWallpaper.banner?.navbar?.enableBlur ?? true;
}
// 更新导航栏的透明模式属性
navbar.setAttribute("data-transparent-mode", transparentMode);
navbar.setAttribute("data-enable-blur", String(enableBlur));
// 移除现有的透明模式类
navbar.classList.remove(
"navbar-transparent-semi",
"navbar-transparent-full",
"navbar-transparent-semifull",
);
// 移除scrolled类
navbar.classList.remove("scrolled");
// 滚动检测功能
if (
transparentMode === "semifull" &&
mode === WALLPAPER_BANNER &&
typeof window.initSemifullScrollDetection === "function"
) {
// 仅在Banner模式的semifull下启用滚动检测
window.initSemifullScrollDetection();
} else if (window.semifullScrollHandler) {
// 移除滚动监听器
window.removeEventListener("scroll", window.semifullScrollHandler);
delete window.semifullScrollHandler;
}
}
function adjustMainContentPosition(
mode: WALLPAPER_MODE | "banner" | "none" | "overlay",
) {
const mainContent = document.querySelector(
".absolute.w-full.z-30",
) as HTMLElement;
if (!mainContent) return;
// 移除现有的位置类
mainContent.classList.remove("mobile-main-no-banner", "no-banner-layout");
switch (mode) {
case "banner":
// Banner模式:主内容在banner下方
mainContent.style.top = "calc(var(--banner-height) - 3rem)";
break;
case "overlay":
// Overlay模式:使用紧凑布局,主内容从导航栏下方开始
mainContent.classList.add("no-banner-layout");
mainContent.style.top = "5.5rem";
break;
case "none":
// 无壁纸模式:主内容从导航栏下方开始
mainContent.classList.add("no-banner-layout");
mainContent.style.top = "5.5rem";
break;
default:
mainContent.style.top = "5.5rem";
break;
}
}
function adjustMainContentTransparency(enable: boolean) {
const mainContent = document.querySelector(".absolute.w-full.z-30");
const body = document.body;
if (!mainContent || !body) return;
if (enable) {
mainContent.classList.add("wallpaper-transparent");
body.classList.add("wallpaper-transparent");
} else {
mainContent.classList.remove("wallpaper-transparent");
body.classList.remove("wallpaper-transparent");
}
}
export function setWallpaperMode(mode: WALLPAPER_MODE): void {
// 检查是否在浏览器环境中
if (
typeof localStorage === "undefined" ||
typeof localStorage.setItem !== "function"
) {
return;
}
localStorage.setItem("wallpaperMode", mode);
applyWallpaperModeToDocument(mode);
}
export function initWallpaperMode(): void {
const storedMode = getStoredWallpaperMode();
applyWallpaperModeToDocument(storedMode);
}
export function getStoredWallpaperMode(): WALLPAPER_MODE {
// 检查是否在浏览器环境中
if (
typeof localStorage === "undefined" ||
typeof localStorage.getItem !== "function"
) {
return backgroundWallpaper.mode;
}
return (
(localStorage.getItem("wallpaperMode") as WALLPAPER_MODE) ||
backgroundWallpaper.mode
);
}
|