mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-04-01 06:39:45 +08:00
✨ 重构UIGF导入导出备份恢复,支持UIGF4.2
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* UIGF工具类
|
||||
* @since Beta v0.9.2
|
||||
* @since Beta v0.9.5
|
||||
*/
|
||||
|
||||
import showLoading from "@comp/func/loading.js";
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import TSUserGacha from "@Sqlm/userGacha.js";
|
||||
import TSUserGachaB from "@Sqlm/userGachaB.js";
|
||||
import { app, path } from "@tauri-apps/api";
|
||||
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { exists, mkdir, readDir, readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { Ajv } from "ajv";
|
||||
import type { ErrorObject } from "ajv/lib/types/index.js";
|
||||
|
||||
@@ -64,9 +65,9 @@ async function getUigfHeader(uid: string, timezone: number): Promise<TGApp.Plugi
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 UIGF v4.1 头部信息
|
||||
* @since Beta v0.7.10
|
||||
* @returns UIGF v4.1 头部信息
|
||||
* 获取 UIGF v4.2 头部信息
|
||||
* @since Beta v0.9.5
|
||||
* @returns UIGF v4.2 头部信息
|
||||
*/
|
||||
export async function getUigf4Header(): Promise<TGApp.Plugins.UIGF.Info4> {
|
||||
const stamp = Date.now();
|
||||
@@ -74,40 +75,62 @@ export async function getUigf4Header(): Promise<TGApp.Plugins.UIGF.Info4> {
|
||||
export_timestamp: Math.floor(stamp / 1000).toString(),
|
||||
export_app: "TeyvatGuide",
|
||||
export_app_version: await app.getVersion(),
|
||||
version: "v4.1",
|
||||
version: "v4.2",
|
||||
lang: "zh-cn",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据转换-数据库到 UIGF
|
||||
* @since Beta v0.7.5
|
||||
* @param data - 数据库数据
|
||||
* 数据库祈愿数据转换为UIGF祈愿数据
|
||||
* @since Beta v0.9.5
|
||||
* @param data - 数据库祈愿数据
|
||||
* @param timezone - 时区
|
||||
* @returns UIGF 数据
|
||||
* @returns UIGF 祈愿数据
|
||||
*/
|
||||
function convertDataToUigf(
|
||||
function convertHk4e2Uigf(
|
||||
data: Array<TGApp.Sqlite.Gacha.Gacha>,
|
||||
timezone: number,
|
||||
): Array<TGApp.Plugins.UIGF.GachaItem> {
|
||||
return data.map((gacha) => {
|
||||
return {
|
||||
gacha_type: gacha.gachaType,
|
||||
item_id: gacha.itemId,
|
||||
count: gacha.count,
|
||||
time: getExportTime(gacha.time, timezone),
|
||||
name: gacha.name,
|
||||
item_type: gacha.type,
|
||||
rank_type: gacha.rank,
|
||||
id: gacha.id,
|
||||
uigf_gacha_type: gacha.uigfType,
|
||||
};
|
||||
});
|
||||
return data.map((gacha) => ({
|
||||
gacha_type: gacha.gachaType,
|
||||
item_id: gacha.itemId,
|
||||
count: gacha.count,
|
||||
time: getExportTime(gacha.time, timezone),
|
||||
name: gacha.name,
|
||||
item_type: gacha.type,
|
||||
rank_type: gacha.rank,
|
||||
id: gacha.id,
|
||||
uigf_gacha_type: gacha.uigfType,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库颂愿数据转换为UIGF颂愿数据
|
||||
* @since Beta v0.9.5
|
||||
* @param data - 数据库颂愿数据
|
||||
* @param timezone - 时区
|
||||
* @returns UIGF 颂愿数据
|
||||
*/
|
||||
function convertUgc2Uigf(
|
||||
data: Array<TGApp.Sqlite.Gacha.GachaB>,
|
||||
timezone: number,
|
||||
): Array<TGApp.Plugins.UIGF.GachaItemB> {
|
||||
return data.map((gacha) => ({
|
||||
id: gacha.id,
|
||||
schedule_id: gacha.scheduleId,
|
||||
item_type: gacha.type,
|
||||
item_id: gacha.itemId,
|
||||
item_name: gacha.name,
|
||||
rank_type: gacha.rank,
|
||||
time: getExportTime(gacha.time, timezone),
|
||||
op_gacha_type: gacha.opGachaType,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否是v4版本的UIGF
|
||||
* @since Beta v0.9.2
|
||||
* @since Beta v0.9.5
|
||||
* @remarks 祈愿&颂愿数据需要进一步确定
|
||||
* @param path - UIGF 文件路径
|
||||
* @returns 是否是v4,null表示数据异常
|
||||
*/
|
||||
@@ -180,7 +203,7 @@ function validateUigfData(data: object): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 UIGF v4.0 数据
|
||||
* 验证 UIGF v4.2 数据
|
||||
* @since Beta v0.5.0
|
||||
* @param data - UIGF 数据
|
||||
* @returns 是否验证通过
|
||||
@@ -197,29 +220,31 @@ function validateUigf4Data(data: object): boolean {
|
||||
|
||||
/**
|
||||
* 读取 UIGF 数据
|
||||
* @since Beta v0.5.0
|
||||
* @since Beta v0.9.5
|
||||
* @todo 重构
|
||||
* @param userPath - UIGF 数据路径
|
||||
* @returns UIGF 数据
|
||||
*/
|
||||
export async function readUigfData(userPath: string): Promise<TGApp.Plugins.UIGF.Schema> {
|
||||
const fileData: string = await readTextFile(userPath);
|
||||
return JSON.parse(fileData) satisfies TGApp.Plugins.UIGF.Schema;
|
||||
return <TGApp.Plugins.UIGF.Schema>JSON.parse(fileData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 UIGF 4.0 数据
|
||||
* @since Beta v0.5.0
|
||||
* @since Beta v0.9.5
|
||||
* @todo 重构
|
||||
* @param userPath - UIGF 数据路径
|
||||
* @returns UIGF 数据
|
||||
*/
|
||||
export async function readUigf4Data(userPath: string): Promise<TGApp.Plugins.UIGF.Schema4> {
|
||||
const fileData: string = await readTextFile(userPath);
|
||||
return JSON.parse(fileData) satisfies TGApp.Plugins.UIGF.Schema4;
|
||||
return <TGApp.Plugins.UIGF.Schema4>JSON.parse(fileData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 UIGF 数据
|
||||
* @since Beta v0.7.5
|
||||
* @since Beta v0.9.5
|
||||
* @param uid - UID
|
||||
* @param gachaList - 祈愿列表
|
||||
* @param savePath - 保存路径
|
||||
@@ -233,29 +258,150 @@ export async function exportUigfData(
|
||||
const timezone = getUigfTimeZone(uid);
|
||||
const UigfData = {
|
||||
info: await getUigfHeader(uid, timezone),
|
||||
list: convertDataToUigf(gachaList, timezone),
|
||||
list: convertHk4e2Uigf(gachaList, timezone),
|
||||
};
|
||||
const filePath = savePath ?? `${await path.appLocalDataDir()}userData\\UIGF_${uid}.json`;
|
||||
const filePath =
|
||||
savePath ?? `${await path.appLocalDataDir()}userData${path.sep()}UIGF_${uid}.json`;
|
||||
await writeTextFile(filePath, JSON.stringify(UigfData));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出UIGF4数据
|
||||
* @since Beta v0.9.1
|
||||
* @param uids - UID列表
|
||||
* @since Beta v0.9.5
|
||||
* @param usg - 祈愿UID列表
|
||||
* @param usb - 颂愿UID列表
|
||||
* @param savePath - 保存路径
|
||||
* @returns
|
||||
*/
|
||||
export async function exportUigf4Data(uids: Array<string> = [], savePath?: string): Promise<void> {
|
||||
export async function exportUigf4Data(
|
||||
usg: Array<string> = [],
|
||||
usb: Array<string> = [],
|
||||
savePath?: string,
|
||||
): Promise<void> {
|
||||
const header = await getUigf4Header();
|
||||
const filePath = savePath ?? `${await path.appLocalDataDir()}userData\\UIGF4.json`;
|
||||
const data: Array<TGApp.Plugins.UIGF.GachaHk4e> = [];
|
||||
for (const uid of uids) {
|
||||
const filePath = savePath ?? `${await path.appLocalDataDir()}userData${path.sep()}UIGF4.json`;
|
||||
const dataHk4e: Array<TGApp.Plugins.UIGF.GachaHk4e> = [];
|
||||
const dataUgc: Array<TGApp.Plugins.UIGF.GachaUgc> = [];
|
||||
for (const uid of usg) {
|
||||
const gachaList = await TSUserGacha.record.all(uid);
|
||||
await showLoading.update(`正在导出${uid}的${gachaList.length}条祈愿记录`);
|
||||
const timezone = getUigfTimeZone(uid);
|
||||
data.push({ uid: uid, timezone: timezone, list: convertDataToUigf(gachaList, timezone) });
|
||||
dataHk4e.push({ uid: uid, timezone: timezone, list: convertHk4e2Uigf(gachaList, timezone) });
|
||||
}
|
||||
const uigf4Data: TGApp.Plugins.UIGF.Schema4 = { info: header, hk4e: data };
|
||||
for (const uid of usb) {
|
||||
const gachaList = await TSUserGachaB.getGachaRecords(uid);
|
||||
await showLoading.update(`正在导出${uid}的${gachaList.length}条颂愿记录`);
|
||||
const timezone = getUigfTimeZone(uid);
|
||||
dataUgc.push({ uid: uid, timezone: timezone, list: convertUgc2Uigf(gachaList, timezone) });
|
||||
}
|
||||
const uigf4Data: TGApp.Plugins.UIGF.Schema4 = { info: header, hk4e: dataHk4e, hk4e_ugc: dataUgc };
|
||||
await writeTextFile(filePath, JSON.stringify(uigf4Data, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份祈愿数据
|
||||
* @since Beta v0.9.5
|
||||
* @param dir - 备份目录
|
||||
* @returns 无返回值
|
||||
*/
|
||||
export async function backUpUigf(dir: string): Promise<void> {
|
||||
if (!(await exists(dir))) {
|
||||
await TGLogger.Warn("不存在指定的祈愿备份目录,即将创建");
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
const usg = await TSUserGacha.getUidList();
|
||||
const usb = await TSUserGachaB.getUidList();
|
||||
// TODO: 调整备份文件名称
|
||||
const savePath = `${dir}${path.sep()}UIGF4.json`;
|
||||
await exportUigf4Data(usg, usb, savePath);
|
||||
await TGLogger.Info("祈愿数据备份完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复祈愿数据-旧版
|
||||
* @since Beta v0.9.5
|
||||
* @param dir - 目录
|
||||
* @param file - 文件名称
|
||||
* @returns 是否恢复成功
|
||||
*/
|
||||
async function restoreUigfByLegacyFile(dir: string, file: string): Promise<boolean> {
|
||||
await showLoading.update(`祈愿文件: ${file}`);
|
||||
const filePath = `${dir}${path.sep()}${file}`;
|
||||
try {
|
||||
const check = await verifyUigfData(filePath);
|
||||
if (!check) {
|
||||
await showLoading.update(`UIGF数据校验失败`);
|
||||
await TGLogger.Warn(`UIGF数据校验失败${filePath}`);
|
||||
return false;
|
||||
}
|
||||
const data = await readUigfData(filePath);
|
||||
const uid = data.info.uid;
|
||||
await showLoading.update(`正在导入${data.info.uid}的${data.list.length}条祈愿数据`);
|
||||
await TSUserGacha.mergeUIGF(uid, data.list, true);
|
||||
} catch (e) {
|
||||
await TGLogger.Error(`恢复祈愿数据失败${dir}`);
|
||||
await TGLogger.Error(typeof e === "string" ? e : JSON.stringify(e));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复祈愿数据
|
||||
* @since Beta v0.9.5
|
||||
* @param filePath - 文件路径
|
||||
* @returns 是否恢复成功
|
||||
*/
|
||||
async function restoreUigfByFile(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const check = await verifyUigfData(filePath, true);
|
||||
if (!check) {
|
||||
await TGLogger.Warn(`UIGF数据校验失败${filePath}`);
|
||||
return false;
|
||||
}
|
||||
const read = await readUigf4Data(filePath);
|
||||
for (const data of read?.hk4e ?? []) {
|
||||
await showLoading.update(`正在导入${data.uid}的${data.list.length}条祈愿记录`);
|
||||
await TSUserGacha.mergeUIGF4(data, true);
|
||||
}
|
||||
for (const data of read?.hk4e_ugc ?? []) {
|
||||
await showLoading.update(`正在导入${data.uid}的${data.list.length}条颂愿记录`);
|
||||
await TSUserGachaB.mergeUIGF4(data, true);
|
||||
}
|
||||
} catch (e) {
|
||||
await TGLogger.Error(`恢复祈愿数据失败 ${filePath}`);
|
||||
await TGLogger.Error(typeof e === "string" ? e : JSON.stringify(e));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复祈愿数据
|
||||
* @since Beta v0.9.5
|
||||
* @param dir - 备份目录
|
||||
* @returns 是否恢复成功
|
||||
*/
|
||||
export async function restoreUigf(dir: string): Promise<boolean> {
|
||||
if (!(await exists(dir))) {
|
||||
await TGLogger.Warn("不存在指定的祈愿备份目录");
|
||||
return false;
|
||||
}
|
||||
let cnt = 0;
|
||||
// 适配旧版备份文件 UIGF_{{uid}}.json
|
||||
const legacyReg = /^UIGF_\d+\.json$/;
|
||||
const legacyFiles = (await readDir(dir)).filter(
|
||||
(item) => item.isFile && legacyReg.test(item.name),
|
||||
);
|
||||
for (const file of legacyFiles) {
|
||||
const checkL = await restoreUigfByLegacyFile(dir, file.name);
|
||||
if (checkL) cnt++;
|
||||
}
|
||||
const filePath = `${dir}${path.sep()}UIGF4.json`;
|
||||
if (!(await exists(filePath))) {
|
||||
await TGLogger.Warn(`未检测到UIGF4备份文件`);
|
||||
} else {
|
||||
await restoreUigfByFile(filePath);
|
||||
}
|
||||
return cnt > 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* 用户数据的备份、恢复、迁移
|
||||
* @since Beta v0.9.0
|
||||
* @since Beta v0.9.5
|
||||
*/
|
||||
|
||||
import showLoading from "@comp/func/loading.js";
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import TSUserAbyss from "@Sqlm/userAbyss.js";
|
||||
@@ -9,15 +10,15 @@ import TSUserAccount from "@Sqlm/userAccount.js";
|
||||
import TSUserAchi from "@Sqlm/userAchi.js";
|
||||
import TSUserChallenge from "@Sqlm/userChallenge.js";
|
||||
import TSUserCombat from "@Sqlm/userCombat.js";
|
||||
import TSUserGacha from "@Sqlm/userGacha.js";
|
||||
import { exists, mkdir } from "@tauri-apps/plugin-fs";
|
||||
import { backUpUigf, restoreUigf } from "@utils/UIGF.js";
|
||||
|
||||
import TGLogger from "./TGLogger.js";
|
||||
|
||||
/**
|
||||
* 备份用户数据
|
||||
* @since Beta v0.9.0
|
||||
* @TODO 重构祈愿部分备份&读取,处理胡桃数据&应用自身数据的导入
|
||||
* @since Beta v0.9.5
|
||||
* @TODO 处理胡桃数据&应用自身数据的导入
|
||||
* @param dir - 备份目录路径
|
||||
* @returns 无返回值
|
||||
*/
|
||||
@@ -36,13 +37,13 @@ export async function backUpUserData(dir: string): Promise<void> {
|
||||
await TSUserCombat.backupCombat(dir);
|
||||
await showLoading.update("正在备份幽境危战数据");
|
||||
await TSUserChallenge.backupChallenge(dir);
|
||||
await showLoading.update("正在备份UIGF祈愿数据");
|
||||
await TSUserGacha.backUpUigf(dir);
|
||||
await showLoading.update("正在备份祈愿数据");
|
||||
await backUpUigf(dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复用户数据
|
||||
* @since Beta v0.9.0
|
||||
* @since Beta v0.9.5
|
||||
* @param dir - 备份目录路径
|
||||
* @returns 无返回值
|
||||
*/
|
||||
@@ -83,7 +84,7 @@ export async function restoreUserData(dir: string): Promise<void> {
|
||||
errNum++;
|
||||
}
|
||||
await showLoading.update("正在恢复祈愿数据");
|
||||
const restoreGacha = await TSUserGacha.restoreUigf(dir);
|
||||
const restoreGacha = await restoreUigf(dir);
|
||||
if (!restoreGacha) {
|
||||
showSnackbar.error("祈愿数据恢复失败");
|
||||
errNum++;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 一些工具函数
|
||||
* @since Beta v0.9.2
|
||||
* @since Beta v0.9.5
|
||||
*/
|
||||
|
||||
import { tz } from "@date-fns/tz";
|
||||
@@ -391,3 +391,17 @@ export function validEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 传入时间字符串跟对应时区,转成utc8时间字符串
|
||||
* @since Beta v0.9.5
|
||||
* @param time - 时间字符串
|
||||
* @param timezone - 时区
|
||||
* @returns 转换后的时间戳
|
||||
*/
|
||||
export function getUtc8Time(time: string, timezone: number): string {
|
||||
const date = new Date(time);
|
||||
const diffTimezone = -timezone + 8;
|
||||
const realDate = new Date(date.getTime() + diffTimezone * 60 * 60 * 1000);
|
||||
return timestampToDate(realDate.getTime());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user