feat: add photoswipe for image zoom (#135)

This commit is contained in:
dabuside 2024-07-28 01:41:11 +08:00 committed by saicaca
parent 336290a92f
commit 51025f0ea7
8 changed files with 173 additions and 95 deletions

View File

@ -1,22 +1,22 @@
import sitemap from '@astrojs/sitemap';
import svelte from "@astrojs/svelte"
import tailwind from "@astrojs/tailwind" import tailwind from "@astrojs/tailwind"
import swup from '@swup/astro';
import Compress from "astro-compress" import Compress from "astro-compress"
import icon from "astro-icon" import icon from "astro-icon"
import { defineConfig } from "astro/config" import { defineConfig } from "astro/config"
import Color from "colorjs.io" import Color from "colorjs.io"
import rehypeAutolinkHeadings from "rehype-autolink-headings" import rehypeAutolinkHeadings from "rehype-autolink-headings"
import rehypeComponents from "rehype-components"; /* Render the custom directive content */
import rehypeKatex from "rehype-katex" import rehypeKatex from "rehype-katex"
import rehypeSlug from "rehype-slug" import rehypeSlug from "rehype-slug"
import remarkMath from "remark-math"
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"
import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs"
import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs"
import remarkDirective from "remark-directive" /* Handle directives */ import remarkDirective from "remark-directive" /* Handle directives */
import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-directives"; import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-directives";
import rehypeComponents from "rehype-components"; /* Render the custom directive content */ import remarkMath from "remark-math"
import svelte from "@astrojs/svelte" import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs"
import swup from '@swup/astro'; import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs"
import sitemap from '@astrojs/sitemap';
import {parseDirectiveNode} from "./src/plugins/remark-directive-rehype.js"; import {parseDirectiveNode} from "./src/plugins/remark-directive-rehype.js";
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"
const oklchToHex = (str) => { const oklchToHex = (str) => {
const DEFAULT_HUE = 250 const DEFAULT_HUE = 250

View File

@ -58,6 +58,7 @@
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.11.0", "@types/sanitize-html": "^2.11.0",
"photoswipe": "^5.4.4",
"remark-github-admonitions-to-directives": "^1.0.5", "remark-github-admonitions-to-directives": "^1.0.5",
"sass": "^1.77.8", "sass": "^1.77.8",
"stylus": "^0.63.0" "stylus": "^0.63.0"
@ -67,5 +68,6 @@
"vite-imagetools": "^6.2.7", "vite-imagetools": "^6.2.7",
"sharp": "^0.33.0" "sharp": "^0.33.0"
} }
} },
"packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
} }

View File

@ -142,6 +142,9 @@ importers:
'@types/sanitize-html': '@types/sanitize-html':
specifier: ^2.11.0 specifier: ^2.11.0
version: 2.11.0 version: 2.11.0
photoswipe:
specifier: ^5.4.4
version: 5.4.4
remark-github-admonitions-to-directives: remark-github-admonitions-to-directives:
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5 version: 1.0.5
@ -3514,6 +3517,10 @@ packages:
periscopic@3.1.0: periscopic@3.1.0:
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
photoswipe@5.4.4:
resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==}
engines: {node: '>= 0.12.0'}
picocolors@1.0.1: picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
@ -8761,6 +8768,8 @@ snapshots:
estree-walker: 3.0.3 estree-walker: 3.0.3
is-reference: 3.0.2 is-reference: 3.0.2
photoswipe@5.4.4: {}
picocolors@1.0.1: {} picocolors@1.0.1: {}
picomatch@2.3.1: {} picomatch@2.3.1: {}

8
src/global.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
import type { AstroIntegration } from '@swup/astro'
declare global {
interface Window {
// type from '@swup/astro' is incorrect
swup: AstroIntegration
}
}

View File

@ -1,53 +1,58 @@
--- ---
import GlobalStyles from "@components/GlobalStyles.astro"; import GlobalStyles from '@components/GlobalStyles.astro'
import '@fontsource/roboto/400.css'; import '@fontsource/roboto/400.css'
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css'
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css'
import ImageWrapper from "@components/misc/ImageWrapper.astro"; import ImageWrapper from '@components/misc/ImageWrapper.astro'
import ConfigCarrier from "@components/ConfigCarrier.astro"; import { profileConfig, siteConfig } from '@/config'
import {profileConfig, siteConfig} from "@/config"; import ConfigCarrier from '@components/ConfigCarrier.astro'
import {type Favicon} from "../types/config"; import {
import {defaultFavicons} from "../constants/icon"; AUTO_MODE,
import {LIGHT_MODE, DARK_MODE, AUTO_MODE, DEFAULT_THEME} from "../constants/constants"; DARK_MODE,
import {pathsEqual, url} from "../utils/url-utils"; DEFAULT_THEME,
LIGHT_MODE,
} from '../constants/constants'
import { defaultFavicons } from '../constants/icon'
import type { Favicon } from '../types/config'
import { url, pathsEqual } from '../utils/url-utils'
interface Props { interface Props {
title?: string; title?: string
banner?: string; banner?: string
description?: string; description?: string
} }
let { title, banner, description } = Astro.props; let { title, banner, description } = Astro.props
// apply a class to the body element to decide the height of the banner, only used for initial page load // 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 // 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 // so use Swup hooks instead to change the height immediately when a link is clicked
const isHomePage = pathsEqual(Astro.url.pathname, url('/')); const isHomePage = pathsEqual(Astro.url.pathname, url('/'))
// defines global css variables // defines global css variables
// why doing this in Layout instead of GlobalStyles: https://github.com/withastro/astro/issues/6728#issuecomment-1502203757 // why doing this in Layout instead of GlobalStyles: https://github.com/withastro/astro/issues/6728#issuecomment-1502203757
const configHue = siteConfig.themeColor.hue; const configHue = siteConfig.themeColor.hue
if (!banner || typeof banner !== 'string' || banner.trim() === '') { if (!banner || typeof banner !== 'string' || banner.trim() === '') {
banner = siteConfig.banner.src; banner = siteConfig.banner.src
} }
// TODO don't use post cover as banner for now // TODO don't use post cover as banner for now
banner = siteConfig.banner.src; banner = siteConfig.banner.src
const enableBanner = siteConfig.banner.enable; const enableBanner = siteConfig.banner.enable
let pageTitle; let pageTitle: string
if (title) { if (title) {
pageTitle = `${title} - ${siteConfig.title}`; pageTitle = `${title} - ${siteConfig.title}`
} else { } else {
pageTitle = `${siteConfig.title} - ${siteConfig.subtitle}`; pageTitle = `${siteConfig.title} - ${siteConfig.subtitle}`
} }
const favicons: Favicon[] = siteConfig.favicon.length > 0 ? siteConfig.favicon : defaultFavicons const favicons: Favicon[] =
siteConfig.favicon.length > 0 ? siteConfig.favicon : defaultFavicons
const siteLang = siteConfig.lang.replace('_', '-') const siteLang = siteConfig.lang.replace('_', '-')
--- ---
<!DOCTYPE html> <!DOCTYPE html>

View File

@ -1,23 +1,21 @@
--- ---
import Layout from "./Layout.astro"; import Footer from '@components/Footer.astro'
import Navbar from "@components/Navbar.astro"; import Navbar from '@components/Navbar.astro'
import SideBar from "@components/widget/SideBar.astro"; import BackToTop from '@components/control/BackToTop.astro'
import Footer from "@components/Footer.astro"; import SideBar from '@components/widget/SideBar.astro'
import BackToTop from "@components/control/BackToTop.astro"; import Layout from './Layout.astro'
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import { siteConfig } from "../config"; import { siteConfig } from '../config';
interface Props { interface Props {
title?: string; title?: string
banner?: string; banner?: string
description?: string; description?: string
} }
const { title, banner, description } = Astro.props const { title, banner, description } = Astro.props
const hasBannerCredit = siteConfig.banner.enable && siteConfig.banner.credit.enable const hasBannerCredit = siteConfig.banner.enable && siteConfig.banner.credit.enable
const hasBannerLink = !!(siteConfig.banner.credit.url) const hasBannerLink = !!(siteConfig.banner.credit.url)
--- ---
<Layout title={title} banner={banner} description={description}> <Layout title={title} banner={banner} description={description}>
@ -46,7 +44,8 @@ const hasBannerLink = !!(siteConfig.banner.credit.url)
<div id="content-wrapper" class="row-start-2 row-end-3 col-span-2 lg:col-span-1 overflow-hidden onload-animation"> <div id="content-wrapper" class="row-start-2 row-end-3 col-span-2 lg:col-span-1 overflow-hidden onload-animation">
<!-- the overflow-hidden here prevent long text break the layout--> <!-- the overflow-hidden here prevent long text break the layout-->
<main id="swup" class="transition-swup-fade"> <!-- make id different from windows.swup global property -->
<main id="swup-container" class="transition-swup-fade">
<slot></slot> <slot></slot>
</main> </main>

View File

@ -1,21 +1,20 @@
--- ---
import MainGridLayout from "../layouts/MainGridLayout.astro"; import type { GetStaticPaths } from 'astro'
import Pagination from "../components/control/Pagination.astro"; import PostPage from '../components/PostPage.astro'
import {getSortedPosts} from "../utils/content-utils"; import Pagination from '../components/control/Pagination.astro'
import {PAGE_SIZE} from "../constants/constants"; import { PAGE_SIZE } from '../constants/constants'
import PostPage from "../components/PostPage.astro"; import MainGridLayout from '../layouts/MainGridLayout.astro'
import {type GetStaticPaths} from "astro"; import { getSortedPosts } from '../utils/content-utils'
export const getStaticPaths = (async ({ paginate }) => { export const getStaticPaths = (async ({ paginate }) => {
const allBlogPosts = await getSortedPosts(); const allBlogPosts = await getSortedPosts()
return paginate(allBlogPosts, { pageSize: PAGE_SIZE }); return paginate(allBlogPosts, { pageSize: PAGE_SIZE })
}) satisfies GetStaticPaths }) satisfies GetStaticPaths
// https://github.com/withastro/astro/issues/6507#issuecomment-1489916992 // https://github.com/withastro/astro/issues/6507#issuecomment-1489916992
const {page} = Astro.props; const { page } = Astro.props
const len = page.data.length;
const len = page.data.length
--- ---
<MainGridLayout> <MainGridLayout>

View File

@ -1,48 +1,48 @@
--- ---
import { getCollection } from 'astro:content'; import path from 'node:path'
import MainGridLayout from "@layouts/MainGridLayout.astro"; import { getCollection } from 'astro:content'
import ImageWrapper from "../../components/misc/ImageWrapper.astro"; import License from '@components/misc/License.astro'
import {Icon} from "astro-icon/components"; import Markdown from '@components/misc/Markdown.astro'
import PostMetadata from "../../components/PostMeta.astro"; import I18nKey from '@i18n/i18nKey'
import {i18n} from "@i18n/translation"; import { i18n } from '@i18n/translation'
import I18nKey from "@i18n/i18nKey"; import MainGridLayout from '@layouts/MainGridLayout.astro'
import {getDir, getPostUrlBySlug} from "@utils/url-utils"; import { getDir, getPostUrlBySlug } from '@utils/url-utils'
import License from "@components/misc/License.astro"; import { Icon } from 'astro-icon/components'
import {licenseConfig} from "src/config"; import { licenseConfig } from 'src/config'
import Markdown from "@components/misc/Markdown.astro"; import PostMetadata from '../../components/PostMeta.astro'
import path from "path"; import ImageWrapper from '../../components/misc/ImageWrapper.astro'
import {profileConfig} from "../../config"; import { profileConfig } from '../../config'
import {formatDateToYYYYMMDD} from "../../utils/date-utils"; import { formatDateToYYYYMMDD } from '../../utils/date-utils'
export async function getStaticPaths() { export async function getStaticPaths() {
const blogEntries = await getCollection('posts', ({ data }) => { const blogEntries = await getCollection('posts', ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true; return import.meta.env.PROD ? data.draft !== true : true
}); })
return blogEntries.map(entry => ({ return blogEntries.map(entry => ({
params: { slug: entry.slug }, props: { entry }, params: { slug: entry.slug },
})); props: { entry },
}))
} }
const { entry } = Astro.props; const { entry } = Astro.props
const { Content } = await entry.render(); const { Content } = await entry.render()
const { remarkPluginFrontmatter } = await entry.render(); const { remarkPluginFrontmatter } = await entry.render()
const jsonLd = { const jsonLd = {
"@context": "https://schema.org", '@context': 'https://schema.org',
"@type": "BlogPosting", '@type': 'BlogPosting',
"headline": entry.data.title, headline: entry.data.title,
"description": entry.data.description || entry.data.title, description: entry.data.description || entry.data.title,
"keywords": entry.data.tags, keywords: entry.data.tags,
"author": { author: {
"@type": "Person", '@type': 'Person',
"name": profileConfig.name, name: profileConfig.name,
"url": Astro.site url: Astro.site,
}, },
"datePublished": formatDateToYYYYMMDD(entry.data.published), datePublished: formatDateToYYYYMMDD(entry.data.published),
// TODO include cover image here // TODO include cover image here
} }
--- ---
<MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description}> <MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description}>
<script is:inline slot="head" type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script> <script is:inline slot="head" type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script>
@ -137,5 +137,61 @@ const jsonLd = {
#post-container :nth-child(3) { animation-delay: calc(var(--content-delay) + 100ms) } #post-container :nth-child(3) { animation-delay: calc(var(--content-delay) + 100ms) }
#post-container :nth-child(4) { animation-delay: calc(var(--content-delay) + 175ms) } #post-container :nth-child(4) { animation-delay: calc(var(--content-delay) + 175ms) }
#post-container :nth-child(5) { animation-delay: calc(var(--content-delay) + 250ms) } #post-container :nth-child(5) { animation-delay: calc(var(--content-delay) + 250ms) }
#post-container :nth-child(6) { animation-delay: calc(var(--content-delay) + 325ms) } #post-container :nth-child(6) { animation-delay: calc(var(--content-delay) + 325ms) }
</style> </style>
<script>
import PhotoSwipeLightbox from "photoswipe/lightbox"
import "photoswipe/style.css"
let lightbox: PhotoSwipeLightbox
function createPhotoSwipe() {
lightbox = new PhotoSwipeLightbox({
gallery: "#post-container img",
pswpModule: () => import("photoswipe"),
})
lightbox.addFilter("domItemData", (itemData, element, linkEl) => {
if (element instanceof HTMLImageElement) {
itemData.src = element.src
itemData.w = Number(element.getAttribute("width"))
itemData.h = Number(element.getAttribute("height"))
itemData.msrc = element.src
}
return itemData
})
lightbox.init()
}
const setup = () => {
if (!lightbox) {
createPhotoSwipe()
}
window.swup.hooks.on("page:view", () => {
createPhotoSwipe()
})
window.swup.hooks.on(
"content:replace",
() => {
console.log("content:replace")
lightbox?.destroy?.()
},
{ before: true },
)
}
if (window.swup) {
setup()
} else {
document.addEventListener("swup:enable", setup)
}
</script>