mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-03-19 04:29:45 +08:00
♻️ 重构xml解析&处理,支持scene切换
This commit is contained in:
@@ -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>
|
||||
|
||||
4
src/types/Archive/Birth.d.ts
vendored
4
src/types/Archive/Birth.d.ts
vendored
@@ -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;
|
||||
/** 对话内容 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user