diff --git a/src/data/index.ts b/src/data/index.ts index 4d354b40..7e43591c 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -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; diff --git a/src/data/schema/uiaf-schema.json b/src/data/schema/uiaf-schema.json new file mode 100644 index 00000000..60beba8b --- /dev/null +++ b/src/data/schema/uiaf-schema.json @@ -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"] +} diff --git a/src/data/app/uigf-schema.json b/src/data/schema/uigf-schema.json similarity index 100% rename from src/data/app/uigf-schema.json rename to src/data/schema/uigf-schema.json diff --git a/src/pages/common/Achievements.vue b/src/pages/common/Achievements.vue index 8875742b..4d4ce9b8 100644 --- a/src/pages/common/Achievements.vue +++ b/src/pages/common/Achievements.vue @@ -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 { - 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 { 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 { 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 { // 获取本地数据 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,32 +435,21 @@ async function handleImportOuter(app: string): Promise { } // 读取 剪贴板 const clipboard = await window.navigator.clipboard.readText(); - let data: TGApp.Plugins.UIAF.Achievement[]; - // 里面是完整的 uiaf 数据 - try { - data = JSON.parse(clipboard).list; - loadingTitle.value = "正在导入数据"; - loading.value = true; - await TGSqlite.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); - } + const check = await verifyUiafDataClipboard(); + if (!check) return; + const data: TGApp.Plugins.UIAF.Data = JSON.parse(clipboard); + loadingTitle.value = "正在导入数据"; + loading.value = true; + await TSUserAchi.mergeUIAF(data); + loading.value = false; + showSnackbar({ + color: "success", + text: "导入成功,即将刷新页面", + }); + await TGLogger.Info("[Achievements][handleImportOuter] 导入成功"); + setTimeout(async () => { + await router.push("/achievements"); + }, 1500); } // 改变成就状态 @@ -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,88 +486,22 @@ 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 { - 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 { - 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 (value === undefined) { - showSnackbar({ - color: "error", - text: "搜索内容不能为空", - }); - 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\`;`; - } + if (type !== "search") { + return TSUserAchi.getAchievements(value); } - return await db.select(sql); -} - -// 获取成就名片 -async function getNameCardName(series: number): Promise { - 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 { - const db = await TGSqlite.getDB(); - const sql = `UPDATE Achievements - SET isCompleted = ${achievement.isCompleted}, - completedTime = '${achievement.completedTime}' - WHERE id = ${achievement.id};`; - await db.execute(sql); + if (value === undefined) { + showSnackbar({ + color: "error", + text: "搜索内容不能为空", + }); + return []; + } + return TSUserAchi.searchAchievements(value); } diff --git a/src/pages/common/Config.vue b/src/pages/common/Config.vue index bf61113f..cd5e1c82 100644 --- a/src/pages/common/Config.vue +++ b/src/pages/common/Config.vue @@ -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 { 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({ diff --git a/src/plugins/Sqlite/index.ts b/src/plugins/Sqlite/index.ts index 7971a9bf..a508a93e 100644 --- a/src/plugins/Sqlite/index.ts +++ b/src/plugins/Sqlite/index.ts @@ -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} - */ - public async getLatestAchievementVersion(): Promise { - 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} - */ - public async mergeUIAF(achievements: TGApp.Plugins.UIAF.Achievement[]): Promise { - 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} - */ - public async getUIAF(): Promise { - 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 diff --git a/src/plugins/Sqlite/modules/userAchi.ts b/src/plugins/Sqlite/modules/userAchi.ts new file mode 100644 index 00000000..20415ae1 --- /dev/null +++ b/src/plugins/Sqlite/modules/userAchi.ts @@ -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 成就概况 + */ +async function getOverview(): Promise { + const db = await TGSqlite.getDB(); + const res = await db.select( + "SELECT SUM(totalCount) as total,SUM(finCount) AS fin From AchievementSeries", + ); + return res[0]; +} + +/** + * @description 获取最新成就版本 + * @since Beta v0.4.7 + * @returns {Promise} 最新成就版本 + */ +async function getLatestAchiVersion(): Promise { + const db = await TGSqlite.getDB(); + type resType = { version: string }; + const res = await db.select( + "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} 成就系列数据 + */ +async function getSeries(id?: number): Promise { + const db = await TGSqlite.getDB(); + let res: TGApp.Sqlite.Achievement.SeriesTable[] = []; + if (id === undefined) { + res = await db.select( + "SELECT * FROM AchievementSeries ORDER BY `order`;", + ); + } else { + res = await db.select( + "SELECT * FROM AchievementSeries WHERE id = ?;", + [id], + ); + } + return res; +} + +/** + * @description 获取成就数据 + * @since Beta v0.4.7 + * @param {number|undefined} id 成就系列ID + * @returns {Promise} 成就数据 + */ +async function getAchievements(id?: string): Promise { + const db = await TGSqlite.getDB(); + let res: TGApp.Sqlite.Achievement.SingleTable[] = []; + if (id === undefined) { + res = await db.select( + "SELECT * FROM Achievements ORDER BY isCompleted,`order`;", + ); + } else { + res = await db.select( + "SELECT * FROM Achievements WHERE series = ? ORDER BY `order`;", + [id], + ); + } + return res; +} + +/** + * @description 获取成就名片 + * @since Beta v0.4.7 + * @param {string} id 成就系列ID + * @returns {Promise} 成就名片 + */ +async function getSeriesNameCard(id: string): Promise { + const db = await TGSqlite.getDB(); + type resType = { nameCard: string }; + const res = await db.select("SELECT nameCard FROM AchievementSeries WHERE id = ?;", [ + id, + ]); + return res[0].nameCard; +} + +/** + * @description 查找成就数据 + * @since Beta v0.4.7 + * @param {string} keyword 关键词 + * @returns {Promise} 成就数据 + */ +async function searchAchievements( + keyword: string, +): Promise { + if (keyword === "") return await getAchievements(); + const db = await TGSqlite.getDB(); + const versionReg = /^v\d+(\.\d+)?$/; + if (versionReg.test(keyword)) { + return await db.select( + "SELECT * FROM Achievements WHERE version LIKE ? ORDER BY isCompleted,`order`;", + [keyword], + ); + } + return await db.select( + "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} + */ +async function updateAchievement(data: TGApp.Sqlite.Achievement.SingleTable): Promise { + 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} + */ +async function getUIAF(): Promise { + const db = await TGSqlite.getDB(); + const data = await db.select("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} + */ +async function mergeUIAF(data: TGApp.Plugins.UIAF.Achievement[]): Promise { + 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; diff --git a/src/plugins/Sqlite/modules/userGacha.ts b/src/plugins/Sqlite/modules/userGacha.ts index 5fa2724e..506fde0e 100644 --- a/src/plugins/Sqlite/modules/userGacha.ts +++ b/src/plugins/Sqlite/modules/userGacha.ts @@ -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); } diff --git a/src/plugins/Sqlite/sql/updateData.ts b/src/plugins/Sqlite/sql/updateData.ts index 8bedb46b..f415e6ad 100644 --- a/src/plugins/Sqlite/sql/updateData.ts +++ b/src/plugins/Sqlite/sql/updateData.ts @@ -7,43 +7,39 @@ 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) => { - let sql; - // 获取完成状态 - const isCompleted = achievement.status === 2 || achievement.status === 3; - if (isCompleted) { - const completedTime = new Date(achievement.timestamp * 1000) - .toISOString() - .replace("T", " ") - .slice(0, 19); - sql = ` - UPDATE Achievements - SET isCompleted = 1, - completedTime = '${completedTime}', - progress = ${achievement.current}, - updated = datetime('now', 'localtime') - WHERE id = ${achievement.id} - AND (isCompleted = 0 OR completedTime != '${completedTime}' OR progress != ${achievement.current}); - `; - } else { - sql = ` +export function importUIAFData(data: TGApp.Plugins.UIAF.Achievement): string[] { + let sql; + const isCompleted = data.status === 2 || data.status === 3; + if (isCompleted) { + const completedTime = new Date(data.timestamp * 1000) + .toISOString() + .replace("T", " ") + .slice(0, 19); + sql = ` + UPDATE Achievements + SET isCompleted = 1, + completedTime = '${completedTime}', + progress = ${data.current}, + updated = datetime('now', 'localtime') + WHERE id = ${data.id} + AND (isCompleted = 0 OR completedTime != '${completedTime}' + OR progress != ${data.current}); + `; + } else { + sql = ` UPDATE Achievements SET progress = ${achievement.current}, updated = datetime('now', 'localtime') WHERE id = ${achievement.id} AND progress != ${achievement.current}; `; - } - return sqlRes.push(minifySql(sql)); - }); - return sqlRes; + } + return minifySql(sql); } /** diff --git a/src/types/Plugins/UIAF.d.ts b/src/types/Plugins/UIAF.d.ts index 4a847244..37d22f31 100644 --- a/src/types/Plugins/UIAF.d.ts +++ b/src/types/Plugins/UIAF.d.ts @@ -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; diff --git a/src/types/Sqlite/Achievement.d.ts b/src/types/Sqlite/Achievement.d.ts index bd45c4cc..c21e2e55 100644 --- a/src/types/Sqlite/Achievement.d.ts +++ b/src/types/Sqlite/Achievement.d.ts @@ -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; + } } diff --git a/src/utils/UIAF.ts b/src/utils/UIAF.ts index 55578283..261fb03a 100644 --- a/src/utils/UIAF.ts +++ b/src/utils/UIAF.ts @@ -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 { } /** - * @description 检测是否存在 UIAF 数据 - * @description 粗略检测,不保证数据完整性 - * @since Alpha v0.2.3 + * @description 检测是否存在 UIAF 数据,采用 ajv 验证 schema + * @since Beta v0.4.7 * @param {string} path - UIAF 数据路径 * @returns {Promise} 是否存在 UIAF 数据 */ export async function verifyUiafData(path: string): Promise { 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 { + 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; + } } /** diff --git a/src/utils/UIGF.ts b/src/utils/UIGF.ts index 5056bc3c..9950a4ed 100644 --- a/src/utils/UIGF.ts +++ b/src/utils/UIGF.ts @@ -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 { 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 { 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; } } diff --git a/src/utils/dataBS.ts b/src/utils/dataBS.ts index 361a7548..89587ba8 100644 --- a/src/utils/dataBS.ts +++ b/src/utils/dataBS.ts @@ -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} */ @@ -20,8 +24,7 @@ export async function backUpUserData(dir: string): Promise { 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 { // 备份深渊数据 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} */ @@ -47,17 +55,18 @@ export async function restoreUserData(dir: string): Promise { }); 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 { }); } // 恢复 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 { }); } // 恢复深渊数据 - 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 { 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: "数据恢复成功",