对话解析&图片overlay

close #98
This commit is contained in:
目棃
2024-03-10 21:19:49 +08:00
parent 7de0b73e6d
commit 29b8bf84a6
4 changed files with 335 additions and 12 deletions

View File

@@ -0,0 +1,252 @@
<template>
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="5px">
<div class="toab-container" v-if="props.data">
<div class="toab-img">
<img :src="props.data.take_picture[Number(props.choice)]" alt="顶部图像" />
<div class="toab-dialog" v-show="showText">
<div v-for="(item, index) in textParse" :key="index" class="toab-dialog-item">
<div class="toab-dialog-item-icon" v-if="item.icon" :title="item.name">
<img :src="item.icon" alt="对白头像" />
</div>
<div v-else-if="item.name !== '未知'" class="toab-dialog-item-name">
{{ item.name }}
</div>
<div :class="item.icon ? 'toab-dialog-item-text' : 'toab-dialog-item-text-mini'">
{{ item.text }}
</div>
</div>
</div>
</div>
<div class="toab-top-tools">
<v-icon @click="onCopy" title="复制到剪贴板">mdi-content-copy</v-icon>
<v-icon @click="onDownload" title="下载到本地">mdi-download</v-icon>
<v-icon @click="loadText" :title="showText ? '隐藏对白' : '显示对白'">
{{ showText ? "mdi-eye-off" : "mdi-eye" }}
</v-icon>
</div>
</div>
</TOverlay>
</template>
<script setup lang="ts">
import { http } from "@tauri-apps/api";
import { ResponseType } from "@tauri-apps/api/http";
import { computed, onMounted, ref, watch } from "vue";
import { xml2json } from "xml-js";
import { copyToClipboard, getImageBuffer, saveCanvasImg } from "../../utils/TGShare";
import { bytesToSize } from "../../utils/toolFunc";
import showSnackbar from "../func/snackbar";
import TOverlay from "../main/t-overlay.vue";
interface ToArcBirthProps {
modelValue: boolean;
data?: TGApp.Archive.Birth.DrawItem;
choice: boolean;
}
interface ToArcBirthEmits {
(event: "update:modelValue", value: boolean): void;
}
const props = defineProps<ToArcBirthProps>();
const emits = defineEmits<ToArcBirthEmits>();
const buffer = ref<Uint8Array | null>(null);
const showText = ref(false);
const textParse = ref<Array<XmlTextParse>>([]);
const visible = computed({
get: () => props.modelValue,
set: (value) => {
emits("update:modelValue", value);
},
});
onMounted(loadData);
watch(() => props.data, loadData);
watch(() => props.choice, loadData);
interface XmlKeyMap {
id: string;
rel: string;
group?: string;
icon: string;
}
interface XmlTextList {
chara: string;
img: string;
text: string;
}
interface XmlTextParse {
name: string;
icon?: string;
text: string;
}
function loadData() {
buffer.value = null;
textParse.value = [];
showText.value = false;
}
async function onCopy(): Promise<void> {
if (!props.data) return;
const image = props.data.take_picture[Number(props.choice)];
if (buffer.value === null) buffer.value = await getImageBuffer(image);
const size = bytesToSize(buffer.value.byteLength);
await copyToClipboard(buffer.value);
showSnackbar({ text: `图片已复制到剪贴板,大小:${size}` });
}
async function onDownload() {
if (!props.data) return;
const image = props.data.take_picture[Number(props.choice)];
if (buffer.value === null) buffer.value = await getImageBuffer(image);
const size = bytesToSize(buffer.value.byteLength);
await saveCanvasImg(buffer.value, Date.now().toString());
showSnackbar({ text: `图片已下载到本地,大小:${size}` });
}
async function loadText(): Promise<void> {
if (!props.data) return;
if (textParse.value.length > 0) {
showText.value = !showText.value;
return;
}
const resSource: any = await parseXml(props.data.gal_resource);
const keyMap: XmlKeyMap[] = resSource["elements"][0]["elements"][0]["elements"]
.map((item: any) => {
if (item["name"] === "chara")
return <XmlKeyMap>{
id: item["attributes"]["id"],
rel: item["attributes"]["rel"],
group: item["attributes"]["group"],
icon: item["attributes"]["src"],
};
})
.filter((item: any) => item !== undefined);
const resXml = await parseXml(props.data.gal_xml);
const textList: XmlTextList[] = resXml["elements"][0]["elements"][0]["elements"][0]["elements"]
.map((item: any) => {
if (item["name"] === "simple_dialog") {
let img = item["attributes"]["img"];
if (!props.choice && img) img = img.replace("aether", "lumine");
return <XmlTextList>{
chara: item["attributes"]["chara"],
img: img,
text: item["elements"][0]["text"],
};
}
})
.filter((item: any) => item !== undefined);
textParse.value = textList.map((item: XmlTextList) => {
const key = keyMap.find((keyItem: XmlKeyMap) => keyItem.id === item.img);
if (!key) {
return {
name: "未知",
text: item.text,
};
}
return {
name: key.group ?? key.id,
text: item.text,
icon: key.icon,
};
});
showText.value = true;
}
async function parseXml(link: string) {
const res = await http.fetch<string>(link, {
method: "GET",
responseType: ResponseType.Text,
});
return JSON.parse(xml2json(res.data));
}
function onCancel() {
visible.value = false;
}
</script>
<style lang="css" scoped>
.toab-container {
position: relative;
display: flex;
width: 50vw;
align-items: center;
justify-content: center;
}
.toab-img {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
}
.toab-img img {
width: 100%;
max-width: 100%;
height: auto;
}
.toab-top-tools {
position: absolute;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
background-color: var(--common-shadow-t-2);
}
.toab-dialog {
position: absolute;
bottom: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
max-height: 40vh;
flex-direction: column;
align-items: center;
justify-content: flex-start;
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
background: var(--common-shadow-t-2);
color: var(--box-text-1);
overflow-y: auto;
}
.toab-dialog-item {
display: flex;
width: 100%;
align-items: center;
justify-content: flex-start;
}
.toab-dialog-item-icon {
width: 50px;
min-width: 50px;
}
.toab-dialog-item-name {
font-size: 1.5rem;
font-weight: bold;
}
.toab-dialog-item-text {
font-size: 1.2rem;
word-break: break-all;
}
.toab-dialog-item-text-mini {
margin-left: 3rem;
font-size: 1rem;
opacity: 0.8;
word-break: break-all;
}
</style>

View File

@@ -1,29 +1,37 @@
<template>
<div class="ab-container">
<div class="ab-draw-top">
<v-switch v-model="isLumine" :label="isLumine ? '' : ''" />
<v-switch v-model="isAether" :label="isAether ? '' : ''" />
</div>
<div class="ab-draw-grid">
<div v-for="item in selectedItem" :key="item.op_id" class="ab-draw-item">
<v-img
:src="item.take_picture[isLumine ? 0 : 1]"
:lazy-src="item.unread_picture[isLumine ? 0 : 1]"
/>
<span>{{ item.year }} {{ item.birthday }} {{ item.role_name }}</span>
<div v-for="item in selectedItem" :key="item.op_id" class="ab-draw">
<div class="ab-draw-cover" @click="showImg(item)">
<img
:src="item.take_picture[Number(isAether)]"
:data-src="item.unread_picture[Number(isAether)]"
:alt="item.word_text"
/>
</div>
<div class="ab-di-info">{{ item.year }} {{ item.birthday }} {{ item.role_name }}</div>
<div class="ab-di-text" :title="item.word_text">{{ item.word_text }}</div>
</div>
</div>
<v-pagination v-model="page" :length="length" />
</div>
<ToArcBrith v-model="showOverlay" :data="current" :choice="isAether" />
</template>
<script lang="ts" setup>
import { onMounted, ref, watch, watchEffect } from "vue";
import ToArcBrith from "../../components/overlay/to-arcBrith.vue";
import { ArcBirDraw } from "../../data";
const page = ref(1);
const length = ref(0);
const selectedItem = ref<TGApp.Archive.Birth.DrawItem[]>(ArcBirDraw);
const isLumine = ref<boolean>(true);
const current = ref<TGApp.Archive.Birth.DrawItem>();
const isAether = ref<boolean>(false);
const showOverlay = ref(false);
watch(page, (val) => {
const start = (val - 1) * 12;
@@ -38,6 +46,11 @@ watchEffect(() => {
onMounted(() => {
length.value = Math.ceil(ArcBirDraw.length / 12);
});
function showImg(item: TGApp.Archive.Birth.DrawItem) {
current.value = item;
showOverlay.value = true;
}
</script>
<style lang="css" scoped>
.ab-container {
@@ -59,11 +72,54 @@ onMounted(() => {
display: grid;
width: 100%;
height: 100%;
column-gap: 10px;
gap: 10px;
grid-template-columns: repeat(4, 1fr);
}
.ab-draw-item {
text-align: center;
.ab-draw {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
.ab-draw-cover {
overflow: hidden;
width: 100%;
max-width: 100%;
border-radius: 5px;
aspect-ratio: 125 / 54;
}
.ab-draw-cover img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
transition: all 0.3s ease-in-out;
}
.ab-draw-cover img:hover {
overflow: hidden;
cursor: pointer;
transform: scale(1.1);
transition: all 0.3s ease-in-out;
}
.ab-di-info {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
}
.ab-di-text {
width: fit-content;
max-width: 100%;
margin-right: auto;
margin-left: auto;
font-size: 14px;
line-height: 1.5;
opacity: 0.8;
}
</style>