♻️ 重构xml解析&处理,支持scene切换

This commit is contained in:
BTMuli
2025-12-22 15:38:22 +08:00
parent bd8ee19b79
commit 35851532a5
4 changed files with 107 additions and 125 deletions

View File

@@ -1,37 +1,53 @@
<!-- 留影叙佳期浮窗 TODO 左右SLOT -->
<!-- 留影叙佳期浮窗 -->
<template>
<TOverlay v-model="visible" blur-val="5px">
<div class="pao-bc-container">
<slot name="left"></slot>
<div class="pao-bc-main">
<div v-if="props.data" class="pao-bc-cover">
<div v-if="props.data" class="pao-bc-main">
<div class="pao-bc-cover">
<TMiImg v-if="showText" :ori="true" :src="curScene.bg" alt="顶部图像" />
<TMiImg
v-if="props.choice"
v-else-if="props.choice"
:ori="true"
:src="props.data.take_picture[1]"
alt="顶部图像"
/>
<TMiImg v-else :ori="true" :src="props.data.take_picture[0]" alt="顶部图像" />
</div>
<div v-show="showText" class="pao-bc-comments">
<div v-for="(item, index) in textParse" :key="index" class="pao-bc-comment">
<div v-if="item.icon" :title="item.name" class="pao-bcc-icon">
<TMiImg :ori="true" :src="item.icon" alt="对白头像" />
<div v-if="showText" class="pao-bc-comments">
<!-- <div v-for="(scene, idx) in birthScenes" :key="idx" class="pao-bc-scene">-->
<div v-for="(item, index) in curScene.comments" :key="index" class="pao-bc-comment">
<div v-if="item.img" :title="item.role" class="pao-bcc-icon">
<template v-if="item.role === 'player'">
<TMiImg
:ori="true"
:src="props.choice ? item.img : (item.img2 ?? '')"
alt="对白头像"
/>
</template>
<TMiImg v-else :ori="true" :src="item.img" alt="对白头像" />
</div>
<div v-else-if="item.name !== '未知'" class="pao-bcc-name">
{{ item.name }}
<div v-else class="pao-bcc-name">
{{ item.role }}
</div>
<div :class="item.icon ? 'pao-bcc-text' : 'pao-bcc-quote'">
<div :class="item.img ? 'pao-bcc-text' : 'pao-bcc-quote'">
{{ item.text }}
</div>
</div>
<!-- </div>-->
</div>
<div class="pao-bc-top-tools">
<v-icon title="复制到剪贴板" @click="onCopy">mdi-content-copy</v-icon>
<v-icon title="下载到本地" @click="onDownload">mdi-download</v-icon>
<template v-if="showText && birthScenes && birthScenes.length > 0">
<v-icon title="上一幕" @click="switchScene(false)">mdi-arrow-left</v-icon>
</template>
<v-icon :title="showText ? '隐藏对白' : '显示对白'" @click="showComments()">
{{ showText ? "mdi-eye-off" : "mdi-eye" }}
</v-icon>
<template v-if="showText && birthScenes && birthScenes.length > 0">
<v-icon title="下一幕" @click="switchScene(true)">mdi-arrow-right</v-icon>
</template>
</div>
</div>
<slot name="right"></slot>
@@ -43,42 +59,40 @@ import TMiImg from "@comp/app/t-mi-img.vue";
import TOverlay from "@comp/app/t-overlay.vue";
import showSnackbar from "@comp/func/snackbar.js";
import { fetch } from "@tauri-apps/plugin-http";
import TGLogger from "@utils/TGLogger.js";
import { parseBirthGal, parseBirthSrc } from "@utils/birthParser.js";
import { copyToClipboard, getImageBuffer, saveCanvasImg } from "@utils/TGShare.js";
import { bytesToSize } from "@utils/toolFunc.js";
import { onMounted, ref, shallowRef, watch } from "vue";
import { xml2json } from "xml-js";
import { computed, onMounted, ref, shallowRef, watch } from "vue";
type ToArcBirthProps = { data?: TGApp.Archive.Birth.DrawItem; choice: boolean };
type XmlKeyMap = { id: string; rel: string; group?: string; icon: string };
type XmlTextList = { chara: string; img: string; text: string };
type XmlTextParse = { name: string; icon?: string; text: string };
const props = defineProps<ToArcBirthProps>();
const visible = defineModel<boolean>();
const showText = ref<boolean>(false);
const isLoad = ref<boolean>(false);
const sceneIdx = ref<number>(0);
const buffer = shallowRef<ArrayBuffer | null>(null);
const textParse = shallowRef<Array<XmlTextParse>>([]);
const birthSrc = shallowRef<TGApp.Archive.Birth.GalSrcFull | null>(null);
const birthScenes = shallowRef<TGApp.Archive.Birth.GalScenes>([]);
const curScene = computed<TGApp.Archive.Birth.GalScriptScene>(
() => birthScenes.value![sceneIdx.value],
);
onMounted(async () => await clearData());
watch(
() => [props.data, props.choice],
() => props.data,
async () => await clearData(),
);
watch(
() => showText.value,
async () => {
if (showText.value) await loadText();
else textParse.value = [];
if (showText.value && !isLoad.value) await loadText();
},
);
async function clearData(): Promise<void> {
buffer.value = null;
if (showText.value) {
textParse.value = [];
await loadText();
}
if (showText.value) await loadText();
}
async function onCopy(): Promise<void> {
@@ -103,91 +117,40 @@ function showComments(): void {
showText.value = !showText.value;
}
function switchScene(isNext: boolean): void {
if (sceneIdx.value < birthScenes.value.length - 1 && isNext) {
sceneIdx.value++;
} else if (sceneIdx.value > 0 && !isNext) {
sceneIdx.value--;
}
}
async function loadText(): Promise<void> {
if (!props.data) return;
if (textParse.value.length > 0) {
showText.value = !showText.value;
return;
}
const resSource: unknown = await parseXml(props.data.gal_resource);
if (resSource === false) {
showSnackbar.warn("对白数据加载失败");
return;
}
const keyMap = getKeyMap(resSource);
const resXml: unknown = await parseXml(props.data.gal_xml);
console.log(resXml);
const textList = getTextList(resXml);
console.log(textList);
textParse.value = textList.map((item) => {
const key = keyMap.find((keyItem) => keyItem.id === item.img);
if (!key) return { name: "未知", text: item.text };
return { name: key.group ?? key.id, text: item.text, icon: key.icon };
birthSrc.value = null;
birthScenes.value = [];
birthSrc.value = await loadXmlSrc(props.data.gal_resource);
birthScenes.value = await loadXmlGal(props.data.gal_xml);
}
async function loadXmlSrc(link: string): Promise<TGApp.Archive.Birth.GalSrcFull> {
console.log("srcLink", link);
const srcResp = await fetch(link, {
method: "GET",
headers: { "Content-Type": "text/xml" },
});
const srcRes = await srcResp.text();
return parseBirthSrc(new DOMParser().parseFromString(srcRes, "text/xml"));
}
function getKeyMap(resSource: unknown): Array<XmlKeyMap> {
const res: Array<XmlKeyMap> = [];
if (!resSource || typeof resSource !== "object") return res;
if (!("elements" in resSource) || !Array.isArray(resSource["elements"])) return res;
const arr1 = resSource.elements;
if (arr1.length === 0 || !("elements" in arr1[0]) || !Array.isArray(arr1[0].elements)) return res;
const arr2 = arr1[0].elements;
if (arr2.length === 0 || !("elements" in arr2[0]) || !Array.isArray(arr2[0].elements)) return res;
const arr3 = arr2[0].elements;
for (const item of arr3) {
if (!("name" in item)) continue;
if (!("attributes" in item)) continue;
const attr = item.attributes;
if (!("id" in attr) || !("rel" in attr) || !("group" in attr) || !("src" in attr)) continue;
if (item.name !== "chara") continue;
res.push({ id: attr.id, rel: attr.rel, group: attr.group, icon: attr.src });
}
return res;
}
// TODO: 重构文本解析||调整渲染方式
function getTextList(resXml: unknown): Array<XmlTextList> {
const res: Array<XmlTextList> = [];
if (!resXml || typeof resXml !== "object") return res;
if (!("elements" in resXml) || !Array.isArray(resXml["elements"])) return res;
const arr1 = resXml.elements;
if (arr1.length === 0 || !("elements" in arr1[0]) || !Array.isArray(arr1[0].elements)) return res;
const arr2 = arr1[0].elements;
if (arr2.length === 0 || !("elements" in arr2[0]) || !Array.isArray(arr2[0].elements)) return res;
const arr3 = arr2[0].elements;
if (arr3.length === 0 || !("elements" in arr3[0]) || !Array.isArray(arr3[0].elements)) return res;
const arr4 = arr3[0].elements;
for (const item of arr4) {
if (!("name" in item)) continue;
if (!("attributes" in item)) continue;
if (!("elements" in item) || !Array.isArray(item.elements)) continue;
const attr = item.attributes;
if (!("chara" in attr) || !("img" in attr)) continue;
if (item.name !== "simple_dialog") continue;
const img = props.choice ? attr.img : attr.img.replace("aether", "lumine");
let findText = "";
const arr5 = item.elements[0].elements;
if (arr5 && arr5.length > 0 && arr5[0].text !== "") findText = arr5[0].text;
else findText = item.elements[0].text;
res.push({ chara: attr.chara, img: img, text: findText });
}
return res;
}
async function parseXml(link: string): Promise<false | unknown> {
try {
const response = await fetch(link, { method: "GET" });
const data = await response.arrayBuffer();
return JSON.parse(xml2json(new TextDecoder("utf-8").decode(data)));
} catch (error) {
if (error instanceof Error) {
await TGLogger.Error(`[to-arcBirth] parseXml: ${error.message}`);
} else {
await TGLogger.Error(`[to-arcBirth] parseXml: 未知错误-${error}`);
}
return false;
}
async function loadXmlGal(link: string): Promise<TGApp.Archive.Birth.GalScenes> {
console.log("sceneLink", link);
const galResp = await fetch(link, {
method: "GET",
headers: { "Content-Type": "text/xml" },
});
const galRes = await galResp.text();
return parseBirthGal(new DOMParser().parseFromString(galRes, "text/xml"), birthSrc.value!);
}
</script>
<style lang="css" scoped>
@@ -293,11 +256,11 @@ async function parseXml(link: string): Promise<false | unknown> {
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
padding: 4px;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
background-color: var(--common-shadow-t-2);
border-bottom-right-radius: 5px;
border-top-left-radius: 5px;
border-bottom-right-radius: 4px;
border-top-left-radius: 4px;
}
</style>

View File

@@ -156,7 +156,7 @@ declare namespace TGApp.Archive.Birth {
/** 前一个场景ID */
prev?: string;
/** 场景脚本 */
scripts: Array<GalDialog>;
comments: Array<GalDialog>;
};
/**
@@ -170,6 +170,8 @@ declare namespace TGApp.Archive.Birth {
role?: string;
/** 图片名称 */
img?: string;
/** 图片2 */
img2?: string;
/** 位置 */
pos?: string;
/** 对话内容 */

View File

@@ -18,15 +18,13 @@ export function parseBirthSrc(data: Document): TGApp.Archive.Birth.GalSrcFull {
const child = resElements.children.item(ci);
if (child === null) continue;
const tmpResItem: TGApp.Archive.Birth.GalSrcRes = {
id: child.getAttribute("id") ?? "",
id: child.id ?? "",
group: child.getAttribute("group") ?? "",
type: child.tagName,
rel: child.getAttribute("rel") ?? "",
src: child.getAttribute("src") ?? "",
};
// 检测是否都是空
const isNotEmpty = Object.values(tmpResItem).every((v) => v !== "");
if (isNotEmpty) tmpRes.push(tmpResItem);
tmpRes.push(tmpResItem);
}
res.resource = tmpRes;
}
@@ -39,9 +37,7 @@ export function parseBirthSrc(data: Document): TGApp.Archive.Birth.GalSrcFull {
name: child.getAttribute("name") ?? "",
key: child.getAttribute("key") ?? undefined,
};
// 检测是否都是空
const isNotEmpty = Object.values(tmpRoleItem).every((v) => v !== "");
if (isNotEmpty) tmpRoles.push(tmpRoleItem);
tmpRoles.push(tmpRoleItem);
}
res.roles = tmpRoles;
}
@@ -51,14 +47,19 @@ export function parseBirthSrc(data: Document): TGApp.Archive.Birth.GalSrcFull {
/**
* 解析Gal数据
* @since Beta v0.9.1
* @todo 结合资源数据,补全图片路径
* @param {Document} data - XML数据
* @param {TGApp.Archive.Birth.GalSrcFull} src - 解析的资源数据
* @return {TGApp.Archive.Birth.GalScenes}
*/
export function parseBirthGal(data: Document): TGApp.Archive.Birth.GalScenes {
export function parseBirthGal(
data: Document,
src: TGApp.Archive.Birth.GalSrcFull,
): TGApp.Archive.Birth.GalScenes {
const scenes: TGApp.Archive.Birth.GalScenes = [];
const sceneElements = data.querySelectorAll("scene");
if (sceneElements.length > 1) console.log(sceneElements);
for (let si = 0; si < sceneElements.length; si++) {
const sceneData = parseBirthScenes(sceneElements.item(si));
const sceneData = parseBirthScenes(sceneElements.item(si), src);
scenes.push(sceneData);
}
return scenes;
@@ -67,30 +68,46 @@ export function parseBirthGal(data: Document): TGApp.Archive.Birth.GalScenes {
/**
* 解析场景数据
* @since Beta v0.9.1
* @param {Document} data - XML数据
* @param {TGApp.Archive.Birth.GalSrcFull} src - 解析的资源数据
* @return {TGApp.Archive.Birth.GalScriptScene}
*/
function parseBirthScenes(data: Element): TGApp.Archive.Birth.GalScriptScene {
function parseBirthScenes(
data: Element,
src: TGApp.Archive.Birth.GalSrcFull,
): TGApp.Archive.Birth.GalScriptScene {
const res: TGApp.Archive.Birth.GalScriptScene = {
id: data.getAttribute("id") ?? "",
title: data.getAttribute("title") ?? "",
prev: data.getAttribute("prev") ?? undefined,
bg: "",
scripts: [],
comments: [],
};
const bgElement = data.querySelector("bg");
if (bgElement !== null) res.bg = bgElement.getAttribute("img") ?? "";
if (bgElement !== null) {
const bgKey = bgElement.getAttribute("img") ?? "";
res.bg = src.resource.find((i) => i.id === bgKey)?.src ?? "";
}
const dialogElements = data.querySelectorAll("simple_dialog");
const tmpScripts: TGApp.Archive.Birth.GalDialog[] = [];
for (let di = 0; di < dialogElements.length; di++) {
const dialogEl = dialogElements.item(di);
const imgKey = dialogEl.getAttribute("img") ?? "";
let img2Src = "";
const imgSrc = src.resource.find((i) => i.id === imgKey)?.src ?? "";
if (imgKey.startsWith("aether")) {
img2Src = src.resource.find((i) => i.id === imgKey.replace("aether", "lumine"))?.src ?? "";
}
const scriptItem: TGApp.Archive.Birth.GalDialog = {
key: dialogEl.getAttribute("key") ?? "",
role: dialogEl.getAttribute("chara") ?? undefined,
img: dialogEl.getAttribute("img") ?? undefined,
img: imgSrc,
img2: img2Src,
pos: dialogEl.getAttribute("pos") ?? undefined,
text: dialogEl.textContent ?? "",
};
tmpScripts.push(scriptItem);
}
res.scripts = tmpScripts;
res.comments = tmpScripts;
return res;
}