mirror of
https://github.com/Ad-closeNN/blog-fuwari.git
synced 2026-05-31 01:00:04 -04:00
850 lines
28 KiB
Plaintext
850 lines
28 KiB
Plaintext
---
|
||
import ConfigCarrier from "@components/ConfigCarrier.astro";
|
||
import { Icon } from "astro-icon/components";
|
||
import { profileConfig, siteConfig, umamiConfig } from "@/config";
|
||
import {
|
||
AUTO_MODE,
|
||
BANNER_HEIGHT,
|
||
BANNER_HEIGHT_EXTEND,
|
||
BANNER_HEIGHT_HOME,
|
||
DARK_MODE,
|
||
DEFAULT_THEME,
|
||
LIGHT_MODE,
|
||
PAGE_WIDTH,
|
||
} from "../constants/constants";
|
||
import { defaultFavicons } from "../constants/icon";
|
||
import type { Favicon } from "../types/config";
|
||
import { pathsEqual, url } from "../utils/url-utils";
|
||
import "katex/dist/katex.css";
|
||
|
||
interface Props {
|
||
title?: string;
|
||
banner?: string;
|
||
description?: string;
|
||
lang?: string;
|
||
setOGTypeArticle?: boolean;
|
||
}
|
||
|
||
let { title, banner, description, lang, setOGTypeArticle } = Astro.props;
|
||
|
||
// apply a class to the body element to decide the height of the banner, only used for initial page load
|
||
// Swup can update the body for each page visit, but it's after the page transition, causing a delay for banner height change
|
||
// so use Swup hooks instead to change the height immediately when a link is clicked
|
||
const isHomePage = pathsEqual(Astro.url.pathname, url("/"));
|
||
|
||
// defines global css variables
|
||
// why doing this in Layout instead of GlobalStyles: https://github.com/withastro/astro/issues/6728#issuecomment-1502203757
|
||
const configHue = siteConfig.themeColor.hue;
|
||
if (!banner || typeof banner !== "string" || banner.trim() === "") {
|
||
banner = siteConfig.banner.src;
|
||
}
|
||
|
||
// TODO don't use post cover as banner for now
|
||
banner = siteConfig.banner.src;
|
||
|
||
const enableBanner = siteConfig.banner.enable;
|
||
|
||
let pageTitle: string;
|
||
if (title) {
|
||
pageTitle = `${title} - ${siteConfig.title}`;
|
||
} else {
|
||
pageTitle = `${siteConfig.title} - ${siteConfig.subtitle}`;
|
||
}
|
||
|
||
const favicons: Favicon[] =
|
||
siteConfig.favicon.length > 0 ? siteConfig.favicon : defaultFavicons;
|
||
|
||
// const siteLang = siteConfig.lang.replace('_', '-')
|
||
if (!lang) {
|
||
lang = `${siteConfig.lang}`;
|
||
}
|
||
const siteLang = lang.replace("_", "-");
|
||
|
||
const bannerOffsetByPosition = {
|
||
top: `${BANNER_HEIGHT_EXTEND}vh`,
|
||
center: `${BANNER_HEIGHT_EXTEND / 2}vh`,
|
||
bottom: "0",
|
||
};
|
||
const bannerOffset =
|
||
bannerOffsetByPosition[siteConfig.banner.position || "center"];
|
||
---
|
||
|
||
<!DOCTYPE html>
|
||
<html lang={siteLang} class="bg-[var(--page-bg)] transition text-[14px] md:text-[16px]"
|
||
data-overlayscrollbars-initialize
|
||
>
|
||
<head>
|
||
|
||
<title>{pageTitle}</title>
|
||
|
||
<meta charset="UTF-8" />
|
||
<meta name="description" content={description || pageTitle}>
|
||
<meta name="author" content={profileConfig.name}>
|
||
|
||
<meta property="og:site_name" content={siteConfig.title}>
|
||
<meta property="og:url" content={Astro.url}>
|
||
<meta property="og:title" content={pageTitle}>
|
||
<meta property="og:description" content={description || pageTitle}>
|
||
{setOGTypeArticle ? (
|
||
<meta property="og:type" content="article" />
|
||
) : (
|
||
<meta property="og:type" content="website" />
|
||
)}
|
||
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta property="twitter:url" content={Astro.url}>
|
||
<meta name="twitter:title" content={pageTitle}>
|
||
<meta name="twitter:description" content={description || pageTitle}>
|
||
|
||
<meta name="viewport" content="width=device-width" />
|
||
<meta name="generator" content={Astro.generator} />
|
||
<meta id="theme-color-meta" name="theme-color" content="#48823b" />
|
||
|
||
{favicons.map(favicon => (
|
||
<link rel="icon"
|
||
href={favicon.src.startsWith('/') ? url(favicon.src) : favicon.src}
|
||
sizes={favicon.sizes}
|
||
media={favicon.theme && `(prefers-color-scheme: ${favicon.theme})`}
|
||
/>
|
||
))}
|
||
|
||
<!-- Set the theme before the page is rendered to avoid a flash -->
|
||
<script is:inline define:vars={{DEFAULT_THEME, LIGHT_MODE, DARK_MODE, AUTO_MODE, BANNER_HEIGHT_EXTEND, PAGE_WIDTH, configHue}}>
|
||
// Load the theme from local storage
|
||
const theme = localStorage.getItem('theme') || DEFAULT_THEME;
|
||
switch (theme) {
|
||
case LIGHT_MODE:
|
||
document.documentElement.classList.remove('dark');
|
||
break
|
||
case DARK_MODE:
|
||
document.documentElement.classList.add('dark');
|
||
break
|
||
case AUTO_MODE:
|
||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||
document.documentElement.classList.add('dark');
|
||
} else {
|
||
document.documentElement.classList.remove('dark');
|
||
}
|
||
}
|
||
|
||
// Load the hue from local storage
|
||
const hue = localStorage.getItem('hue') || configHue;
|
||
document.documentElement.style.setProperty('--hue', hue);
|
||
|
||
// calculate the --banner-height-extend, which needs to be a multiple of 4 to avoid blurry text
|
||
let offset = Math.floor(window.innerHeight * (BANNER_HEIGHT_EXTEND / 100));
|
||
offset = offset - offset % 4;
|
||
document.documentElement.style.setProperty('--banner-height-extend', `${offset}px`);
|
||
</script>
|
||
<style define:vars={{
|
||
configHue,
|
||
'page-width': `${PAGE_WIDTH}rem`,
|
||
/* https://github.com/afoim/fuwari/commit/aad0a7734be5f6e96a54a3aed51e49760ab3162a */
|
||
'bg-url': siteConfig.background.src ? `url(${siteConfig.background.src})` : 'none',
|
||
'bg-enable': siteConfig.background.enable ? '1' : '0',
|
||
'bg-position': siteConfig.background.position || 'center',
|
||
'bg-size': siteConfig.background.size || 'cover',
|
||
'bg-repeat': siteConfig.background.repeat || 'no-repeat',
|
||
'bg-attachment': siteConfig.background.attachment || 'fixed',
|
||
'bg-opacity': (siteConfig.background.opacity || 0.3).toString()
|
||
}}>
|
||
:root {
|
||
--bg-url: var(--bg-url);
|
||
--bg-enable: var(--bg-enable);
|
||
--bg-position: var(--bg-position);
|
||
--bg-size: var(--bg-size);
|
||
--bg-repeat: var(--bg-repeat);
|
||
--bg-attachment: var(--bg-attachment);
|
||
--bg-opacity: var(--bg-opacity);
|
||
}
|
||
|
||
/* Background image configuration */
|
||
body {
|
||
--bg-url: var(--bg-url);
|
||
--bg-enable: var(--bg-enable);
|
||
--bg-position: var(--bg-position);
|
||
--bg-size: var(--bg-size);
|
||
--bg-repeat: var(--bg-repeat);
|
||
--bg-attachment: var(--bg-attachment);
|
||
--bg-opacity: var(--bg-opacity);
|
||
}
|
||
|
||
body::before {
|
||
content: '' !important;
|
||
position: fixed !important;
|
||
top: 0 !important;
|
||
left: 0 !important;
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
background-image: var(--bg-url) !important;
|
||
background-position: var(--bg-position) !important;
|
||
background-size: var(--bg-size) !important;
|
||
background-repeat: var(--bg-repeat) !important;
|
||
background-attachment: var(--bg-attachment) !important;
|
||
opacity: 0 !important;
|
||
pointer-events: none !important;
|
||
z-index: -1 !important;
|
||
display: block !important;
|
||
transition: opacity 0.3s ease-in-out !important;
|
||
}
|
||
|
||
body.bg-loaded::before {
|
||
opacity: calc(var(--bg-opacity) * var(--bg-enable)) !important;
|
||
}
|
||
</style> <!-- defines global css variables. This will be applied to <html> <body> and some other elements idk why -->
|
||
|
||
|
||
<slot name="head"></slot>
|
||
|
||
<script is:inline define:vars={{ umamiConfig }} data-swup-ignore-script>
|
||
window['__blogUmami'] = window['__blogUmami'] || {
|
||
shareDataPromise: null,
|
||
statsPromiseCache: new Map(),
|
||
statsValueCache: new Map(),
|
||
async getShareData() {
|
||
if (!umamiConfig.enable) {
|
||
return null;
|
||
}
|
||
if (this.shareDataPromise) {
|
||
return this.shareDataPromise;
|
||
}
|
||
this.shareDataPromise = fetch(`${umamiConfig.baseUrl}/api/share/${umamiConfig.shareId}`)
|
||
.then((response) => {
|
||
if (!response.ok) {
|
||
throw new Error('获取分享信息失败');
|
||
}
|
||
return response.json();
|
||
})
|
||
.catch((error) => {
|
||
this.shareDataPromise = null;
|
||
throw error;
|
||
});
|
||
return this.shareDataPromise;
|
||
},
|
||
async getStats(cacheKey, buildStatsUrl) {
|
||
if (this.statsValueCache.has(cacheKey)) {
|
||
return this.statsValueCache.get(cacheKey);
|
||
}
|
||
if (this.statsPromiseCache.has(cacheKey)) {
|
||
return this.statsPromiseCache.get(cacheKey);
|
||
}
|
||
const requestPromise = this.getShareData()
|
||
.then(async (shareData) => {
|
||
if (!shareData) {
|
||
return null;
|
||
}
|
||
const statsUrl = buildStatsUrl(shareData.websiteId);
|
||
const statsResponse = await fetch(statsUrl, {
|
||
headers: {
|
||
'x-umami-share-token': shareData.token,
|
||
},
|
||
});
|
||
if (!statsResponse.ok) {
|
||
throw new Error('获取统计数据失败');
|
||
}
|
||
const statsData = await statsResponse.json();
|
||
this.statsValueCache.set(cacheKey, statsData);
|
||
this.statsPromiseCache.delete(cacheKey);
|
||
return statsData;
|
||
})
|
||
.catch((error) => {
|
||
this.statsPromiseCache.delete(cacheKey);
|
||
throw error;
|
||
});
|
||
this.statsPromiseCache.set(cacheKey, requestPromise);
|
||
return requestPromise;
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<link rel="alternate" type="application/rss+xml" title={profileConfig.name} href={`${Astro.site}rss.xml`}/>
|
||
<!-- Umami分析(自建) -->
|
||
<script defer src="https://umami.adclosenn.top/script.js" data-website-id="7610548d-677b-40cb-9ebd-31dc14e16be7" data-swup-ignore-script></script>
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-YRCGFG45C1"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag(){dataLayer.push(arguments);}
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-YRCGFG45C1');
|
||
</script>
|
||
|
||
</head>
|
||
<body class=" min-h-screen transition " class:list={[{"lg:is-home": isHomePage, "enable-banner": enableBanner}]}
|
||
data-overlayscrollbars-initialize
|
||
>
|
||
<ConfigCarrier></ConfigCarrier>
|
||
<div id="dns-warning-banner" class="hidden sticky top-0 z-[100] w-full bg-amber-50/90 dark:bg-amber-950/80 backdrop-blur-md border-b border-amber-200/60 dark:border-amber-700/40 text-amber-900 dark:text-amber-100 px-4 py-3 text-sm text-center">
|
||
<div class="select-none flex items-center justify-center gap-2 max-w-[var(--page-width)] mx-auto">
|
||
<Icon is:inline name="material-symbols:info-outline-rounded" class="pointer-events-none shrink-0 text-lg" aria-hidden="true"></Icon>
|
||
<span>本站近期遭到反诈部门 DNS 污染,可能无法正常访问。如有需要可开启代理或使用国外 DNS 服务器。</span>
|
||
<button id="dns-warning-close" class="select-auto shrink-0 btn-plain scale-animation rounded-lg w-7 h-7 flex items-center justify-center text-amber-600 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-200 active:scale-90" aria-label="Close notice">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function() {
|
||
var banner = document.getElementById('dns-warning-banner');
|
||
var closeBtn = document.getElementById('dns-warning-close');
|
||
if (!banner || !closeBtn) return;
|
||
if (!localStorage.getItem('dns-warning-dismissed')) {
|
||
banner.classList.remove('hidden');
|
||
}
|
||
closeBtn.addEventListener('click', function() {
|
||
banner.classList.add('hidden');
|
||
localStorage.setItem('dns-warning-dismissed', '1');
|
||
});
|
||
})();
|
||
</script>
|
||
<slot />
|
||
|
||
<!-- increase the page height during page transition to prevent the scrolling animation from jumping -->
|
||
<div id="page-height-extend" class="hidden h-[300vh]"></div>
|
||
</body>
|
||
</html>
|
||
|
||
<style is:global define:vars={{
|
||
bannerOffset,
|
||
'banner-height-home': `${BANNER_HEIGHT_HOME}vh`,
|
||
'banner-height': `${BANNER_HEIGHT}vh`,
|
||
}}>
|
||
@tailwind components;
|
||
@layer components {
|
||
.enable-banner.is-home #banner-wrapper {
|
||
@apply h-[var(--banner-height-home)] translate-y-[var(--banner-height-extend)]
|
||
}
|
||
.enable-banner #banner-wrapper {
|
||
@apply h-[var(--banner-height-home)]
|
||
}
|
||
|
||
.enable-banner.is-home #banner {
|
||
@apply h-[var(--banner-height-home)] translate-y-0
|
||
}
|
||
.enable-banner #banner {
|
||
@apply h-[var(--banner-height-home)] translate-y-[var(--bannerOffset)]
|
||
}
|
||
.enable-banner.is-home #main-grid {
|
||
@apply translate-y-[var(--banner-height-extend)];
|
||
}
|
||
.enable-banner #top-row {
|
||
@apply h-[calc(var(--banner-height-home)_-_4.5rem)] transition-all duration-300
|
||
}
|
||
.enable-banner.is-home #sidebar-sticky {
|
||
@apply top-[calc(1rem_-_var(--banner-height-extend))]
|
||
}
|
||
.navbar-hidden {
|
||
@apply opacity-0 -translate-y-16
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
import 'overlayscrollbars/overlayscrollbars.css';
|
||
import {
|
||
OverlayScrollbars,
|
||
// ScrollbarsHidingPlugin,
|
||
// SizeObserverPlugin,
|
||
// ClickScrollPlugin
|
||
} from 'overlayscrollbars';
|
||
import {applyCustomThemeStyles, getHue, getStoredTheme, setHue, setTheme} from "../utils/setting-utils";
|
||
import {pathsEqual, url} from "../utils/url-utils";
|
||
import {
|
||
BANNER_HEIGHT,
|
||
BANNER_HEIGHT_HOME,
|
||
BANNER_HEIGHT_EXTEND,
|
||
MAIN_PANEL_OVERLAPS_BANNER_HEIGHT
|
||
} from "../constants/constants";
|
||
import { siteConfig } from '../config';
|
||
|
||
/* Preload fonts */
|
||
// (async function() {
|
||
// try {
|
||
// await Promise.all([
|
||
// document.fonts.load("400 1em Roboto"),
|
||
// document.fonts.load("700 1em Roboto"),
|
||
// ]);
|
||
// document.body.classList.remove("hidden");
|
||
// } catch (error) {
|
||
// console.log("Failed to load fonts:", error);
|
||
// }
|
||
// })();
|
||
|
||
/* TODO This is a temporary solution for style flicker issue when the transition is activated */
|
||
/* issue link: https://github.com/withastro/astro/issues/8711, the solution get from here too */
|
||
/* update: fixed in Astro 3.2.4 */
|
||
/*
|
||
function disableAnimation() {
|
||
const css = document.createElement('style')
|
||
css.appendChild(
|
||
document.createTextNode(
|
||
`*{
|
||
-webkit-transition:none!important;
|
||
-moz-transition:none!important;
|
||
-o-transition:none!important;
|
||
-ms-transition:none!important;
|
||
transition:none!important
|
||
}`
|
||
)
|
||
)
|
||
document.head.appendChild(css)
|
||
|
||
return () => {
|
||
// Force restyle
|
||
;(() => window.getComputedStyle(document.body))()
|
||
|
||
// Wait for next tick before removing
|
||
setTimeout(() => {
|
||
document.head.removeChild(css)
|
||
}, 1)
|
||
}
|
||
}
|
||
*/
|
||
|
||
const bannerEnabled = !!document.getElementById('banner-wrapper')
|
||
|
||
function setClickOutsideToClose(panel: string, ignores: string[]) {
|
||
document.addEventListener("click", event => {
|
||
let panelDom = document.getElementById(panel);
|
||
let tDom = event.target;
|
||
if (!(tDom instanceof Node)) return; // Ensure the event target is an HTML Node
|
||
for (let ig of ignores) {
|
||
let ie = document.getElementById(ig)
|
||
if (ie == tDom || (ie?.contains(tDom))) {
|
||
return;
|
||
}
|
||
}
|
||
panelDom!.classList.add("float-panel-closed");
|
||
});
|
||
}
|
||
setClickOutsideToClose("display-setting", ["display-setting", "display-settings-switch"])
|
||
setClickOutsideToClose("nav-menu-panel", ["nav-menu-panel", "nav-menu-switch"])
|
||
setClickOutsideToClose("search-panel", ["search-panel", "search-bar", "search-switch"])
|
||
|
||
|
||
function loadTheme() {
|
||
const theme = getStoredTheme()
|
||
setTheme(theme)
|
||
}
|
||
|
||
function loadHue() {
|
||
setHue(getHue())
|
||
}
|
||
|
||
function initCustomScrollbar() {
|
||
const bodyElement = document.querySelector('body');
|
||
if (!bodyElement) return;
|
||
OverlayScrollbars(
|
||
// docs say that a initialization to the body element would affect native functionality like window.scrollTo
|
||
// but just leave it here for now
|
||
{
|
||
target: bodyElement,
|
||
cancel: {
|
||
nativeScrollbarsOverlaid: true, // don't initialize the overlay scrollbar if there is a native one
|
||
}
|
||
}, {
|
||
scrollbars: {
|
||
theme: 'scrollbar-base scrollbar-auto py-1',
|
||
autoHide: 'move',
|
||
autoHideDelay: 500,
|
||
autoHideSuspend: false,
|
||
},
|
||
});
|
||
|
||
const katexElements = document.querySelectorAll('.katex-display') as NodeListOf<HTMLElement>;
|
||
|
||
const katexObserverOptions = {
|
||
root: null,
|
||
rootMargin: '100px',
|
||
threshold: 0.1
|
||
};
|
||
|
||
const processKatexElement = (element: HTMLElement) => {
|
||
if (!element.parentNode) return;
|
||
if (element.hasAttribute('data-scrollbar-initialized')) return;
|
||
|
||
const container = document.createElement('div');
|
||
container.className = 'katex-display-container';
|
||
container.setAttribute('aria-label', 'scrollable container for formulas');
|
||
|
||
element.parentNode.insertBefore(container, element);
|
||
container.appendChild(element);
|
||
|
||
OverlayScrollbars(container, {
|
||
scrollbars: {
|
||
theme: 'scrollbar-base scrollbar-auto',
|
||
autoHide: 'leave',
|
||
autoHideDelay: 500,
|
||
autoHideSuspend: false
|
||
}
|
||
});
|
||
|
||
element.setAttribute('data-scrollbar-initialized', 'true');
|
||
};
|
||
|
||
const katexObserver = new IntersectionObserver((entries, observer) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
processKatexElement(entry.target as HTMLElement);
|
||
observer.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, katexObserverOptions);
|
||
|
||
katexElements.forEach(element => {
|
||
katexObserver.observe(element);
|
||
});
|
||
}
|
||
|
||
function showBanner() {
|
||
if (!siteConfig.banner.enable) return;
|
||
|
||
const banner = document.getElementById('banner');
|
||
if (!banner) {
|
||
console.error('Banner element not found');
|
||
return;
|
||
}
|
||
|
||
// 在显示Banner前,先移除所有已加载的onload-animation类,防止动画重复触发
|
||
const animatedElements = document.querySelectorAll<HTMLElement>('.onload-animation');
|
||
animatedElements.forEach((el) => {
|
||
el.style.animation = 'none';
|
||
el.style.opacity = '1';
|
||
});
|
||
|
||
banner.classList.remove('opacity-0', 'scale-105');
|
||
}
|
||
|
||
function init() {
|
||
// disableAnimation()() // TODO
|
||
loadTheme();
|
||
loadHue();
|
||
applyCustomThemeStyles();
|
||
initCustomScrollbar();
|
||
showBanner();
|
||
}
|
||
|
||
/* Load settings when entering the site */
|
||
init();
|
||
|
||
const setup = () => {
|
||
// TODO: temp solution to change the height of the banner
|
||
/*
|
||
window.swup.hooks.on('animation:out:start', () => {
|
||
const path = window.location.pathname
|
||
const body = document.querySelector('body')
|
||
if (path[path.length - 1] === '/' && !body.classList.contains('is-home')) {
|
||
body.classList.add('is-home')
|
||
} else if (path[path.length - 1] !== '/' && body.classList.contains('is-home')) {
|
||
body.classList.remove('is-home')
|
||
}
|
||
})
|
||
*/
|
||
window.swup.hooks.on('link:click', () => {
|
||
// Remove the delay for the first time page load
|
||
document.documentElement.style.setProperty('--content-delay', '0ms')
|
||
|
||
// prevent elements from overlapping the navbar
|
||
if (!bannerEnabled) {
|
||
return
|
||
}
|
||
let threshold = window.innerHeight * (BANNER_HEIGHT / 100) - 72 - 16
|
||
let navbar = document.getElementById('navbar-wrapper')
|
||
if (!navbar || !document.body.classList.contains('lg:is-home')) {
|
||
return
|
||
}
|
||
if (document.body.scrollTop >= threshold || document.documentElement.scrollTop >= threshold) {
|
||
navbar.classList.add('navbar-hidden')
|
||
}
|
||
})
|
||
window.swup.hooks.on('content:replace', initCustomScrollbar)
|
||
window.swup.hooks.on('visit:start', (visit: {to: {url: string}}) => {
|
||
// 切页前断开 observer、关闭并清理 panel
|
||
// @ts-ignore
|
||
if (document.body.__copyPageObserver) {
|
||
// @ts-ignore
|
||
document.body.__copyPageObserver.disconnect();
|
||
// @ts-ignore
|
||
document.body.__copyPageObserver = undefined;
|
||
}
|
||
const copyPagePanel = document.getElementById('copy-page-panel');
|
||
if (copyPagePanel) {
|
||
copyPagePanel.classList.add('pointer-events-none', 'opacity-0', '-translate-y-1');
|
||
copyPagePanel.classList.remove('opacity-100', 'translate-y-0');
|
||
// 如果 panel 已被移到 body(portal 模式),直接移除
|
||
if (copyPagePanel.parentElement === document.body) {
|
||
copyPagePanel.remove();
|
||
}
|
||
}
|
||
const copyPageMenu = document.getElementById('copy-page-menu');
|
||
const copyPageArrow = copyPageMenu?.querySelector('.copy-page-arrow');
|
||
const copyPageSwitch = document.getElementById('copy-page-switch');
|
||
if (copyPageArrow) copyPageArrow.classList.remove('rotate-180');
|
||
if (copyPageSwitch) copyPageSwitch.setAttribute('aria-expanded', 'false');
|
||
|
||
// change banner height immediately when a link is clicked
|
||
const bodyElement = document.querySelector('body')
|
||
if (pathsEqual(visit.to.url, url('/'))) {
|
||
bodyElement!.classList.add('lg:is-home');
|
||
} else {
|
||
bodyElement!.classList.remove('lg:is-home');
|
||
}
|
||
|
||
// increase the page height during page transition to prevent the scrolling animation from jumping
|
||
const heightExtend = document.getElementById('page-height-extend')
|
||
if (heightExtend) {
|
||
heightExtend.classList.remove('hidden')
|
||
}
|
||
|
||
// Hide the TOC while scrolling back to top
|
||
let toc = document.getElementById('toc-wrapper');
|
||
if (toc) {
|
||
toc.classList.add('toc-not-ready')
|
||
}
|
||
});
|
||
window.swup.hooks.on('page:view', () => {
|
||
// hide the temp high element when the transition is done
|
||
const heightExtend = document.getElementById('page-height-extend')
|
||
if (heightExtend) {
|
||
heightExtend.classList.remove('hidden')
|
||
}
|
||
});
|
||
window.swup.hooks.on('visit:end', (_visit: {to: {url: string}}) => {
|
||
setTimeout(() => {
|
||
const heightExtend = document.getElementById('page-height-extend')
|
||
if (heightExtend) {
|
||
heightExtend.classList.add('hidden')
|
||
}
|
||
|
||
// Just make the transition looks better
|
||
const toc = document.getElementById('toc-wrapper');
|
||
if (toc) {
|
||
toc.classList.remove('toc-not-ready')
|
||
}
|
||
}, 200)
|
||
});
|
||
}
|
||
if (window?.swup?.hooks) {
|
||
setup()
|
||
} else {
|
||
document.addEventListener('swup:enable', setup)
|
||
}
|
||
|
||
let backToTopBtn = document.getElementById('back-to-top-btn');
|
||
let toc = document.getElementById('toc-wrapper');
|
||
let navbar = document.getElementById('navbar-wrapper')
|
||
function scrollFunction() {
|
||
let bannerHeight = window.innerHeight * (BANNER_HEIGHT / 100)
|
||
|
||
if (backToTopBtn) {
|
||
if (document.body.scrollTop > bannerHeight || document.documentElement.scrollTop > bannerHeight) {
|
||
backToTopBtn.classList.remove('hide')
|
||
} else {
|
||
backToTopBtn.classList.add('hide')
|
||
}
|
||
}
|
||
|
||
if (bannerEnabled && toc) {
|
||
if (document.body.scrollTop > bannerHeight || document.documentElement.scrollTop > bannerHeight) {
|
||
toc.classList.remove('toc-hide')
|
||
} else {
|
||
toc.classList.add('toc-hide')
|
||
}
|
||
}
|
||
|
||
if (!bannerEnabled) return
|
||
if (navbar) {
|
||
const NAVBAR_HEIGHT = 72
|
||
const MAIN_PANEL_EXCESS_HEIGHT = MAIN_PANEL_OVERLAPS_BANNER_HEIGHT * 16 // The height the main panel overlaps the banner
|
||
|
||
let bannerHeight = BANNER_HEIGHT
|
||
if (document.body.classList.contains('lg:is-home') && window.innerWidth >= 1024) {
|
||
bannerHeight = BANNER_HEIGHT_HOME
|
||
}
|
||
let threshold = window.innerHeight * (bannerHeight / 100) - NAVBAR_HEIGHT - MAIN_PANEL_EXCESS_HEIGHT - 16
|
||
if (document.body.scrollTop >= threshold || document.documentElement.scrollTop >= threshold) {
|
||
navbar.classList.add('navbar-hidden')
|
||
} else {
|
||
navbar.classList.remove('navbar-hidden')
|
||
}
|
||
}
|
||
}
|
||
window.onscroll = scrollFunction
|
||
|
||
window.onresize = () => {
|
||
// calculate the --banner-height-extend, which needs to be a multiple of 4 to avoid blurry text
|
||
let offset = Math.floor(window.innerHeight * (BANNER_HEIGHT_EXTEND / 100));
|
||
offset = offset - offset % 4;
|
||
document.documentElement.style.setProperty('--banner-height-extend', `${offset}px`);
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
import PhotoSwipeLightbox from "photoswipe/lightbox"
|
||
import "photoswipe/style.css"
|
||
import "../styles/photoswipe.css"
|
||
|
||
const zoomTargetSelector = ".custom-md img, #post-cover img"
|
||
const pswpModule = import("photoswipe")
|
||
|
||
type PhotoSwipeState = {
|
||
lightbox: PhotoSwipeLightbox | null
|
||
hookRegistered: boolean
|
||
}
|
||
|
||
const photoSwipeState = (window.__blogPhotoSwipe ??= {
|
||
lightbox: null,
|
||
hookRegistered: false,
|
||
pageLoadRegistered: false,
|
||
}) as PhotoSwipeState & { pageLoadRegistered: boolean }
|
||
|
||
function getZoomTargets() {
|
||
return Array.from(document.querySelectorAll<HTMLImageElement>(zoomTargetSelector))
|
||
}
|
||
|
||
function destroyPhotoSwipe() {
|
||
photoSwipeState.lightbox?.destroy()
|
||
photoSwipeState.lightbox = null
|
||
}
|
||
|
||
function createPhotoSwipe() {
|
||
const lightbox = new PhotoSwipeLightbox({
|
||
gallery: "body",
|
||
children: zoomTargetSelector,
|
||
initialZoomLevel: "fit",
|
||
pswpModule: () => pswpModule,
|
||
closeSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></svg>',
|
||
zoomSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M340-540h-40q-17 0-28.5-11.5T260-580q0-17 11.5-28.5T300-620h40v-40q0-17 11.5-28.5T380-700q17 0 28.5 11.5T420-660v40h40q17 0 28.5 11.5T500-580q0 17-11.5 28.5T460-540h-40v40q0 17-11.5 28.5T380-460q-17 0-28.5-11.5T340-500v-40Zm40 220q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>',
|
||
padding: { top: 20, bottom: 20, left: 20, right: 20 },
|
||
showHideAnimationType: "fade",
|
||
showAnimationDuration: 160,
|
||
hideAnimationDuration: 140,
|
||
secondaryZoomLevel: (zoomLevel) => {
|
||
const fitLevel = zoomLevel.fit
|
||
const forcedZoomLevel = fitLevel * 1.8
|
||
return Math.max(forcedZoomLevel, Math.min(3, zoomLevel.max))
|
||
},
|
||
maxZoomLevel: (zoomLevel) => Math.max(4, zoomLevel.fit * 2.5, zoomLevel.max),
|
||
wheelToZoom: true,
|
||
arrowPrev: false,
|
||
arrowNext: false,
|
||
imageClickAction: "close",
|
||
tapAction: "close",
|
||
doubleTapAction: "zoom",
|
||
})
|
||
|
||
lightbox.addFilter("domItemData", (itemData, element) => {
|
||
if (element instanceof HTMLImageElement) {
|
||
const fullSrc = element.dataset.pswpSrc || element.currentSrc || element.src
|
||
const width = Number(element.dataset.pswpWidth) || element.naturalWidth || element.width || window.innerWidth
|
||
const height = Number(element.dataset.pswpHeight) || element.naturalHeight || element.height || window.innerHeight
|
||
const thumbSrc = element.currentSrc || element.src
|
||
const sourceElement = element.closest(zoomTargetSelector)
|
||
const allImages = getZoomTargets()
|
||
const index = sourceElement instanceof HTMLImageElement ? allImages.indexOf(sourceElement) : -1
|
||
|
||
itemData.src = fullSrc
|
||
itemData.w = Number(width)
|
||
itemData.h = Number(height)
|
||
itemData.msrc = thumbSrc
|
||
if (index >= 0) {
|
||
itemData.element = sourceElement
|
||
}
|
||
}
|
||
|
||
return itemData
|
||
})
|
||
|
||
lightbox.addFilter("clickedIndex", (clickedIndex, event) => {
|
||
const target = event.target instanceof Element ? event.target.closest(zoomTargetSelector) : null
|
||
if (!(target instanceof HTMLImageElement)) {
|
||
return clickedIndex
|
||
}
|
||
|
||
const allImages = getZoomTargets()
|
||
const index = allImages.indexOf(target)
|
||
return index >= 0 ? index : clickedIndex
|
||
})
|
||
|
||
lightbox.on("uiRegister", () => {
|
||
lightbox.pswp?.ui?.registerElement({
|
||
name: "open-link",
|
||
order: 8,
|
||
isButton: true,
|
||
tagName: "a",
|
||
className: "pswp__button pswp__button--open-link",
|
||
title: "打开原图",
|
||
html: '<svg class="pswp__icn" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3h7v7h-2V6.41l-9.29 9.3-1.42-1.42 9.3-9.29H14V3Zm5 16V11h2v10H3V3h10v2H5v14h14Z"/></svg>',
|
||
onInit: (el, pswp) => {
|
||
if (!(el instanceof HTMLAnchorElement)) {
|
||
return
|
||
}
|
||
|
||
el.target = "_blank"
|
||
el.rel = "noreferrer noopener"
|
||
|
||
const syncHref = () => {
|
||
const currSlide = pswp.currSlide
|
||
const link = typeof currSlide?.data?.src === "string" ? currSlide.data.src : ""
|
||
el.href = link
|
||
el.classList.toggle("pswp__button--disabled", !link)
|
||
el.setAttribute("aria-disabled", link ? "false" : "true")
|
||
el.tabIndex = link ? 0 : -1
|
||
}
|
||
|
||
pswp.on("change", syncHref)
|
||
syncHref()
|
||
},
|
||
})
|
||
})
|
||
|
||
lightbox.init()
|
||
photoSwipeState.lightbox = lightbox
|
||
}
|
||
|
||
function initPhotoSwipe() {
|
||
if (photoSwipeState.lightbox || getZoomTargets().length === 0) {
|
||
return
|
||
}
|
||
createPhotoSwipe()
|
||
}
|
||
|
||
function reinitPhotoSwipe() {
|
||
destroyPhotoSwipe()
|
||
window.requestAnimationFrame(() => {
|
||
initPhotoSwipe()
|
||
})
|
||
}
|
||
|
||
function registerPhotoSwipeHook() {
|
||
if (photoSwipeState.hookRegistered || !window.swup?.hooks) {
|
||
return
|
||
}
|
||
window.swup.hooks.on("page:view", reinitPhotoSwipe)
|
||
photoSwipeState.hookRegistered = true
|
||
}
|
||
|
||
function scheduleInitialPhotoSwipe() {
|
||
window.requestAnimationFrame(() => {
|
||
initPhotoSwipe()
|
||
})
|
||
}
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", scheduleInitialPhotoSwipe, { once: true })
|
||
} else {
|
||
scheduleInitialPhotoSwipe()
|
||
}
|
||
|
||
if (!photoSwipeState.pageLoadRegistered) {
|
||
document.addEventListener("astro:page-load", scheduleInitialPhotoSwipe)
|
||
photoSwipeState.pageLoadRegistered = true
|
||
}
|
||
|
||
if (window.swup?.hooks) {
|
||
registerPhotoSwipeHook()
|
||
} else {
|
||
document.addEventListener("swup:enable", registerPhotoSwipeHook, { once: true })
|
||
}
|
||
</script>
|