Files
blog-fuwari/src/layouts/Layout.astro
T
2026-05-01 23:01:32 +08:00

850 lines
28 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 已被移到 bodyportal 模式),直接移除
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>