完成成就导入

This commit is contained in:
BTMuli
2025-12-01 23:52:39 +08:00
committed by 目棃
parent 14c47369e7
commit 38f3301664
8 changed files with 133 additions and 115 deletions

View File

@@ -1,6 +1,5 @@
//! @file src/commands.rs //! 命令模块,负责处理命令
//! @desc 命令模块,负责处理命令 //! @since Beta v0.7.8
//! @since Beta v0.7.4
use tauri::{AppHandle, Emitter, Manager, WebviewWindowBuilder}; use tauri::{AppHandle, Emitter, Manager, WebviewWindowBuilder};
use tauri_utils::config::{WebviewUrl, WindowConfig}; use tauri_utils::config::{WebviewUrl, WindowConfig};
@@ -72,9 +71,13 @@ pub async fn get_dir_size(path: String) -> u64 {
} }
// 判断是否是管理员权限 // 判断是否是管理员权限
#[cfg(target_os = "windows")]
#[tauri::command] #[tauri::command]
pub fn is_in_admin() -> bool { pub fn is_in_admin() -> bool {
#[cfg(not(target_os = "windows"))]
{
Err("This function is only supported on Windows.".into())
}
use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::{ use windows_sys::Win32::Security::{
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, SID_IDENTIFIER_AUTHORITY, TOKEN_QUERY, AllocateAndInitializeSid, CheckTokenMembership, FreeSid, SID_IDENTIFIER_AUTHORITY, TOKEN_QUERY,
@@ -113,10 +116,14 @@ pub fn is_in_admin() -> bool {
} }
} }
// 在 Windows 上以管理员权限重启应用 // 以管理员权限重启应用
#[cfg(target_os = "windows")]
#[tauri::command] #[tauri::command]
pub fn run_with_admin() -> Result<(), String> { pub fn run_with_admin() -> Result<(), String> {
#[cfg(not(target_os = "windows"))]
{
Err("This function is only supported on Windows.".into())
}
use std::ffi::OsStr; use std::ffi::OsStr;
use std::iter::once; use std::iter::once;
use std::os::windows::ffi::OsStrExt; use std::os::windows::ffi::OsStrExt;

View File

@@ -104,6 +104,10 @@ fn read_exact_vec<R: Read>(r: &mut R, len: usize) -> io::Result<Vec<u8>> {
/// 调用 dll /// 调用 dll
#[tauri::command] #[tauri::command]
pub fn call_yae_dll(app_handle: AppHandle, game_path: String) -> () { pub fn call_yae_dll(app_handle: AppHandle, game_path: String) -> () {
#[cfg(not(target_os = "windows"))]
{
Err("This function is only supported on Windows.".into())
}
let dll_path = app_handle.path().resource_dir().unwrap().join("resources/YaeAchievementLib.dll"); let dll_path = app_handle.path().resource_dir().unwrap().join("resources/YaeAchievementLib.dll");
dbg!(&dll_path); dbg!(&dll_path);
// 0. 创建 YaeAchievementPipe 的 命名管道,获取句柄 // 0. 创建 YaeAchievementPipe 的 命名管道,获取句柄

View File

@@ -1,13 +1,12 @@
//! Yae 成就信息的 Protobuf 定义 //! Yae 成就信息的 Protobuf 定义
//! @since Beta v0.9.0 //! @since Beta v0.8.7
use prost::encoding::{decode_key, WireType}; use prost::encoding::{decode_key, WireType};
use prost::DecodeError; use prost::DecodeError;
use prost::Message; use prost::Message;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Cursor; use std::io::{Cursor, Read};
use std::io::Read;
#[derive(Clone, PartialEq, Message, Serialize)] #[derive(Clone, PartialEq, Message, Serialize)]
pub struct AchievementProtoFieldInfo { pub struct AchievementProtoFieldInfo {
@@ -106,15 +105,14 @@ pub struct AchievementInfo {
} }
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
pub struct AchiItemRes { pub struct UiafAchiItem {
pub id: u32, pub id: u32,
pub total: u32, pub current: u32,
pub cur: u32, pub timestamp: u32,
pub ts: u32, pub status: u32,
pub stat: u32,
} }
pub fn parse_achi_list(bytes: &[u8]) -> Result<Vec<AchiItemRes>, DecodeError> { pub fn parse_achi_list(bytes: &[u8]) -> Result<Vec<UiafAchiItem>, DecodeError> {
let mut cursor = Cursor::new(bytes); let mut cursor = Cursor::new(bytes);
let mut dicts: Vec<HashMap<u32, u32>> = Vec::new(); let mut dicts: Vec<HashMap<u32, u32>> = Vec::new();
@@ -146,13 +144,13 @@ pub fn parse_achi_list(bytes: &[u8]) -> Result<Vec<AchiItemRes>, DecodeError> {
let achievements = dicts let achievements = dicts
.into_iter() .into_iter()
.map(|d| AchiItemRes { .map(|d| UiafAchiItem {
id: d.get(&15).copied().unwrap_or(0), id: d.get(&15).copied().unwrap_or(0),
stat: d.get(&11).copied().unwrap_or(0), status: d.get(&11).copied().unwrap_or(0),
total: d.get(&8).copied().unwrap_or(0), current: d.get(&13).copied().unwrap_or(0),
cur: d.get(&13).copied().unwrap_or(0), timestamp: d.get(&7).copied().unwrap_or(0),
ts: d.get(&7).copied().unwrap_or(0),
}) })
.filter(|a| a.timestamp != 0)
.collect(); .collect();
Ok(achievements) Ok(achievements)

View File

@@ -7,7 +7,7 @@
* *
* @since Beta v0.7.8 * @since Beta v0.7.8
*/ */
export const YaeAchiStatEnum: typeof TGApp.Plugins.Yae.AchiItemStat = { export const UiafAchiStatEnum: typeof TGApp.Plugins.UIAF.AchiItemStat = {
/** 无效状态 */ /** 无效状态 */
Invalid: 0, Invalid: 0,
/** 未完成 */ /** 未完成 */

View File

@@ -79,7 +79,7 @@ import TSUserAchi from "@Sqlm/userAchi.js";
import useAppStore from "@store/app.js"; import useAppStore from "@store/app.js";
import { path } from "@tauri-apps/api"; import { path } from "@tauri-apps/api";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { listen, type UnlistenFn, type Event } from "@tauri-apps/api/event";
import { open, save } from "@tauri-apps/plugin-dialog"; import { open, save } from "@tauri-apps/plugin-dialog";
import { exists, writeTextFile } from "@tauri-apps/plugin-fs"; import { exists, writeTextFile } from "@tauri-apps/plugin-fs";
import { platform } from "@tauri-apps/plugin-os"; import { platform } from "@tauri-apps/plugin-os";
@@ -133,7 +133,14 @@ onMounted(async () => {
achiListener = await listen<void>("updateAchi", async () => await refreshOverview()); achiListener = await listen<void>("updateAchi", async () => await refreshOverview());
yaeListener = await listen<TGApp.Plugins.Yae.AchiListRes>( yaeListener = await listen<TGApp.Plugins.Yae.AchiListRes>(
"yae_achi_list", "yae_achi_list",
(e: Event<TGApp.Plugins.Yae.AchiListRes>) => tryParseYaeAchi(e.payload), async (e: Event<string>) => {
try {
await tryParseYaeAchi(JSON.parse(e.payload));
} catch (err) {
await TGLogger.Error(`[Achievements][yae_achi_list] 解析Yae成就数据失败: ${err}`);
showSnackbar.error(`解析Yae成就数据失败:${err}`);
}
},
); );
}); });
@@ -327,8 +334,28 @@ async function toYae(): Promise<void> {
} }
async function tryParseYaeAchi(payload: TGApp.Plugins.Yae.AchiListRes): Promise<void> { async function tryParseYaeAchi(payload: TGApp.Plugins.Yae.AchiListRes): Promise<void> {
console.log(payload); console.log(typeof payload, payload);
showSnackbar.success(`已收到来自Yae的成就数据${payload.list.length}`); if (payload.length === 0) {
showSnackbar.warn(`未从Yae获取到成就数据`);
return;
}
const input = await showDialog.input("请输入存档UID", "UID:", uidCur.value.toString());
if (!input) {
showSnackbar.cancel("已取消存档导入");
return;
}
if (input === "" || isNaN(Number(input))) {
showSnackbar.warn("请输入合法数字");
return;
}
await showLoading.start("正在导入成就数据", `UID:${input},数量:${payload.length}`);
await TSUserAchi.mergeUiaf(payload, input);
await showLoading.end();
showSnackbar.success("导入成功,即将刷新页面");
await TGLogger.Info("[Achievements][handleImportOuter] 导入成功");
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
await router.push("/achievements");
window.location.reload();
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,9 +1,9 @@
/** /**
* @file plugins/Sqlite/modules/userAchi.ts * 用户成就模块
* @description 用户成就模块 * @since Beta v0.8.7
* @since Beta v0.6.0
*/ */
import { UiafAchiStatEnum } from "@enum/uiaf.js";
import { path } from "@tauri-apps/api"; import { path } from "@tauri-apps/api";
import { exists, mkdir, readDir, readTextFile, writeTextFile } from "@tauri-apps/plugin-fs"; import { exists, mkdir, readDir, readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import TGLogger from "@utils/TGLogger.js"; import TGLogger from "@utils/TGLogger.js";
@@ -155,11 +155,16 @@ async function getAchievements(
} }
/** /**
* @description 查找成就数据 * 查找成就数据
* @since Beta v0.6.0 * @since Beta v0.8.7
* @remarks
* 支持三种搜索方式:
* 1. 版本搜索:输入 vx.x 格式的关键词(如 v1.2),搜索对应版本的成就
* 2. ID搜索输入 ixxx 格式的关键词(如 i1001搜索对应ID的成就
* 3. 名称/描述搜索:输入任意关键词,搜索成就名称或描述中包含该关键词的成就
* @param {number} uid - 存档 UID * @param {number} uid - 存档 UID
* @param {string} keyword - 关键词 * @param {string} keyword - 关键词
* @returns {Promise<TGApp.Sqlite.Achievement.RenderAchi[]>} 成就数据 * @returns {Promise<Array<TGApp.Sqlite.Achievement.RenderAchi>>} 成就数据
*/ */
async function searchAchi( async function searchAchi(
uid: number, uid: number,
@@ -167,11 +172,15 @@ async function searchAchi(
): Promise<TGApp.Sqlite.Achievement.RenderAchi[]> { ): Promise<TGApp.Sqlite.Achievement.RenderAchi[]> {
if (keyword === "") return await getAchievements(uid); if (keyword === "") return await getAchievements(uid);
const versionReg = /^v\d+(\.\d+)?$/; const versionReg = /^v\d+(\.\d+)?$/;
let rawData: TGApp.App.Achievement.Item[]; const idReg = /^i\d+$/;
const res: TGApp.Sqlite.Achievement.RenderAchi[] = []; let rawData: Array<TGApp.App.Achievement.Item>;
const res: Array<TGApp.Sqlite.Achievement.RenderAchi> = [];
if (versionReg.test(keyword)) { if (versionReg.test(keyword)) {
const version = keyword.replace("v", ""); const version = keyword.replace("v", "");
rawData = AppAchievementsData.filter((i) => i.version.includes(version)); rawData = AppAchievementsData.filter((i) => i.version.includes(version));
} else if (idReg.test(keyword)) {
const id = parseInt(keyword.replace("i", ""));
rawData = AppAchievementsData.filter((a) => a.id === id);
} else { } else {
rawData = AppAchievementsData.filter((a) => { rawData = AppAchievementsData.filter((a) => {
if (a.name.includes(keyword)) return true; if (a.name.includes(keyword)) return true;
@@ -312,16 +321,19 @@ async function restoreUiaf(dir: string): Promise<boolean> {
} }
/** /**
* @description 导入Uiaf数据 * 导入Uiaf数据
* @since Beta v0.6.0 * @since Beta v0.7.8
* @param {TGApp.Plugins.UIAF.Achievement[]} data - 成就数据 * @param {Array<TGApp.Plugins.UIAF.Achievement>} data - 成就数据
* @param {number} uid - 存档UID * @param {number} uid - 存档UID
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function mergeUiaf(data: TGApp.Plugins.UIAF.Achievement[], uid: number): Promise<void> { async function mergeUiaf(data: Array<TGApp.Plugins.UIAF.Achievement>, uid: number): Promise<void> {
const db = await TGSqlite.getDB(); const db = await TGSqlite.getDB();
for (const achi of data) { for (const achi of data) {
const status = achi.status === 2 || achi.status === 3 ? 1 : 0; const status =
achi.status === UiafAchiStatEnum.Finished || achi.status === UiafAchiStatEnum.RewardTaken
? 1
: 0;
const timeC = status === 1 ? timestampToDate(achi.timestamp * 1000) : ""; const timeC = status === 1 ? timestampToDate(achi.timestamp * 1000) : "";
const timeN = timestampToDate(new Date().getTime()); const timeN = timestampToDate(new Date().getTime());
await db.execute( await db.execute(

View File

@@ -1,60 +1,69 @@
/** /**
* @file types/Plugins/UIAF.d.ts * UIAF 插件类型定义文件
* @description UIAF 插件类型定义文件 * @since Beta v0.7.8
* @since Beta v0.6.0
*/ */
/**
* @description UIAF 插件类型命名空间
* @namespace TGApp.Plugins.UIAF
* @merberof TGApp.Plugins
* @since Beta v0.6.0
*/
declare namespace TGApp.Plugins.UIAF { declare namespace TGApp.Plugins.UIAF {
/** /**
* @interface Data * UIAF完整数据
* @since Alpha v0.1.5 * @since Alpha v0.1.5
* @description UIAF 成就数据
* @property {Export} info UIAF 头部信息
* @property {Achievement[]} list UIAF 成就列表
* @return Data
*/ */
interface Data { type Data = {
/* UIAF 头部信息 */
info: Export; info: Export;
list: Achievement[]; /* UIAF 成就列表 */
} list: Array<Achievement>;
};
/** /**
* @interface Export * UIAF 头部信息
* @since Alpha v0.1.5 * @since Alpha v0.1.5
* @description UIAF 头部信息
* @property {string} export_app 导出的应用名称
* @property {number} export_timestamp 导出时间戳,正确时间戳得乘以 1000
* @property {string} export_app_version 导出的应用版本
* @property {string} uiaf_version UIAF 版本
* @return Export
*/ */
interface Export { type Export = {
/* 导出的应用名称 */
export_app: string; export_app: string;
/* 导出时间戳,秒级 */
export_timestamp: number; export_timestamp: number;
/* 导出的应用版本 */
export_app_version: string; export_app_version: string;
/* UIAF 版本 */
uiaf_version: string; uiaf_version: string;
} };
/** /**
* @interface Achievement * 成就完成状态
* @since Alpha v0.1.5 * @since Beta v0.7.8
* @description UIAF 单个成就数据
* @property {number} id 成就 ID
* @property {number} timestamp 成就记录时间戳,正确时间戳得乘以 1000
* @property {number} current 成就进度
* @property {number} status 成就状态0 为未完成1 为已完成
* @return Achievement
*/ */
interface Achievement { const AchiItemStat = <const>{
/* 无效状态 */
Invalid: 0,
/* 未完成 */
Unfinished: 1,
/* 已完成未领取奖励 */
Finished: 2,
/* 已领取奖励 */
RewardTaken: 3,
};
/**
* 成就完成状态枚举
* @since Beta v0.7.8
*/
type AchiItemStatEnum = (typeof AchiItemStat)[keyof typeof AchiItemStat];
/**
* 成就信息
* @since Beta v0.7.8
* @description UIAF 单个成就数据
*/
type Achievement = {
/* 成就 ID */
id: number; id: number;
/* 成就记录时间戳,秒级 */
timestamp: number; timestamp: number;
/* 成就进度 */
current: number; current: number;
status: number; /* 成就状态 */
} status: AchiItemStatEnum;
};
} }

View File

@@ -8,44 +8,5 @@ declare namespace TGApp.Plugins.Yae {
* 后端返的成就列表数据 * 后端返的成就列表数据
* @since Beta v0.7.8 * @since Beta v0.7.8
*/ */
type AchiListRes = Array<AchiItemRes>; type AchiListRes = Array<TGApp.Plugins.UIAF.Achievement>;
/**
* 成就完成状态
* @since Beta v0.7.8
*/
const AchiItemStat = <const>{
/* 无效状态 */
Invalid: 0,
/* 未完成 */
Unfinished: 1,
/* 已完成未领取奖励 */
Finished: 2,
/* 已领取奖励 */
RewardTaken: 3,
};
/**
* 成就完成状态m枚举
* @since Beta v0.7.8
*/
type AchiItemStatEnum = (typeof AchiItemStat)[keyof typeof AchiItemStat];
/**
* 后端返的单个成就数据
* @since Beta v0.7.8
* @see src-tauri/yae/proto.rs AchiItemRes
*/
type AchiItemRes = {
/* 成就 ID */
id: number;
/* 成就总进度 */
total: number;
/* 成就当前进度 */
cur: number;
/* 完成时间戳,单位秒 */
ts: number;
/* 成就完成状态 */
stat: AchiItemStatEnum;
};
} }