From c6ba952413c2f3fcaebc4db31104b78eaba68de0 Mon Sep 17 00:00:00 2001 From: Joe Bao Date: Mon, 8 Jun 2026 16:35:02 +1000 Subject: [PATCH 1/4] feat: add local update package import functionality and UI support --- src-tauri/Cargo.lock | 1 + src/App.tsx | 3 + src/components/GlobalUpdateDropOverlay.tsx | 72 +++++++++++++ src/components/settings/UpdateSection.tsx | 51 ++++++++++ src/hooks/useLocalUpdatePackageImport.ts | 113 +++++++++++++++++++++ src/i18n/locales/en-US.ts | 16 +++ src/i18n/locales/ja-JP.ts | 16 +++ src/i18n/locales/ko-KR.ts | 16 +++ src/i18n/locales/zh-CN.ts | 16 +++ src/i18n/locales/zh-TW.ts | 16 +++ src/services/updateService.ts | 92 ++++++++++++++++- 11 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 src/components/GlobalUpdateDropOverlay.tsx create mode 100644 src/hooks/useLocalUpdatePackageImport.ts diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 322246ca..76e6bd5e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2640,6 +2640,7 @@ dependencies = [ "semver", "serde", "serde_json", + "sha2", "shell-words", "tar", "tauri", diff --git a/src/App.tsx b/src/App.tsx index cea49cb9..c6c6db0e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -82,6 +82,7 @@ import { import { LoadingScreen } from './components/app'; import { ConnectionLostOverlay } from './components/app/ConnectionLostOverlay'; import { WebUIBetaBanner } from './components/app/WebUIBetaBanner'; +import { GlobalUpdateDropOverlay } from './components/GlobalUpdateDropOverlay'; import { startGlobalCallbackListener } from './components/connection/callbackCache'; import { useIsMobile } from '@/hooks/useIsMobile'; import { ScrollText } from 'lucide-react'; @@ -1677,6 +1678,7 @@ function App() { className={`h-full flex flex-col bg-bg-primary relative ${backgroundImageDataUrl ? 'has-background-image' : ''}`} > +
@@ -1760,6 +1762,7 @@ function App() { className={`h-full flex flex-col bg-bg-primary relative ${backgroundImageDataUrl ? 'has-background-image' : ''}`} > +
{/* WebUI 模式下的连接断开覆盖层 */} diff --git a/src/components/GlobalUpdateDropOverlay.tsx b/src/components/GlobalUpdateDropOverlay.tsx new file mode 100644 index 00000000..d78602ab --- /dev/null +++ b/src/components/GlobalUpdateDropOverlay.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react'; +import { Loader2, UploadCloud } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useLocalUpdatePackageImport } from '@/hooks/useLocalUpdatePackageImport'; +import { isTauri } from '@/utils/windowUtils'; + +export function GlobalUpdateDropOverlay() { + const { t } = useTranslation(); + const [draggingFile, setDraggingFile] = useState(false); + const { importSinglePackage, disabled } = useLocalUpdatePackageImport(); + + useEffect(() => { + if (!isTauri()) return; + + let unlisten: (() => void) | undefined; + let mounted = true; + + const setup = async () => { + try { + const { getCurrentWebview } = await import('@tauri-apps/api/webview'); + const webview = getCurrentWebview(); + unlisten = await webview.onDragDropEvent((event) => { + const payload = event.payload; + + if (payload.type === 'over') { + setDraggingFile(true); + return; + } + + if (payload.type === 'drop') { + setDraggingFile(false); + if (!disabled) { + void importSinglePackage(payload.paths); + } + return; + } + + setDraggingFile(false); + }); + } catch { + if (mounted) setDraggingFile(false); + } + }; + + void setup(); + + return () => { + mounted = false; + unlisten?.(); + }; + }, [disabled, importSinglePackage]); + + if (!draggingFile) return null; + + return ( +
+
+ {disabled ? ( + + ) : ( + + )} +
+

+ {disabled ? t('mirrorChyan.localPackageBusy') : t('mirrorChyan.dropLocalPackage')} +

+

{t('mirrorChyan.localPackageHint')}

+
+
+
+ ); +} diff --git a/src/components/settings/UpdateSection.tsx b/src/components/settings/UpdateSection.tsx index 493ff1a6..353e33da 100644 --- a/src/components/settings/UpdateSection.tsx +++ b/src/components/settings/UpdateSection.tsx @@ -13,6 +13,7 @@ import { PackageCheck, Bug, Network, + FileUp, } from 'lucide-react'; import clsx from 'clsx'; @@ -25,12 +26,15 @@ import { cancelDownload, MIRRORCHYAN_ERROR_CODES, isDebugVersion, + savePendingUpdateInfo, } from '@/services/updateService'; import { createProxySettings, proxySettingsForUpdateDownload } from '@/services/proxyService'; import { resolveI18nText } from '@/services/contentResolver'; import { getInterfaceLangKey } from '@/i18n'; import { loggers } from '@/utils/logger'; import { ReleaseNotes, DownloadProgressBar } from '../UpdateInfoCard'; +import { useLocalUpdatePackageImport } from '@/hooks/useLocalUpdatePackageImport'; +import { isTauri } from '@/utils/windowUtils'; export function UpdateSection() { const { t } = useTranslation(); @@ -65,6 +69,11 @@ export function UpdateSection() { const [proxyError, setProxyError] = useState(false); const [checkFailed, setCheckFailed] = useState(false); const [, setDebugLog] = useState([]); + const { + importSinglePackage, + supportedExtensions, + disabled: localPackageImportDisabled, + } = useLocalUpdatePackageImport(); const addDebugLog = useCallback((msg: string) => { setDebugLog((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]); @@ -152,6 +161,16 @@ export function UpdateSection() { // 使用实际保存路径(可能与请求路径不同,如果从 302 重定向检测到正确文件名) setDownloadSavePath(result.actualSavePath); setDownloadStatus('completed'); + savePendingUpdateInfo({ + versionName: info.versionName, + releaseNote: info.releaseNote, + channel: info.channel, + downloadSavePath: result.actualSavePath, + fileSize: info.fileSize, + updateType: info.updateType, + downloadSource: info.downloadSource, + timestamp: Date.now(), + }); } else { setDownloadStatus('failed'); } @@ -357,6 +376,22 @@ export function UpdateSection() { } }; + const handleSelectLocalPackage = useCallback(async () => { + const { open } = await import('@tauri-apps/plugin-dialog'); + const selected = await open({ + multiple: false, + filters: [ + { + name: t('mirrorChyan.localPackageFilter'), + extensions: supportedExtensions.map((ext) => ext.replace(/^\./, '')), + }, + ], + }); + + if (!selected || Array.isArray(selected)) return; + await importSinglePackage([selected]); + }, [importSinglePackage, supportedExtensions, t]); + if (!projectInterface?.mirrorchyan_rid) { return null; } @@ -526,6 +561,22 @@ export function UpdateSection() { )} {/* 更新状态显示 */} + {isTauri() && downloadStatus !== 'downloading' && installStatus !== 'installing' && ( + + )} + {updateInfo && !updateInfo.hasUpdate && !updateInfo.errorCode && (

{t('mirrorChyan.upToDate', { version: updateInfo.versionName })} diff --git a/src/hooks/useLocalUpdatePackageImport.ts b/src/hooks/useLocalUpdatePackageImport.ts new file mode 100644 index 00000000..334733dc --- /dev/null +++ b/src/hooks/useLocalUpdatePackageImport.ts @@ -0,0 +1,113 @@ +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; +import { useAppStore } from '@/stores/appStore'; +import { + importLocalUpdatePackage, + LocalUpdatePackageError, + getSupportedUpdatePackageExtensions, + savePendingUpdateInfo, +} from '@/services/updateService'; +import { loggers } from '@/utils/logger'; + +export function useLocalUpdatePackageImport() { + const { t } = useTranslation(); + const { + projectInterface, + mirrorChyanSettings, + downloadStatus, + installStatus, + setUpdateInfo, + setUpdateCheckLoading, + setDownloadStatus, + setDownloadProgress, + setDownloadSavePath, + setShowInstallConfirmModal, + } = useAppStore(); + + const getErrorMessage = useCallback( + (error: unknown) => { + if (error instanceof LocalUpdatePackageError) { + return t(`mirrorChyan.localPackageErrors.${error.code}`); + } + return error instanceof Error ? error.message : t('mirrorChyan.localPackageErrors.checkFailed'); + }, + [t], + ); + + const importPackage = useCallback( + async (filePath: string) => { + const toastId = toast.loading(t('mirrorChyan.verifyingLocalPackage')); + setUpdateCheckLoading(true); + + try { + const updateInfo = await importLocalUpdatePackage({ + filePath, + projectInterface, + cdk: mirrorChyanSettings.cdk || undefined, + channel: mirrorChyanSettings.channel, + }); + + setUpdateInfo(updateInfo); + setDownloadSavePath(filePath); + setDownloadProgress({ + downloadedSize: updateInfo.fileSize || 0, + totalSize: updateInfo.fileSize || 0, + speed: 0, + progress: 100, + }); + setDownloadStatus('completed'); + savePendingUpdateInfo({ + versionName: updateInfo.versionName, + releaseNote: updateInfo.releaseNote, + channel: updateInfo.channel, + downloadSavePath: filePath, + fileSize: updateInfo.fileSize, + updateType: updateInfo.updateType, + downloadSource: updateInfo.downloadSource, + timestamp: Date.now(), + }); + setShowInstallConfirmModal(true); + toast.success(t('mirrorChyan.localPackageReady', { version: updateInfo.versionName }), { + id: toastId, + }); + } catch (error) { + loggers.ui.error('Local update package import failed:', error); + toast.error(getErrorMessage(error), { id: toastId }); + } finally { + setUpdateCheckLoading(false); + } + }, + [ + projectInterface, + mirrorChyanSettings.cdk, + mirrorChyanSettings.channel, + setUpdateInfo, + setUpdateCheckLoading, + setDownloadStatus, + setDownloadProgress, + setDownloadSavePath, + setShowInstallConfirmModal, + getErrorMessage, + t, + ], + ); + + const importSinglePackage = useCallback( + async (filePaths: string[]) => { + if (filePaths.length !== 1) { + toast.error(t('mirrorChyan.localPackageErrors.multipleFiles')); + return; + } + await importPackage(filePaths[0]); + }, + [importPackage, t], + ); + + return { + importPackage, + importSinglePackage, + supportedExtensions: getSupportedUpdatePackageExtensions(), + disabled: downloadStatus === 'downloading' || installStatus === 'installing', + }; +} diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index d4f88073..08a7edca 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -655,6 +655,22 @@ export default { preparingDownload: 'Preparing download...', downloadFromGitHub: 'Download from GitHub', downloadFromMirrorChyan: 'Download via MirrorChyan CDN', + selectLocalPackage: 'Select Local Update Package', + localPackageFilter: 'Update Package', + verifyingLocalPackage: 'Verifying update package...', + localPackageReady: 'Update package verified. Ready to install {{version}}', + dropLocalPackage: 'Release to verify update package', + localPackageBusy: 'Update is busy. Please try again later', + localPackageHint: 'Supports zip, tar.gz, tgz, exe, dmg', + localPackageErrors: { + unsupportedFile: 'Unsupported update package format', + missingProjectInfo: 'This project has no update metadata', + debugMode: 'Updates are disabled for debug builds', + busy: 'An update is currently downloading or installing', + checkFailed: 'Failed to check for updates. Please try again later', + noUpdate: 'There is no new version to install', + multipleFiles: 'Drop one update package at a time', + }, // Update installation installing: 'Installing update...', installComplete: 'Installation Complete', diff --git a/src/i18n/locales/ja-JP.ts b/src/i18n/locales/ja-JP.ts index 8cd76746..1802d18d 100644 --- a/src/i18n/locales/ja-JP.ts +++ b/src/i18n/locales/ja-JP.ts @@ -653,6 +653,22 @@ export default { preparingDownload: 'ダウンロードを準備中...', downloadFromGitHub: 'GitHub からダウンロード', downloadFromMirrorChyan: 'Mirror醤 CDN からダウンロード', + selectLocalPackage: 'ローカル更新パッケージを選択', + localPackageFilter: '更新パッケージ', + verifyingLocalPackage: '更新パッケージを検証中...', + localPackageReady: '更新パッケージを検証しました。{{version}} をインストールできます', + dropLocalPackage: 'ドロップして更新パッケージを検証', + localPackageBusy: '更新処理中です。後でもう一度お試しください', + localPackageHint: 'zip、tar.gz、tgz、exe、dmg に対応', + localPackageErrors: { + unsupportedFile: 'サポートされていない更新パッケージ形式です', + missingProjectInfo: 'このプロジェクトには更新情報が設定されていません', + debugMode: 'デバッグビルドでは更新機能が無効です', + busy: '更新をダウンロードまたはインストール中です', + checkFailed: '更新の確認に失敗しました。後でもう一度お試しください', + noUpdate: 'インストール可能な新しいバージョンはありません', + multipleFiles: '更新パッケージは 1 つずつドロップしてください', + }, // アップデートインストール installing: 'アップデートをインストール中...', installComplete: 'インストール完了', diff --git a/src/i18n/locales/ko-KR.ts b/src/i18n/locales/ko-KR.ts index b021fcf5..c1767214 100644 --- a/src/i18n/locales/ko-KR.ts +++ b/src/i18n/locales/ko-KR.ts @@ -647,6 +647,22 @@ export default { preparingDownload: '다운로드 준비 중...', downloadFromGitHub: 'GitHub에서 다운로드', downloadFromMirrorChyan: 'Mirror짱 CDN에서 다운로드', + selectLocalPackage: '로컬 업데이트 패키지 선택', + localPackageFilter: '업데이트 패키지', + verifyingLocalPackage: '업데이트 패키지 확인 중...', + localPackageReady: '업데이트 패키지가 확인되었습니다. {{version}} 설치 가능', + dropLocalPackage: '놓아서 업데이트 패키지 확인', + localPackageBusy: '업데이트 처리 중입니다. 나중에 다시 시도하세요', + localPackageHint: 'zip, tar.gz, tgz, exe, dmg 지원', + localPackageErrors: { + unsupportedFile: '지원하지 않는 업데이트 패키지 형식입니다', + missingProjectInfo: '현재 프로젝트에 업데이트 정보가 설정되어 있지 않습니다', + debugMode: '디버그 빌드에서는 업데이트가 비활성화됩니다', + busy: '업데이트를 다운로드하거나 설치하는 중입니다', + checkFailed: '업데이트 확인에 실패했습니다. 나중에 다시 시도하세요', + noUpdate: '설치할 새 버전이 없습니다', + multipleFiles: '업데이트 패키지는 한 번에 하나만 놓으세요', + }, // 업데이트 설치 installing: '업데이트 설치 중...', installComplete: '설치 완료', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 0359f807..4623d265 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -638,6 +638,22 @@ export default { preparingDownload: '准备下载...', downloadFromGitHub: '通过 海外渠道(GitHub)下载', downloadFromMirrorChyan: '通过 Mirror酱 CDN 下载', + selectLocalPackage: '选择本地更新包', + localPackageFilter: '更新包', + verifyingLocalPackage: '正在校验更新包...', + localPackageReady: '更新包已校验,可安装 {{version}}', + dropLocalPackage: '释放以校验更新包', + localPackageBusy: '当前正在更新,请稍后再试', + localPackageHint: '支持 zip、tar.gz、tgz、exe、dmg', + localPackageErrors: { + unsupportedFile: '不支持的更新包格式', + missingProjectInfo: '当前项目未配置更新信息', + debugMode: '调试版本已禁用更新功能', + busy: '当前正在下载或安装更新', + checkFailed: '检查更新失败,请稍后重试', + noUpdate: '当前没有可安装的新版本', + multipleFiles: '一次只能拖入一个更新包', + }, // 更新安装 installing: '正在安装更新...', installComplete: '安装完成', diff --git a/src/i18n/locales/zh-TW.ts b/src/i18n/locales/zh-TW.ts index f954a9ab..571d99c2 100644 --- a/src/i18n/locales/zh-TW.ts +++ b/src/i18n/locales/zh-TW.ts @@ -634,6 +634,22 @@ export default { preparingDownload: '準備下載...', downloadFromGitHub: '透過 海外渠道(GitHub)下載', downloadFromMirrorChyan: '透過 Mirror酱 CDN 下載', + selectLocalPackage: '選擇本機更新包', + localPackageFilter: '更新包', + verifyingLocalPackage: '正在校驗更新包...', + localPackageReady: '更新包已校驗,可安裝 {{version}}', + dropLocalPackage: '放開以校驗更新包', + localPackageBusy: '目前正在更新,請稍後再試', + localPackageHint: '支援 zip、tar.gz、tgz、exe、dmg', + localPackageErrors: { + unsupportedFile: '不支援的更新包格式', + missingProjectInfo: '目前專案未設定更新資訊', + debugMode: '除錯版本已停用更新功能', + busy: '目前正在下載或安裝更新', + checkFailed: '檢查更新失敗,請稍後重試', + noUpdate: '目前沒有可安裝的新版本', + multipleFiles: '一次只能拖入一個更新包', + }, // 更新安裝 installing: '正在安裝更新...', installComplete: '安裝完成', diff --git a/src/services/updateService.ts b/src/services/updateService.ts index 921e259e..1d545140 100644 --- a/src/services/updateService.ts +++ b/src/services/updateService.ts @@ -3,6 +3,7 @@ import type { DownloadProgress, UpdateInfo } from '@/stores/appStore'; import type { ProxySettings, UpdateChannel } from '@/types/config'; +import type { ProjectInterface } from '@/types/interface'; import { loggers } from '@/utils/logger'; import { getCacheDir, joinPath } from '@/utils/paths'; import { invoke } from '@tauri-apps/api/core'; @@ -93,6 +94,26 @@ export const MIRRORCHYAN_ERROR_CODES = { } as const; // MirrorChyan API 响应类型 +export type LocalUpdatePackageErrorCode = + | 'unsupportedFile' + | 'missingProjectInfo' + | 'debugMode' + | 'busy' + | 'checkFailed' + | 'noUpdate'; + +export class LocalUpdatePackageError extends Error { + public readonly code: LocalUpdatePackageErrorCode; + + constructor(code: LocalUpdatePackageErrorCode, message?: string) { + super(message || code); + this.name = 'LocalUpdatePackageError'; + this.code = code; + } +} + +const SUPPORTED_UPDATE_PACKAGE_EXTENSIONS = ['.zip', '.tar.gz', '.tgz', '.exe', '.dmg']; + interface MirrorChyanApiResponse { code: number; msg: string; @@ -100,7 +121,6 @@ interface MirrorChyanApiResponse { version_name: string; version_number?: number; url?: string; - sha256?: string; release_note?: string; custom_data?: string; update_type?: 'incremental' | 'full'; @@ -163,6 +183,19 @@ function extractFilenameFromUrl(url: string): string | undefined { } } +function getFilenameFromPath(path: string): string { + return path.split(/[/\\]/).pop() || path; +} + +export function isSupportedUpdatePackage(filePath: string): boolean { + const lower = filePath.toLowerCase(); + return SUPPORTED_UPDATE_PACKAGE_EXTENSIONS.some((ext) => lower.endsWith(ext)); +} + +export function getSupportedUpdatePackageExtensions(): string[] { + return [...SUPPORTED_UPDATE_PACKAGE_EXTENSIONS]; +} + // 获取 OS 的常见别名(用于匹配文件名) function getOSAliases(): string[] { const os = getOS(); @@ -904,6 +937,63 @@ export async function getUpdateSavePath(filename?: string): Promise { return joinPath(cacheDir, name); } +export interface ImportLocalUpdatePackageOptions { + filePath: string; + projectInterface: ProjectInterface | null; + cdk?: string; + channel?: UpdateChannel; +} + +export async function importLocalUpdatePackage({ + filePath, + projectInterface, + cdk, + channel = 'stable', +}: ImportLocalUpdatePackageOptions): Promise { + if (!isSupportedUpdatePackage(filePath)) { + throw new LocalUpdatePackageError('unsupportedFile'); + } + + if ( + !projectInterface?.mirrorchyan_rid || + !projectInterface.version || + !projectInterface.name + ) { + throw new LocalUpdatePackageError('missingProjectInfo'); + } + + if (import.meta.env.DEV || isDebugVersion(projectInterface.version)) { + throw new LocalUpdatePackageError('debugMode'); + } + + if (isDownloading || isInstalling) { + throw new LocalUpdatePackageError('busy'); + } + + const updateInfo = await checkUpdate({ + resourceId: projectInterface.mirrorchyan_rid, + currentVersion: projectInterface.version, + cdk: cdk || undefined, + channel, + userAgent: 'MXU', + }); + + if (!updateInfo) { + throw new LocalUpdatePackageError('checkFailed'); + } + + if (!updateInfo.hasUpdate) { + throw new LocalUpdatePackageError('noUpdate'); + } + + return { + ...updateInfo, + downloadUrl: undefined, + filename: getFilenameFromPath(filePath), + downloadSource: undefined, + }; +} + // ============================================================================ // 更新安装相关 // ============================================================================ From d24f9c3b443bc7ade578f89fe379e27466c855e5 Mon Sep 17 00:00:00 2001 From: Joe Bao Date: Mon, 8 Jun 2026 20:06:52 +1000 Subject: [PATCH 2/4] feat: implement local update package interface reading and related functionality --- src-tauri/Cargo.lock | 1 - src-tauri/src/commands/update.rs | 77 ++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src/App.tsx | 3 - src/components/GlobalUpdateDropOverlay.tsx | 72 -------------------- src/components/settings/UpdateSection.tsx | 7 +- src/hooks/useLocalUpdatePackageImport.ts | 37 ++++++----- src/i18n/locales/en-US.ts | 5 +- src/i18n/locales/ja-JP.ts | 5 +- src/i18n/locales/ko-KR.ts | 5 +- src/i18n/locales/zh-CN.ts | 5 +- src/i18n/locales/zh-TW.ts | 5 +- src/services/updateService.ts | 50 +++++++------- 13 files changed, 128 insertions(+), 145 deletions(-) delete mode 100644 src/components/GlobalUpdateDropOverlay.tsx diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 76e6bd5e..322246ca 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2640,7 +2640,6 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", "shell-words", "tar", "tauri", diff --git a/src-tauri/src/commands/update.rs b/src-tauri/src/commands/update.rs index 5c2fb955..830a75b7 100644 --- a/src-tauri/src/commands/update.rs +++ b/src-tauri/src/commands/update.rs @@ -3,6 +3,7 @@ //! 提供解压、增量/全量更新、文件移动等功能 use log::{info, warn}; +use std::io::Read; use super::file_ops::get_exe_dir; use super::types::ChangesJson; @@ -22,6 +23,82 @@ pub fn extract_zip(zip_path: String, dest_dir: String) -> Result<(), String> { } } +/// 读取更新包中的 interface.json +#[tauri::command] +pub fn read_update_package_interface(package_path: String) -> Result { + info!("read_update_package_interface called: {}", package_path); + + let path_lower = package_path.to_lowercase(); + if path_lower.ends_with(".zip") { + read_interface_from_zip(&package_path) + } else if path_lower.ends_with(".tar.gz") || path_lower.ends_with(".tgz") { + read_interface_from_tar_gz(&package_path) + } else { + Err("不支持的更新包格式".to_string()) + } +} + +fn is_interface_json_path(path: &str) -> bool { + path.replace('\\', "/") + .rsplit('/') + .next() + .is_some_and(|name| name.eq_ignore_ascii_case("interface.json")) +} + +fn read_interface_from_zip(zip_path: &str) -> Result { + let file = std::fs::File::open(zip_path) + .map_err(|e| format!("无法打开 ZIP 文件 [{}]: {}", zip_path, e))?; + let mut archive = + zip::ZipArchive::new(file).map_err(|e| format!("无法解析 ZIP 文件: {}", e))?; + + for i in 0..archive.len() { + let mut file = archive + .by_index(i) + .map_err(|e| format!("无法读取 ZIP 条目 {}: {}", i, e))?; + if file.is_dir() || !is_interface_json_path(file.name()) { + continue; + } + + let mut content = String::new(); + file.read_to_string(&mut content) + .map_err(|e| format!("无法读取 interface.json: {}", e))?; + return Ok(content); + } + + Err("更新包内未找到 interface.json".to_string()) +} + +fn read_interface_from_tar_gz(tar_path: &str) -> Result { + use flate2::read::GzDecoder; + use tar::Archive; + + let file = std::fs::File::open(tar_path) + .map_err(|e| format!("无法打开 tar.gz 文件 [{}]: {}", tar_path, e))?; + let gz = GzDecoder::new(file); + let mut archive = Archive::new(gz); + let entries = archive + .entries() + .map_err(|e| format!("无法读取 tar.gz 条目: {}", e))?; + + for entry in entries { + let mut entry = entry.map_err(|e| format!("无法读取 tar.gz 条目: {}", e))?; + let path = entry + .path() + .map_err(|e| format!("无法读取 tar.gz 条目路径: {}", e))?; + if !is_interface_json_path(&path.to_string_lossy()) { + continue; + } + + let mut content = String::new(); + entry + .read_to_string(&mut content) + .map_err(|e| format!("无法读取 interface.json: {}", e))?; + return Ok(content); + } + + Err("更新包内未找到 interface.json".to_string()) +} + /// 解压 ZIP 文件 fn extract_zip_file(zip_path: &str, dest_dir: &str) -> Result<(), String> { let file = std::fs::File::open(zip_path) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4a76d36e..cf157927 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -256,6 +256,7 @@ pub fn run() { commands::state::clear_instance_logs, // 更新安装命令 commands::update::extract_zip, + commands::update::read_update_package_interface, commands::update::check_changes_json, commands::update::apply_incremental_update, commands::update::apply_full_update, diff --git a/src/App.tsx b/src/App.tsx index c6c6db0e..cea49cb9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -82,7 +82,6 @@ import { import { LoadingScreen } from './components/app'; import { ConnectionLostOverlay } from './components/app/ConnectionLostOverlay'; import { WebUIBetaBanner } from './components/app/WebUIBetaBanner'; -import { GlobalUpdateDropOverlay } from './components/GlobalUpdateDropOverlay'; import { startGlobalCallbackListener } from './components/connection/callbackCache'; import { useIsMobile } from '@/hooks/useIsMobile'; import { ScrollText } from 'lucide-react'; @@ -1678,7 +1677,6 @@ function App() { className={`h-full flex flex-col bg-bg-primary relative ${backgroundImageDataUrl ? 'has-background-image' : ''}`} > -

@@ -1762,7 +1760,6 @@ function App() { className={`h-full flex flex-col bg-bg-primary relative ${backgroundImageDataUrl ? 'has-background-image' : ''}`} > -
{/* WebUI 模式下的连接断开覆盖层 */} diff --git a/src/components/GlobalUpdateDropOverlay.tsx b/src/components/GlobalUpdateDropOverlay.tsx deleted file mode 100644 index d78602ab..00000000 --- a/src/components/GlobalUpdateDropOverlay.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Loader2, UploadCloud } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { useLocalUpdatePackageImport } from '@/hooks/useLocalUpdatePackageImport'; -import { isTauri } from '@/utils/windowUtils'; - -export function GlobalUpdateDropOverlay() { - const { t } = useTranslation(); - const [draggingFile, setDraggingFile] = useState(false); - const { importSinglePackage, disabled } = useLocalUpdatePackageImport(); - - useEffect(() => { - if (!isTauri()) return; - - let unlisten: (() => void) | undefined; - let mounted = true; - - const setup = async () => { - try { - const { getCurrentWebview } = await import('@tauri-apps/api/webview'); - const webview = getCurrentWebview(); - unlisten = await webview.onDragDropEvent((event) => { - const payload = event.payload; - - if (payload.type === 'over') { - setDraggingFile(true); - return; - } - - if (payload.type === 'drop') { - setDraggingFile(false); - if (!disabled) { - void importSinglePackage(payload.paths); - } - return; - } - - setDraggingFile(false); - }); - } catch { - if (mounted) setDraggingFile(false); - } - }; - - void setup(); - - return () => { - mounted = false; - unlisten?.(); - }; - }, [disabled, importSinglePackage]); - - if (!draggingFile) return null; - - return ( -
-
- {disabled ? ( - - ) : ( - - )} -
-

- {disabled ? t('mirrorChyan.localPackageBusy') : t('mirrorChyan.dropLocalPackage')} -

-

{t('mirrorChyan.localPackageHint')}

-
-
-
- ); -} diff --git a/src/components/settings/UpdateSection.tsx b/src/components/settings/UpdateSection.tsx index 353e33da..225e77d2 100644 --- a/src/components/settings/UpdateSection.tsx +++ b/src/components/settings/UpdateSection.tsx @@ -70,7 +70,7 @@ export function UpdateSection() { const [checkFailed, setCheckFailed] = useState(false); const [, setDebugLog] = useState([]); const { - importSinglePackage, + importPackage, supportedExtensions, disabled: localPackageImportDisabled, } = useLocalUpdatePackageImport(); @@ -389,8 +389,8 @@ export function UpdateSection() { }); if (!selected || Array.isArray(selected)) return; - await importSinglePackage([selected]); - }, [importSinglePackage, supportedExtensions, t]); + await importPackage(selected); + }, [importPackage, supportedExtensions, t]); if (!projectInterface?.mirrorchyan_rid) { return null; @@ -560,7 +560,6 @@ export function UpdateSection() { )} - {/* 更新状态显示 */} {isTauri() && downloadStatus !== 'downloading' && installStatus !== 'installing' && (