♻️ UIAF重构,支持祈愿备份/恢复

close #109
This commit is contained in:
目棃
2024-05-07 19:52:34 +08:00
parent 2803d06418
commit 2f14405cab
14 changed files with 468 additions and 235 deletions

View File

@@ -6,7 +6,6 @@
import type { SchemaType } from "ajv/lib/types/index.js";
// 应用数据
import achievements from "./app/achievements.json";
import achievementSeries from "./app/achievementSeries.json";
import calendar from "./app/calendar.json";
@@ -14,17 +13,17 @@ import character from "./app/character.json";
import gacha from "./app/gacha.json";
import GCG from "./app/GCG.json";
import nameCards from "./app/namecard.json";
import uigfSchema from "./app/uigf-schema.json";
import weapon from "./app/weapon.json";
// 存档数据
import arcBirCalendar from "./archive/birth_calendar.json";
import arcBirDraw from "./archive/birth_draw.json";
import arcBirRole from "./archive/birth_role.json";
// Wiki 数据
import schemaUiaf from "./schema/uiaf-schema.json";
import schemaUigf from "./schema/uigf-schema.json";
import wikiCharacter from "./WIKI/character.json";
import wikiMaterial from "./WIKI/material.json";
import wikiWeapon from "./WIKI/weapon.json";
// App
export const AppAchievementsData: TGApp.App.Achievement.Item[] = achievements;
export const AppAchievementSeriesData: TGApp.App.Achievement.Series[] = achievementSeries;
export const AppCalendarData: TGApp.App.Calendar.Item[] = calendar;
@@ -33,10 +32,14 @@ export const AppGachaData: TGApp.App.Gacha.PoolItem[] = gacha;
export const AppGCGData: TGApp.App.GCG.WikiBriefInfo[] = GCG;
export const AppNameCardsData: TGApp.App.NameCard.Item[] = nameCards;
export const AppWeaponData: TGApp.App.Weapon.WikiBriefInfo[] = weapon;
export const AppUigfSchema: SchemaType = uigfSchema;
// Schema
export const UiafSchema: SchemaType = schemaUiaf;
export const UigfSchema: SchemaType = schemaUigf;
// Archive
export const ArcBirCalendar: TGApp.Archive.Birth.CalendarData = arcBirCalendar;
export const ArcBirDraw: TGApp.Archive.Birth.DrawItem[] = arcBirDraw;
export const ArcBirRole: TGApp.Archive.Birth.RoleItem[] = arcBirRole;
// Wiki
export const WikiCharacterData: TGApp.App.Character.WikiItem[] = wikiCharacter;
export const WikiWeaponData: TGApp.App.Weapon.WikiItem[] = wikiWeapon;
export const WikiMaterialData: TGApp.App.Material.WikiItem[] = wikiMaterial;

View File

@@ -0,0 +1,59 @@
{
"type": "object",
"properties": {
"info": {
"type": "object",
"properties": {
"export_app": {
"type": "string",
"description": "Export application name"
},
"export_app_version": {
"type": "string",
"description": "Export application version"
},
"uiaf_version": {
"type": "string",
"description": "UIAF version applied; Used to prevent application not working when UIGF have breaking update",
"pattern": "v\\d+.\\d+"
},
"export_timestamp": {
"type": "number",
"description": "Export time in UNIX timestamp"
}
},
"required": ["export_app", "uiaf_version"],
"description": "Include basic information defined by export application"
},
"list": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "Achievement ID"
},
"current": {
"type": "number",
"description": "Process"
},
"status": {
"type": "number",
"description": "Finished status",
"enum": [0, 1, 2, 3],
"enumDesc": "ACHIEVEMENT_INVALID = 0; ACHIEVEMENT_UNFINISHED = 1; ACHIEVEMENT_FINISHED = 2;ACHIEVEMENT_POINT_TAKEN = 3;"
},
"timestamp": {
"type": "number",
"description": "Finished time"
}
},
"required": ["id", "current", "status", "timestamp"],
"description": "To represent an achievement"
},
"description": "Include finished or unfinished achievements"
}
},
"required": ["info", "list"]
}

View File

@@ -126,11 +126,16 @@ import ToAchiInfo from "../../components/overlay/to-achiInfo.vue";
import ToLoading from "../../components/overlay/to-loading.vue";
import ToNamecard from "../../components/overlay/to-namecard.vue";
import { AppAchievementSeriesData, AppNameCardsData } from "../../data";
import TGSqlite from "../../plugins/Sqlite";
import TSUserAchi from "../../plugins/Sqlite/modules/userAchi.js";
import { useAchievementsStore } from "../../store/modules/achievements";
import TGLogger from "../../utils/TGLogger";
import { getNowStr } from "../../utils/toolFunc";
import { getUiafHeader, readUiafData, verifyUiafData } from "../../utils/UIAF";
import {
getUiafHeader,
readUiafData,
verifyUiafData,
verifyUiafDataClipboard,
} from "../../utils/UIAF";
// Store
const achievementsStore = useAchievementsStore();
@@ -185,7 +190,7 @@ async function switchHideFin() {
// 刷新概况
async function flushOverview(): Promise<void> {
const { total, fin } = await getAchiOverview();
const { total, fin } = await TSUserAchi.getOverview();
achievementsStore.flushData(total, fin);
title.value = achievementsStore.title;
}
@@ -197,8 +202,8 @@ onMounted(async () => {
loadingTitle.value = "正在获取成就系列数据";
await flushOverview();
await TGLogger.Info(`[Achievements][onMounted] ${title.value}`);
allSeriesData.value = await getSeriesData();
achievementsStore.lastVersion = await TGSqlite.getLatestAchievementVersion();
allSeriesData.value = await TSUserAchi.getSeries();
achievementsStore.lastVersion = await TSUserAchi.getLatestAchiVersion();
loadingTitle.value = "正在获取成就数据";
selectedAchievement.value = await getAchiData("all");
loading.value = false;
@@ -229,7 +234,7 @@ async function selectSeries(index: number): Promise<void> {
selectedSeries.value = index;
selectedAchievement.value = await getAchiData("series", index.toString());
loadingTitle.value = "正在查找对应的成就名片";
curCardName.value = await getNameCardName(index);
curCardName.value = await TSUserAchi.getSeriesNameCard(index);
if (curCardName.value !== "") {
curCard.value = AppNameCardsData.find((item) => item.name === curCardName.value);
}
@@ -360,7 +365,7 @@ async function importJson(): Promise<void> {
loadingTitle.value = "正在解析数据";
loading.value = true;
loadingTitle.value = "正在合并成就数据";
await TGSqlite.mergeUIAF(remoteRaw.list);
await TSUserAchi.mergeUIAF(remoteRaw.list);
loadingTitle.value = "即将刷新页面";
setTimeout(() => {
window.location.reload();
@@ -382,7 +387,7 @@ async function exportJson(): Promise<void> {
// 获取本地数据
const UiafData = {
info: await getUiafHeader(),
list: await TGSqlite.getUIAF(),
list: await TSUserAchi.getUIAF(),
};
const fileName = `UIAF_${UiafData.info.export_app}_${UiafData.info.export_app_version}_${UiafData.info.export_timestamp}`;
const isSave = await dialog.save({
@@ -430,33 +435,22 @@ async function handleImportOuter(app: string): Promise<void> {
}
// 读取 剪贴板
const clipboard = await window.navigator.clipboard.readText();
let data: TGApp.Plugins.UIAF.Achievement[];
// 里面是完整的 uiaf 数据
try {
data = JSON.parse(clipboard).list;
const check = await verifyUiafDataClipboard();
if (!check) return;
const data: TGApp.Plugins.UIAF.Data = JSON.parse(clipboard);
loadingTitle.value = "正在导入数据";
loading.value = true;
await TGSqlite.mergeUIAF(data);
await TSUserAchi.mergeUIAF(data);
loading.value = false;
showSnackbar({
color: "success",
text: "导入成功,即将刷新页面",
});
await TGLogger.Info("[Achievements][handleImportOuter] 导入成功");
} catch (e) {
if (e instanceof Error)
await TGLogger.Error(`[Achievements][handleImportOuter] 导入失败 ${e.name}: ${e.message}`);
else console.error(e);
showSnackbar({
color: "error",
text: "读取 UIAF 数据失败,请检查文件是否符合规范",
});
} finally {
setTimeout(async () => {
await router.push("/achievements");
}, 1500);
}
}
// 改变成就状态
async function setAchi(
@@ -474,10 +468,12 @@ async function setAchi(
}
renderSelect.value[renderSelect.value.findIndex((item) => item.id === achievement.id)] =
newAchievement;
await setAchiDB(newAchievement);
await TSUserAchi.updateAchievement(newAchievement);
await flushOverview();
allSeriesData.value[allSeriesData.value.findIndex((item) => item.id === newAchievement.series)] =
(await getSeriesData(newAchievement.series))[0];
const seriesIndex = allSeriesData.value.findIndex((item) => item.id === newAchievement.series);
if (seriesIndex === -1) return;
const seriesGet = await TSUserAchi.getSeries(newAchievement.series);
allSeriesData.value[seriesIndex] = seriesGet[0];
showSnackbar({
text: `已将成就 ${newAchievement.name}[${newAchievement.id}] 标记为 ${
target ? "已完成" : "未完成"
@@ -490,46 +486,14 @@ async function setAchi(
);
}
/* 以下为数据库操作 */
// 获取成就概况
async function getAchiOverview(): Promise<{
total: number;
fin: number;
}> {
const db = await TGSqlite.getDB();
const sql = "SELECT SUM(totalCount) AS total, SUM(finCount) AS fin FROM AchievementSeries;";
const res: Array<{ total: number; fin: number }> = await db.select(sql);
return res[0];
}
// 获取成就系列
async function getSeriesData(series?: number): Promise<TGApp.Sqlite.Achievement.SeriesTable[]> {
const db = await TGSqlite.getDB();
let sql = "SELECT * FROM AchievementSeries ORDER BY `order`;";
if (series) {
sql = `SELECT *
FROM AchievementSeries
WHERE id = ${series}
ORDER BY \`order\`;`;
}
return await db.select(sql);
}
// 获取成就(某个系列)
async function getAchiData(
type: "all" | "series" | "search",
value?: string,
): Promise<TGApp.Sqlite.Achievement.SingleTable[]> {
const db = await TGSqlite.getDB();
let sql = "";
if (type === "all" || (type == "series" && value === undefined)) {
sql = "SELECT * FROM Achievements ORDER BY isCompleted, `order`;";
} else if (type === "series") {
sql = `SELECT *
FROM Achievements
WHERE series = ${value}
ORDER BY isCompleted, \`order\`;`;
} else if (type === "search") {
if (type !== "search") {
return TSUserAchi.getAchievements(value);
}
if (value === undefined) {
showSnackbar({
color: "error",
@@ -537,41 +501,7 @@ async function getAchiData(
});
return [];
}
if (value.startsWith("v")) {
const version = value.replace("v", "");
sql = `SELECT *
FROM Achievements
WHERE version LIKE '%${version}%'
ORDER BY isCompleted, \`order\`;`;
} else {
sql = `SELECT *
FROM Achievements
WHERE name LIKE '%${value}%'
OR description LIKE '%${value}%'
ORDER BY isCompleted, \`order\`;`;
}
}
return await db.select(sql);
}
// 获取成就名片
async function getNameCardName(series: number): Promise<string> {
const db = await TGSqlite.getDB();
const sql = `SELECT nameCard
FROM AchievementSeries
WHERE id = ${series};`;
const res: Array<{ nameCard: string }> = await db.select(sql);
return res[0].nameCard;
}
// 更新成就数据
async function setAchiDB(achievement: TGApp.Sqlite.Achievement.SingleTable): Promise<void> {
const db = await TGSqlite.getDB();
const sql = `UPDATE Achievements
SET isCompleted = ${achievement.isCompleted},
completedTime = '${achievement.completedTime}'
WHERE id = ${achievement.id};`;
await db.execute(sql);
return TSUserAchi.searchAchievements(value);
}
</script>
<!-- 顶部栏跟 wrap 大概布局 -->

View File

@@ -98,6 +98,7 @@ import showConfirm from "../../components/func/confirm";
import showSnackbar from "../../components/func/snackbar";
import ToLoading from "../../components/overlay/to-loading.vue";
import TGSqlite from "../../plugins/Sqlite";
import TSUserAchi from "../../plugins/Sqlite/modules/userAchi.js";
import { useAchievementsStore } from "../../store/modules/achievements";
import { useAppStore } from "../../store/modules/app";
import { useHomeStore } from "../../store/modules/home";
@@ -237,7 +238,7 @@ async function confirmUpdate(title?: string): Promise<void> {
loadingTitle.value = "正在更新数据库...";
loading.value = true;
await TGSqlite.update();
achievementsStore.lastVersion = await TGSqlite.getLatestAchievementVersion();
achievementsStore.lastVersion = await TSUserAchi.getLatestAchiVersion();
appStore.buildTime = getBuildTime();
loading.value = false;
showSnackbar({

View File

@@ -1,14 +1,12 @@
/**
* @file plugins/Sqlite/index.ts
* @description Sqlite 数据库操作类
* @since Beta v0.4.5
* @since Beta v0.4.7
*/
import { app } from "@tauri-apps/api";
import Database from "tauri-plugin-sql-api";
import { getUiafStatus } from "../../utils/UIAF";
import initDataSql from "./sql/initData";
import {
importAbyssData,
@@ -18,7 +16,6 @@ import {
insertRecordData,
insertRoleData,
} from "./sql/insertData";
import { importUIAFData } from "./sql/updateData";
class Sqlite {
/**
@@ -201,57 +198,6 @@ class Sqlite {
await this.initDB();
}
/**
* @description 获取最新成就版本
* @since Beta v0.3.3
* @returns {Promise<string>}
*/
public async getLatestAchievementVersion(): Promise<string> {
const db = await this.getDB();
const sql = "SELECT version FROM Achievements ORDER BY version DESC LIMIT 1;";
const res: Array<{ version: string }> = await db.select(sql);
return res[0].version;
}
/**
* @description 合并 UIAF 数据
* @since Beta v0.3.3
* @param {TGApp.Plugins.UIAF.Achievement[]} achievements UIAF 数据
* @returns {Promise<void>}
*/
public async mergeUIAF(achievements: TGApp.Plugins.UIAF.Achievement[]): Promise<void> {
const db = await this.getDB();
const sql = importUIAFData(achievements);
for (const item of sql) {
await db.execute(item);
}
}
/**
* @description 获取 UIAF 数据
* @since Beta v0.3.3
* @returns {Promise<TGApp.Plugins.UIAF.Achievement[]>}
*/
public async getUIAF(): Promise<TGApp.Plugins.UIAF.Achievement[]> {
const db = await this.getDB();
const sql = "SELECT * FROM Achievements WHERE isCompleted = 1 OR progress > 0";
const res: TGApp.Sqlite.Achievement.SingleTable[] = await db.select(sql);
const achievements: TGApp.Plugins.UIAF.Achievement[] = [];
for (const item of res) {
const completed = item.isCompleted === 1;
const status = getUiafStatus(completed, item.progress);
achievements.push({
id: item.id,
status,
timestamp:
completed && item.completedTime ? new Date(item.completedTime).getTime() / 1000 : 0,
current: item.progress,
});
}
return achievements;
}
/**
* @description 保存深渊数据
* @since Beta v0.3.3

View File

@@ -0,0 +1,194 @@
/**
* @file plugins/Sqlite/modules/userAchi.ts
* @description 用户成就模块
* @since Beta v0.4.7
*/
import { getUiafStatus } from "../../../utils/UIAF.js";
import TGSqlite from "../index";
import { importUIAFData } from "../sql/updateData";
/**
* @description 获取成就概况
* @since Beta v0.4.7
* @returns {Promise<TGApp.Sqlite.Achievement.Overview}> 成就概况
*/
async function getOverview(): Promise<TGApp.Sqlite.Achievement.Overview> {
const db = await TGSqlite.getDB();
const res = await db.select<TGApp.Sqlite.Achievement.Overview>(
"SELECT SUM(totalCount) as total,SUM(finCount) AS fin From AchievementSeries",
);
return res[0];
}
/**
* @description 获取最新成就版本
* @since Beta v0.4.7
* @returns {Promise<string>} 最新成就版本
*/
async function getLatestAchiVersion(): Promise<string> {
const db = await TGSqlite.getDB();
type resType = { version: string };
const res = await db.select<resType>(
"SELECT version FROM Achievements ORDER BY version DESC LIMIT 1;",
);
return res[0].version;
}
/**
* @description 获取成就系列数据
* @since Beta v0.4.7
* @param {number|undefined} id 成就系列ID
* @returns {Promise<TGApp.Sqlite.Achievement.SeriesTable[]>} 成就系列数据
*/
async function getSeries(id?: number): Promise<TGApp.Sqlite.Achievement.SeriesTable[]> {
const db = await TGSqlite.getDB();
let res: TGApp.Sqlite.Achievement.SeriesTable[] = [];
if (id === undefined) {
res = await db.select<TGApp.Sqlite.Achievement.SeriesTable>(
"SELECT * FROM AchievementSeries ORDER BY `order`;",
);
} else {
res = await db.select<TGApp.Sqlite.Achievement.SeriesTable>(
"SELECT * FROM AchievementSeries WHERE id = ?;",
[id],
);
}
return res;
}
/**
* @description 获取成就数据
* @since Beta v0.4.7
* @param {number|undefined} id 成就系列ID
* @returns {Promise<TGApp.Sqlite.Achievement.SingleTable[]>} 成就数据
*/
async function getAchievements(id?: string): Promise<TGApp.Sqlite.Achievement.SingleTable[]> {
const db = await TGSqlite.getDB();
let res: TGApp.Sqlite.Achievement.SingleTable[] = [];
if (id === undefined) {
res = await db.select<TGApp.Sqlite.Achievement.SingleTable>(
"SELECT * FROM Achievements ORDER BY isCompleted,`order`;",
);
} else {
res = await db.select<TGApp.Sqlite.Achievement.SingleTable>(
"SELECT * FROM Achievements WHERE series = ? ORDER BY `order`;",
[id],
);
}
return res;
}
/**
* @description 获取成就名片
* @since Beta v0.4.7
* @param {string} id 成就系列ID
* @returns {Promise<string>} 成就名片
*/
async function getSeriesNameCard(id: string): Promise<string> {
const db = await TGSqlite.getDB();
type resType = { nameCard: string };
const res = await db.select<resType>("SELECT nameCard FROM AchievementSeries WHERE id = ?;", [
id,
]);
return res[0].nameCard;
}
/**
* @description 查找成就数据
* @since Beta v0.4.7
* @param {string} keyword 关键词
* @returns {Promise<TGApp.Sqlite.Achievement.SingleTable[]>} 成就数据
*/
async function searchAchievements(
keyword: string,
): Promise<TGApp.Sqlite.Achievement.SingleTable[]> {
if (keyword === "") return await getAchievements();
const db = await TGSqlite.getDB();
const versionReg = /^v\d+(\.\d+)?$/;
if (versionReg.test(keyword)) {
return await db.select<TGApp.Sqlite.Achievement.SingleTable>(
"SELECT * FROM Achievements WHERE version LIKE ? ORDER BY isCompleted,`order`;",
[keyword],
);
}
return await db.select<TGApp.Sqlite.Achievement.SingleTable>(
"SELECT * FROM Achievements WHERE name LIKE ? OR description LIKE ? ORDER BY isCompleted,`order`;",
[`%${keyword}%`, `%${keyword}%`],
);
}
/**
* @description 更新成就数据
* @since Beta v0.4.7
* @param {TGApp.Sqlite.Achievement.SingleTable} data UIAF数据
* @returns {Promise<void>}
*/
async function updateAchievement(data: TGApp.Sqlite.Achievement.SingleTable): Promise<void> {
const db = await TGSqlite.getDB();
await db.execute("UPDATE Achievements SET isCompleted = ?, completedTime = ? WHERE id = ?;", [
data.isCompleted,
data.completedTime.toString(),
data.id,
]);
}
/**
* @description 将数据库数据转换为UIAF数据
* @since Beta v0.4.7
* @param {TGApp.Sqlite.Achievement.SingleTable} data 数据库数据
* @returns {TGApp.Plugins.UIAF.Achievement} UIAF数据
*/
function transDb2Uiaf(data: TGApp.Sqlite.Achievement.SingleTable): TGApp.Plugins.UIAF.Achievement {
const isCompleted = data.isCompleted === 1;
const status = getUiafStatus(isCompleted, data.progress);
return {
id: data.id,
timestamp: data.timestamp,
current: data.progress,
status,
};
}
/**
* @description 获取UIAF数据
* @since Beta v0.4.7
* @returns {Promise<TGApp.Plugins.UIAF.Achievement[]>}
*/
async function getUIAF(): Promise<TGApp.Plugins.UIAF.Achievement[]> {
const db = await TGSqlite.getDB();
const data = await db.select<TGApp.Sqlite.Achievement.SingleTable>("SELECT * FROM Achievements;");
const res: TGApp.Plugins.UIAF.Achievement[] = [];
for (const item: TGApp.Sqlite.Achievement.SingleTable of data) {
res.push(transDb2Uiaf(item));
}
return res;
}
/**
* @description 合并UIAF数据
* @since Beta v0.4.7
* @param {TGApp.Plugins.UIAF.Achievement[]} data UIAF数据
* @returns {Promise<void>}
*/
async function mergeUIAF(data: TGApp.Plugins.UIAF.Achievement[]): Promise<void> {
const db = await TGSqlite.getDB();
for (const item of data) {
const sql = importUIAFData(item);
await db.execute(sql);
}
}
const TSUserAchi = {
getOverview,
getLatestAchiVersion,
getSeries,
getSeriesNameCard,
getAchievements,
searchAchievements,
updateAchievement,
getUIAF,
mergeUIAF,
};
export default TSUserAchi;

View File

@@ -130,7 +130,6 @@ async function mergeUIGF(uid: string, data: TGApp.Plugins.UIGF.GachaItem[]): Pro
const db = await TGSqlite.getDB();
for (const gacha of data) {
const trans = transGacha(gacha);
console.log(trans);
const sql = importUIGFData(uid, trans);
await db.execute(sql);
}

View File

@@ -7,19 +7,16 @@
import minifySql from "../../../utils/minifySql";
/**
* @description 导入UIAF数据
* @since Alpha v0.2.3
* @param {TGApp.Plugins.UIAF.Achievement[]} data
* @returns {string[]} sql
* @description 导入UIAF数据-单项
* @since Beta v0.4.7
* @param {TGApp.Plugins.UIAF.Achievement} data
* @returns {string} sql
*/
export function importUIAFData(data: TGApp.Plugins.UIAF.Achievement[]): string[] {
const sqlRes: string[] = [];
data.map((achievement) => {
export function importUIAFData(data: TGApp.Plugins.UIAF.Achievement): string[] {
let sql;
// 获取完成状态
const isCompleted = achievement.status === 2 || achievement.status === 3;
const isCompleted = data.status === 2 || data.status === 3;
if (isCompleted) {
const completedTime = new Date(achievement.timestamp * 1000)
const completedTime = new Date(data.timestamp * 1000)
.toISOString()
.replace("T", " ")
.slice(0, 19);
@@ -27,10 +24,11 @@ export function importUIAFData(data: TGApp.Plugins.UIAF.Achievement[]): string[]
UPDATE Achievements
SET isCompleted = 1,
completedTime = '${completedTime}',
progress = ${achievement.current},
progress = ${data.current},
updated = datetime('now', 'localtime')
WHERE id = ${achievement.id}
AND (isCompleted = 0 OR completedTime != '${completedTime}' OR progress != ${achievement.current});
WHERE id = ${data.id}
AND (isCompleted = 0 OR completedTime != '${completedTime}'
OR progress != ${data.current});
`;
} else {
sql = `
@@ -41,9 +39,7 @@ export function importUIAFData(data: TGApp.Plugins.UIAF.Achievement[]): string[]
AND progress != ${achievement.current};
`;
}
return sqlRes.push(minifySql(sql));
});
return sqlRes;
return minifySql(sql);
}
/**

View File

@@ -14,7 +14,7 @@ declare namespace TGApp.Plugins.UIAF {
* @property {Achievement[]} list UIAF 成就列表
* @return Data
*/
export interface Data {
interface Data {
info: Export;
list: Achievement[];
}
@@ -29,7 +29,7 @@ declare namespace TGApp.Plugins.UIAF {
* @property {string} uiaf_version UIAF 版本
* @return Export
*/
export interface Export {
interface Export {
export_app: string;
export_timestamp: number;
export_app_version: string;
@@ -46,7 +46,7 @@ declare namespace TGApp.Plugins.UIAF {
* @property {number} status 成就状态0 为未完成1 为已完成
* @return Achievement
*/
export interface Achievement {
interface Achievement {
id: number;
timestamp: number;
current: number;

View File

@@ -1,7 +1,7 @@
/**
* @file types/Sqlite/Achievement.d.ts
* @description 数据库成就相关类型定义文件
* @since Alpha v0.2.0
* @since Beta v0.4.7
*/
declare namespace TGApp.Sqlite.Achievement {
@@ -60,4 +60,17 @@ declare namespace TGApp.Sqlite.Achievement {
nameCard: string;
updated: string;
}
/**
* @description 成就概况
* @since Beta v0.4.7
* @interface Overview
* @property {number} total - 总成就数
* @property {number} fin - 已完成成就数
* @returns Overview
*/
interface Overview {
total: number;
fin: number;
}
}

View File

@@ -1,10 +1,17 @@
/**
* @file utils/UIAF.ts
* @description UIAF工具类
* @since Beta v0.4.1
* @since Beta v0.4.7
*/
import { app, fs } from "@tauri-apps/api";
import Ajv from "ajv";
import { ErrorObject } from "ajv/lib/types/index.js";
import showSnackbar from "../components/func/snackbar.js";
import { UiafSchema } from "../data/index.js";
import TGLogger from "./TGLogger.js";
/**
* @description 根据 completed 跟 progress 获取 status
@@ -40,16 +47,64 @@ export async function getUiafHeader(): Promise<TGApp.Plugins.UIAF.Export> {
}
/**
* @description 检测是否存在 UIAF 数据
* @description 粗略检测,不保证数据完整性
* @since Alpha v0.2.3
* @description 检测是否存在 UIAF 数据,采用 ajv 验证 schema
* @since Beta v0.4.7
* @param {string} path - UIAF 数据路径
* @returns {Promise<boolean>} 是否存在 UIAF 数据
*/
export async function verifyUiafData(path: string): Promise<boolean> {
const fileData: string = await fs.readTextFile(path);
const UiafData: TGApp.Plugins.UIAF.Export = JSON.parse(fileData)?.info;
return UiafData?.uiaf_version !== undefined;
const ajv = new Ajv();
const validate = ajv.compile(UiafSchema);
try {
const fileJson = JSON.parse(fileData);
if (!validate(fileJson)) {
const error: ErrorObject = validate.errors[0];
showSnackbar({
text: `${error.instancePath || error.schemaPath} ${error.message}`,
color: "error",
});
await TGLogger.Error(`UIAF 数据验证失败,文件路径:${path}`);
await TGLogger.Error(`错误信息 ${validate.errors}`);
return false;
}
return true;
} catch (e) {
showSnackbar({ text: `UIAF 数据格式错误 ${e}`, color: "error" });
await TGLogger.Error(`UIAF 数据格式错误,文件路径:${path}`);
await TGLogger.Error(`错误信息 ${e}`);
return false;
}
}
/**
* @description 验证UIAF数据-剪贴板
* @since Beta v0.4.7
* @returns {boolean} 是否验证通过
*/
export async function verifyUiafDataClipboard(): Promise<boolean> {
const ajv = new Ajv();
const validate = ajv.compile(UiafSchema);
const data = await window.navigator.clipboard.readText();
try {
const fileJson = JSON.parse(data);
if (!validate(fileJson)) {
const error: ErrorObject = validate.errors[0];
showSnackbar({
text: `${error.instancePath || error.schemaPath} ${error.message}`,
color: "error",
});
await TGLogger.Error(`UIAF 数据验证失败,剪贴板数据:${data}`);
await TGLogger.Error(`错误信息 ${validate.errors}`);
return false;
}
return true;
} catch (e) {
showSnackbar({ text: `UIAF 数据格式错误 ${e}`, color: "error" });
await TGLogger.Error(`UIAF 数据格式错误,剪贴板数据:${data}`);
await TGLogger.Error(`错误信息 ${e}`);
return false;
}
}
/**

View File

@@ -9,8 +9,9 @@ import Ajv from "ajv";
import { ErrorObject } from "ajv/lib/types/index.js";
import showSnackbar from "../components/func/snackbar.js";
import { AppUigfSchema } from "../data/index.js";
import { UigfSchema } from "../data/index.js";
import TGLogger from "./TGLogger.js";
import { timestampToDate } from "./toolFunc";
/**
@@ -78,7 +79,7 @@ export function convertDataToUigf(
export async function verifyUigfData(path: string): Promise<boolean> {
const fileData: string = await fs.readTextFile(path);
const ajv = new Ajv();
const validate = ajv.compile(AppUigfSchema);
const validate = ajv.compile(UigfSchema);
try {
const fileJson = JSON.parse(fileData);
if (!validate(fileJson)) {
@@ -87,11 +88,15 @@ export async function verifyUigfData(path: string): Promise<boolean> {
text: `${error.instancePath || error.schemaPath} ${error.message}`,
color: "error",
});
await TGLogger.Error(`UIGF 数据验证失败,文件路径:${path}`);
await TGLogger.Error(`错误信息 ${validate.errors}`);
return false;
}
return true;
} catch (e) {
showSnackbar({ text: `UIGF 数据格式错误 ${e}`, color: "error" });
await TGLogger.Error(`UIGF 数据格式错误,文件路径:${path}`);
await TGLogger.Error(`错误信息 ${e}`);
return false;
}
}

View File

@@ -1,17 +1,21 @@
/**
* @file utils/dataBS.ts
* @description 用户数据的备份、恢复、迁移
* @since Beta v0.4.1
* @since Beta v0.4.7
*/
import { fs, path } from "@tauri-apps/api";
import showSnackbar from "../components/func/snackbar";
import TGSqlite from "../plugins/Sqlite";
import TSUserAchi from "../plugins/Sqlite/modules/userAchi.js";
import TSUserGacha from "../plugins/Sqlite/modules/userGacha.js";
import { exportUigfData, readUigfData, verifyUigfData } from "./UIGF.js";
/**
* @description 备份用户数据
* @since Beta v0.4.1
* @since Beta v0.4.7
* @param {string} dir 备份目录路径
* @returns {Promise<void>}
*/
@@ -20,8 +24,7 @@ export async function backUpUserData(dir: string): Promise<void> {
console.log("备份目录不存在,创建备份目录");
await fs.createDir(dir, { recursive: true });
}
// 备份成就数据
const dataAchi = await TGSqlite.getUIAF();
const dataAchi = await TSUserAchi.getUIAF();
await fs.writeTextFile(`${dir}${path.sep}UIAF.json`, JSON.stringify(dataAchi));
// 备份 ck
const dataCK = await TGSqlite.getCookie();
@@ -29,12 +32,17 @@ export async function backUpUserData(dir: string): Promise<void> {
// 备份深渊数据
const dataAbyss = await TGSqlite.getAbyss();
await fs.writeTextFile(`${dir}${path.sep}abyss.json`, JSON.stringify(dataAbyss));
// todo 添加祈愿数据备份支持
// 备份祈愿数据
const uidList = await TSUserGacha.getUidList();
for (const uid of uidList) {
const dataGacha = await TSUserGacha.getGachaRecords(uid);
await exportUigfData(uid, dataGacha);
}
}
/**
* @description 恢复用户数据
* @since Beta v0.4.1
* @since Beta v0.4.7
* @param {string} dir 备份目录路径
* @returns {Promise<void>}
*/
@@ -47,17 +55,18 @@ export async function restoreUserData(dir: string): Promise<void> {
});
return;
}
const files = (await fs.readDir(dir)).filter((item) => item.type === "File");
// 恢复成就数据
const dataAchiPath = `${dir}${path.sep}UIAF.json`;
if (await fs.exists(dataAchiPath)) {
const achiFind = files.find((item) => item.name === "UIAF.json");
if (achiFind) {
try {
const dataAchi: TGApp.Plugins.UIAF.Achievement[] = JSON.parse(
await fs.readTextFile(dataAchiPath),
await fs.readTextFile(achiFind.path),
);
await TGSqlite.mergeUIAF(dataAchi);
await TSUserAchi.mergeUIAF(dataAchi);
} catch (e) {
showSnackbar({
text: "成就数据恢复失败",
text: `成就数据恢复失败 ${e}`,
color: "error",
});
errNum++;
@@ -69,14 +78,14 @@ export async function restoreUserData(dir: string): Promise<void> {
});
}
// 恢复 ck
const dataCKPath = `${dir}${path.sep}cookie.json`;
if (await fs.exists(dataCKPath)) {
const ckFind = files.find((item) => item.name === "cookie.json");
if (ckFind) {
try {
const dataCK = await fs.readTextFile(dataCKPath);
const dataCK = await fs.readTextFile(ckFind.path);
await TGSqlite.saveAppData("cookie", JSON.stringify(JSON.parse(dataCK)));
} catch (e) {
showSnackbar({
text: "Cookie 数据恢复失败",
text: `Cookie 数据恢复失败 ${e}`,
color: "error",
});
errNum++;
@@ -88,11 +97,11 @@ export async function restoreUserData(dir: string): Promise<void> {
});
}
// 恢复深渊数据
const dataAbyssPath = `${dir}${path.sep}abyss.json`;
if (await fs.exists(dataAbyssPath)) {
const abyssFind = files.find((item) => item.name === "abyss.json");
if (abyssFind) {
try {
const dataAbyss: TGApp.Sqlite.Abyss.SingleTable[] = JSON.parse(
await fs.readTextFile(dataAbyssPath),
await fs.readTextFile(abyssFind.path),
);
await TGSqlite.restoreAbyss(dataAbyss);
} catch (e) {
@@ -108,6 +117,29 @@ export async function restoreUserData(dir: string): Promise<void> {
color: "warn",
});
}
// 恢复祈愿数据
const reg = /UIGF_(\d+).json/;
const dataGachaList = files.filter((item) => reg.test(item.name));
for (const item of dataGachaList) {
const check = await verifyUigfData(item.path);
if (!check) {
errNum++;
continue;
}
try {
const data = await readUigfData(item.path);
const uid = data.info.uid;
for (const item of data.list) {
await TSUserGacha.mergeUIGF(uid, item);
}
} catch (e) {
showSnackbar({
text: `UID: ${uid} 祈愿数据恢复失败`,
color: "error",
});
errNum++;
}
}
if (errNum === 0) {
showSnackbar({
text: "数据恢复成功",