Merge pull request #2249 from tangly1024/feat/theme-game-pwa

game 主题支持PWA
This commit is contained in:
tangly1024
2024-04-01 13:20:24 +08:00
committed by GitHub
8 changed files with 143 additions and 39 deletions

80
components/PWA.js Normal file
View File

@@ -0,0 +1,80 @@
import { compressImage } from '@/lib/notion/mapImage'
import { isBrowser } from '../lib/utils'
/**
* 初始化PWA
* @see https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps
* @param {*} props
* @returns
*/
export function PWA(post, siteInfo) {
if (!isBrowser || !post) {
return
}
// 将 manifest 数据转换为 JSON 字符串
const manifestData = {
id: post?.id,
name: post?.title + ' | ' + siteInfo.title,
short_name: post?.title,
description: post?.summary || siteInfo.description,
icons: [
{
src: compressImage(post?.pageCoverThumbnail, 192),
type: 'image/png',
sizes: '192x192'
}
],
form_factor: 'phone',
start_url: window.location.href,
scope: window.location.href,
display: 'standalone',
background_color: '#181818',
theme_color: '#181818'
}
// 删除已有的 manifest link 元素(如果存在)
const existingManifest = document.querySelector('link[rel="manifest"]')
if (existingManifest) {
existingManifest.parentNode.removeChild(existingManifest)
}
// 创建 manifest 元素
const manifest = document.createElement('link')
manifest.rel = 'manifest'
// 设置 manifest 的 href 为一个 Blob URL
const blobUrl = URL.createObjectURL(
new Blob([JSON.stringify(manifestData)], {
type: 'application/json'
})
)
// 这里会报错因为前端收到的事一个转义了双引号的字符串无法解析成json不知道怎么解决
manifest.href = blobUrl
// 将 manifest 添加到 head 中
document.head.appendChild(manifest)
// 不要忘记在适当的时候释放 Blob URL避免内存泄漏
// 例如,在页面卸载或不再需要该 Blob URL 时
window.addEventListener('unload', () => {
URL.revokeObjectURL(blobUrl)
})
}
/**
* 截去url结尾的 / 便于和slug拼接
* @param {*} str
* @returns
*/
// function getRootPath() {
// const protocol = window.location.protocol
// const hostname = window.location.hostname
// const port = window.location.port
// // 如果端口号存在且不是默认的80或443则包含端口号
// if (port && port !== '80' && port !== '443') {
// return protocol + '//' + hostname + ':' + port
// } else {
// return protocol + '//' + hostname
// }
// }

View File

@@ -225,7 +225,7 @@ function getCategoryOptions(schema) {
* @param from
* @returns {Promise<{title,description,pageCover,icon}>}
*/
function getSiteInfo({ collection, block }) {
function getSiteInfo({ collection, block, NOTION_CONFIG }) {
const title = collection?.name?.[0][0] || BLOG.TITLE
const description = collection?.description
? Object.assign(collection).description[0][0]
@@ -233,19 +233,21 @@ function getSiteInfo({ collection, block }) {
const pageCover = collection?.cover
? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value)
: BLOG.HOME_BANNER_IMAGE
let icon = collection?.icon
? mapImgUrl(collection?.icon, collection, 'collection')
: BLOG.AVATAR
// 用户头像压缩一下
icon = compressImage(icon)
let icon = compressImage(
collection?.icon
? mapImgUrl(collection?.icon, collection, 'collection')
: BLOG.AVATAR
)
// 站点网址
const link = NOTION_CONFIG?.LINK || BLOG.LINK
// 站点图标不能是emoji
// 站点图标不能是emoji
const emojiPattern = /\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g
if (!icon || emojiPattern.test(icon)) {
icon = BLOG.AVATAR
}
return { title, description, pageCover, icon }
return { title, description, pageCover, icon, link }
}
/**
@@ -355,7 +357,6 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
return EmptyData(pageId)
}
const collection = Object.values(pageRecordMap.collection)[0]?.value || {}
const siteInfo = getSiteInfo({ collection, block })
const collectionId = rawMetadata?.collection_id
const collectionQuery = pageRecordMap.collection_query
const collectionView = pageRecordMap.collection_view
@@ -422,6 +423,11 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
// 文章计数
let postCount = 0
// 站点配置优先读取配置表格否则读取blog.config.js 文件
const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {}
const siteInfo = getSiteInfo({ collection, block })
// 查找所有的Post和Page
const allPages = collectionData.filter(post => {
if (post?.type === 'Post' && post.status === 'Published') {
@@ -435,9 +441,6 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
)
})
// 站点配置优先读取配置表格否则读取blog.config.js 文件
const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {}
// Sort by date
if (BLOG.POSTS_SORT_BY === 'date') {
allPages.sort((a, b) => {

View File

@@ -1,5 +1,5 @@
// 封装异步加载资源的方法
import { memo } from 'react';
import { memo } from 'react'
/**
* 判断是否客户端
@@ -9,18 +9,18 @@ export const isBrowser = typeof window !== 'undefined'
/**
* 打乱数组
* @param {*} array
* @returns
* @param {*} array
* @returns
*/
export const shuffleArray = (array) => {
if (!array) {
return []
}
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
export const shuffleArray = array => {
if (!array) {
return []
}
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[array[i], array[j]] = [array[j], array[i]]
}
return array
}
/**

View File

@@ -96,19 +96,27 @@ module.exports = withBundleAnalyzer({
if (!isServer) {
console.log('[加载主题]', path.resolve(__dirname, 'themes', THEME))
}
config.resolve.alias['@theme-components'] = path.resolve(__dirname, 'themes', THEME)
config.resolve.alias['@theme-components'] = path.resolve(
__dirname,
'themes',
THEME
)
return config
},
experimental: {
scrollRestoration: true
},
exportPathMap: async function (defaultPathMap, { dev, dir, outDir, distDir, buildId }) {
// 导出时 忽略/pages/sitemap.xml.js 否则报错getServerSideProps
exportPathMap: async function (
defaultPathMap,
{ dev, dir, outDir, distDir, buildId }
) {
// export 静态导出时 忽略/pages/sitemap.xml.js 否则和getServerSideProps这个动态文件冲突
const pages = { ...defaultPathMap }
delete pages['/sitemap.xml']
return pages
},
publicRuntimeConfig: { // 这里的配置既可以服务端获取到,也可以在浏览器端获取到
publicRuntimeConfig: {
// 这里的配置既可以服务端获取到,也可以在浏览器端获取到
NODE_ENV_API: process.env.NODE_ENV_API || 'prod',
THEMES: themes
}

View File

@@ -13,7 +13,10 @@ import { useRouter } from 'next/router'
*/
const Index = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
@@ -25,7 +28,9 @@ export async function getStaticProps() {
const from = 'index'
const props = await getGlobalData({ from })
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
props.posts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
// 处理分页
if (siteConfig('POST_LIST_STYLE') === 'scroll') {
@@ -41,7 +46,11 @@ export async function getStaticProps() {
if (post.password && post.password !== '') {
continue
}
post.blockMap = await getPostBlocks(post.id, 'slug', siteConfig('POST_PREVIEW_LINES'))
post.blockMap = await getPostBlocks(
post.id,
'slug',
siteConfig('POST_PREVIEW_LINES')
)
}
}

View File

@@ -4,7 +4,7 @@
* 全屏按钮
* @returns
*/
export default function FullScreen() {
export default function FullScreenButton() {
function toggleFullScreen() {
// window.scrollTo(0, 2)
document?.querySelector('#game-wrapper')?.scrollIntoView({

View File

@@ -37,6 +37,7 @@ export const GameListIndexCombine = ({ posts }) => {
// 试图将4合一卡组塞满
while (gamesClone?.length > 0 && groupItems.length < 4) {
const item = gamesClone.shift()
index++
if (
item.tags?.some(
t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG)
@@ -59,6 +60,7 @@ export const GameListIndexCombine = ({ posts }) => {
// 剩余的4合一不满4个的给他放大卡
while (groupItems.length > 0) {
const item = groupItems.shift()
index++
components.push(
<GameItem key={index++} item={item} isLargeCard={true} />
)

View File

@@ -4,6 +4,7 @@ import { Draggable } from '@/components/Draggable'
import { AdSlot } from '@/components/GoogleAdsense'
import replaceSearchResult from '@/components/Mark'
import NotionPage from '@/components/NotionPage'
import { PWA as initialPWA } from '@/components/PWA'
import ShareBar from '@/components/ShareBar'
import { siteConfig } from '@/lib/config'
import { loadWowJS } from '@/lib/plugins/wow'
@@ -17,7 +18,7 @@ import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import BlogPostBar from './components/BlogPostBar'
import { Footer } from './components/Footer'
import FullScreen from './components/FullScreen'
import FullScreenButton from './components/FullScreenButton'
import { GameListIndexCombine } from './components/GameListIndexCombine'
import { GameListRelate } from './components/GameListRealate'
import { GameListRecent } from './components/GameListRecent'
@@ -45,9 +46,6 @@ export const useGameGlobal = () => useContext(ThemeGlobalGame)
*/
const LayoutBase = props => {
const { allNavPages, children } = props
// const fullWidth = post?.fullWidth ?? false
// const { onLoading } = useGlobal()
const searchModal = useRef(null)
// 在列表中进行实时过滤
const [filterKey, setFilterKey] = useState('')
@@ -279,13 +277,17 @@ const LayoutArchive = props => {
* @returns
*/
const LayoutSlug = props => {
const { post, allNavPages, recommendPosts, lock, validPassword } = props
const { post, siteInfo, allNavPages, recommendPosts, lock, validPassword } =
props
const game = deepClone(post)
const [loading, setLoading] = useState(true)
// const [url, setUrl] = useState(game?.ext?.href)
const relateGames = recommendPosts
const randomGames = shuffleArray(deepClone(allNavPages))
// 初始化可安装应用
initialPWA(game, siteInfo)
// 将当前游戏加入到最近游玩
useEffect(() => {
// 更新最新游戏
@@ -403,8 +405,8 @@ const LayoutSlug = props => {
{/* 游戏窗口装饰器 */}
{game && !loading && (
<div className='game-decorator bg-[#0B0D14] right-0 bottom-0 flex justify-center h-12 md:w-12 z-10 md:absolute'>
{/* 加入全屏按钮 */}
<FullScreen />
{/* 全屏按钮 */}
<FullScreenButton />
</div>
)}
</div>