From e3d62701e10441a236c9e5e3a65e437bb3bfa453 Mon Sep 17 00:00:00 2001 From: ChangeSuger Date: Sun, 17 May 2026 21:46:57 +0800 Subject: [PATCH 1/2] feat: support series argument for unlockCg --- .../src/Core/gameScripts/changeBg/index.ts | 3 +- .../webgal/src/Core/gameScripts/unlockCg.ts | 7 +- packages/webgal/src/UI/Extra/ExtraCg.tsx | 62 +++++++++++++--- .../webgal/src/UI/Extra/ExtraCgElement.tsx | 70 +++++++++++++------ .../webgal/src/store/userDataInterface.ts | 1 + packages/webgal/src/store/userDataReducer.ts | 3 +- 6 files changed, 112 insertions(+), 34 deletions(-) diff --git a/packages/webgal/src/Core/gameScripts/changeBg/index.ts b/packages/webgal/src/Core/gameScripts/changeBg/index.ts index cfcc01b7b..ad30b3c57 100644 --- a/packages/webgal/src/Core/gameScripts/changeBg/index.ts +++ b/packages/webgal/src/Core/gameScripts/changeBg/index.ts @@ -25,6 +25,7 @@ export const changeBg = (sentence: ISentence): IPerform => { const url = sentence.content; const unlockName = getStringArgByKey(sentence, 'unlockname') ?? ''; const series = getStringArgByKey(sentence, 'series') ?? 'default'; + const order = getNumberArgByKey(sentence, 'order') ?? 0; const transformString = getStringArgByKey(sentence, 'transform'); let duration = getNumberArgByKey(sentence, 'duration') ?? DEFAULT_BG_OUT_DURATION; const enterDuration = getNumberArgByKey(sentence, 'enterDuration') ?? duration; @@ -34,7 +35,7 @@ export const changeBg = (sentence: ISentence): IPerform => { const dispatch = webgalStore.dispatch; if (unlockName !== '') { - dispatch(unlockCgInUserData({ name: unlockName, url, series })); + dispatch(unlockCgInUserData({ name: unlockName, url, series, order })); const userDataState = webgalStore.getState().userData; localforage.setItem(WebGAL.gameKey, userDataState).then(() => {}); } diff --git a/packages/webgal/src/Core/gameScripts/unlockCg.ts b/packages/webgal/src/Core/gameScripts/unlockCg.ts index 3d8cca97d..9dcf694be 100644 --- a/packages/webgal/src/Core/gameScripts/unlockCg.ts +++ b/packages/webgal/src/Core/gameScripts/unlockCg.ts @@ -6,7 +6,7 @@ import { logger } from '@/Core/util/logger'; import localforage from 'localforage'; import { WebGAL } from '@/Core/WebGAL'; -import { getStringArgByKey } from '../util/getSentenceArg'; +import { getStringArgByKey, getNumberArgByKey } from '../util/getSentenceArg'; /** * 解锁cg @@ -16,8 +16,9 @@ export const unlockCg = (sentence: ISentence): IPerform => { const url = sentence.content; const name = getStringArgByKey(sentence, 'name') ?? sentence.content; const series = getStringArgByKey(sentence, 'series') ?? 'default'; - logger.info(`解锁CG:${name},路径:${url},所属系列:${series}`); - webgalStore.dispatch(unlockCgInUserData({ name, url, series })); + const order = getNumberArgByKey(sentence, 'order') ?? 0; + logger.info(`解锁CG:${name},路径:${url},所属系列:${series},排序:${order}`); + webgalStore.dispatch(unlockCgInUserData({ name, url, series, order })); const userDataState = webgalStore.getState().userData; localforage.setItem(WebGAL.gameKey, userDataState).then(() => {}); return createNonePerform(); diff --git a/packages/webgal/src/UI/Extra/ExtraCg.tsx b/packages/webgal/src/UI/Extra/ExtraCg.tsx index 8f610ebc7..698f920fa 100644 --- a/packages/webgal/src/UI/Extra/ExtraCg.tsx +++ b/packages/webgal/src/UI/Extra/ExtraCg.tsx @@ -1,23 +1,30 @@ import styles from '@/UI/Extra/extra.module.scss'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { RootState } from '@/store/store'; import { useValue } from '@/hooks/useValue'; import './extraCG_animation_List.scss'; -import { ExtraCgElement } from '@/UI/Extra/ExtraCgElement'; +import { ExtraCgElement, isVideoFile } from '@/UI/Extra/ExtraCgElement'; import useSoundEffect from '@/hooks/useSoundEffect'; +import { IAppreciationAsset } from '@/store/userDataInterface'; + +interface IExtraCgDisplayItem { + key: string; + name: string; + resources: IAppreciationAsset[]; +} export function ExtraCg() { const cgPerPage = 8; const extraState = useSelector((state: RootState) => state.userData.appreciationData); - const pageNumber = Math.ceil(extraState.cg.length / cgPerPage); - // const pageNumber = 10; + const groupedCgList = useMemo(() => buildGroupedCgList(extraState.cg), [extraState.cg]); + const pageNumber = Math.ceil(groupedCgList.length / cgPerPage); const currentPage = useValue(1); const { playSeEnter, playSeClick } = useSoundEffect(); // 开始生成立绘鉴赏的图片 const showCgList = []; - const len = extraState.cg.length; + const len = groupedCgList.length; for ( let i = (currentPage.value - 1) * cgPerPage; i < Math.min(len, (currentPage.value - 1) * cgPerPage + cgPerPage); @@ -27,11 +34,11 @@ export function ExtraCg() { const deg = Random(-5, 5); const temp = ( ); showCgList.push(temp); @@ -73,3 +80,42 @@ export function ExtraCg() { function Random(min: number, max: number) { return Math.round(Math.random() * (max - min)) + min; } + +function buildGroupedCgList(cgList: IAppreciationAsset[]): IExtraCgDisplayItem[] { + const groupedCgList: IExtraCgDisplayItem[] = []; + const seriesIndexMap = new Map(); + + cgList.forEach((cg, index) => { + // 视频不进行分组 + if (cg.series !== 'default' && !isVideoFile(cg.url)) { + const groupedIndex = seriesIndexMap.get(cg.series); + if (groupedIndex !== undefined) { + groupedCgList[groupedIndex].resources.push(cg); + return; + } + + seriesIndexMap.set(cg.series, groupedCgList.length); + groupedCgList.push({ + key: `series:${cg.series}`, + name: cg.name, + resources: [cg], + }); + return; + } + + groupedCgList.push({ + key: `default:${index}:${cg.url}`, + name: cg.name, + resources: [cg], + }); + }); + + return groupedCgList.map((groupedCg) => ({ + ...groupedCg, + resources: groupedCg.resources.length > 1 ? [...groupedCg.resources].sort(sortCgByOrder) : groupedCg.resources, + })); +} + +function sortCgByOrder(prev: IAppreciationAsset, next: IAppreciationAsset) { + return (prev.order ?? 0) - (next.order ?? 0); +} diff --git a/packages/webgal/src/UI/Extra/ExtraCgElement.tsx b/packages/webgal/src/UI/Extra/ExtraCgElement.tsx index 63a36287c..671e09399 100644 --- a/packages/webgal/src/UI/Extra/ExtraCgElement.tsx +++ b/packages/webgal/src/UI/Extra/ExtraCgElement.tsx @@ -1,37 +1,45 @@ import { useValue } from '@/hooks/useValue'; import styles from '@/UI/Extra/extra.module.scss'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import useSoundEffect from '@/hooks/useSoundEffect'; +import { IAppreciationAsset } from '@/store/userDataInterface'; + +export const isVideoFile = (url: string) => { + const extension = url.split('.').pop()?.toLowerCase() || ''; + return ['mp4', 'webm', 'mkv'].includes(extension); +} interface IProps { name: string; - resourceUrl: string; + resources: IAppreciationAsset[]; transformDeg: number; index: number; } export function ExtraCgElement(props: IProps) { const showFull = useValue(false); + const [currentResourceIndex, setCurrentResourceIndex] = useState(0); const { playSeEnter, playSeClick } = useSoundEffect(); + const previewResource = props.resources[0]; + const currentResource = props.resources[currentResourceIndex] ?? previewResource; // Determine if the resource is a video based on file extension const isVideo = useMemo(() => { - const extension = props.resourceUrl.split('.').pop()?.toLowerCase() || ''; - return ['mp4', 'webm', 'mkv'].includes(extension); - }, [props.resourceUrl]); + return isVideoFile(previewResource.url); + }, [previewResource.url]); // Determine if the resource is an image based on file extension const isImage = useMemo(() => { - const extension = props.resourceUrl.split('.').pop()?.toLowerCase() || ''; + const extension = previewResource.url.split('.').pop()?.toLowerCase() || ''; return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(extension); - }, [props.resourceUrl]); + }, [previewResource.url]); // Render media content based on resource type - const renderMedia = (fullScreen: boolean) => { + const renderMedia = (resourceUrl: string) => { if (isVideo) { return (