add update button

This commit is contained in:
musistudio
2025-08-13 21:32:57 +08:00
parent bdf608fffc
commit bc08c4ab48
10 changed files with 353 additions and 59 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { SettingsDialog } from "@/components/SettingsDialog";
@@ -9,13 +9,21 @@ import { JsonEditor } from "@/components/JsonEditor";
import { Button } from "@/components/ui/button";
import { useConfig } from "@/components/ConfigProvider";
import { api } from "@/lib/api";
import { Settings, Languages, Save, RefreshCw, FileJson } from "lucide-react";
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Toast } from "@/components/ui/toast";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from "@/components/ui/dialog";
import "@/styles/animations.css";
function App() {
@@ -26,53 +34,13 @@ function App() {
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
useEffect(() => {
const checkAuth = async () => {
// If we already have a config, we're authenticated
if (config) {
setIsCheckingAuth(false);
return;
}
// For empty API key, allow access without checking config
const apiKey = localStorage.getItem('apiKey');
if (!apiKey) {
setIsCheckingAuth(false);
return;
}
// If we don't have a config, try to fetch it
try {
await api.getConfig();
// If successful, we don't need to do anything special
// The ConfigProvider will handle setting the config
} catch (err) {
// If it's a 401, the API client will redirect to login
// For other errors, we still show the app to display the error
console.error('Error checking auth:', err);
// Redirect to login on authentication error
if ((err as Error).message === 'Unauthorized') {
navigate('/login');
}
} finally {
setIsCheckingAuth(false);
}
};
checkAuth();
// Listen for unauthorized events
const handleUnauthorized = () => {
navigate('/login');
};
window.addEventListener('unauthorized', handleUnauthorized);
return () => {
window.removeEventListener('unauthorized', handleUnauthorized);
};
}, [config, navigate]);
// 版本检查状态
const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false);
const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false);
const [newVersionInfo, setNewVersionInfo] = useState<{ version: string; changelog: string } | null>(null);
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false);
const hasAutoCheckedUpdate = useRef(false);
const saveConfig = async () => {
// Handle case where config might be null or undefined
@@ -152,6 +120,124 @@ function App() {
setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' });
}
};
// 检查更新函数
const checkForUpdates = useCallback(async (showDialog: boolean = true) => {
// 如果已经检查过且有新版本,根据参数决定是否显示对话框
if (hasCheckedUpdate && isNewVersionAvailable) {
if (showDialog) {
setIsUpdateDialogOpen(true);
}
return;
}
setIsCheckingUpdate(true);
try {
const updateInfo = await api.checkForUpdates();
if (updateInfo.hasUpdate && updateInfo.latestVersion && updateInfo.changelog) {
setIsNewVersionAvailable(true);
setNewVersionInfo({
version: updateInfo.latestVersion,
changelog: updateInfo.changelog
});
// 只有在showDialog为true时才显示对话框
if (showDialog) {
setIsUpdateDialogOpen(true);
}
} else if (showDialog) {
// 只有在showDialog为true时才显示没有更新的提示
setToast({ message: t('app.no_updates_available'), type: 'success' });
}
setHasCheckedUpdate(true);
} catch (error) {
console.error('Failed to check for updates:', error);
if (showDialog) {
setToast({ message: t('app.update_check_failed') + ': ' + (error as Error).message, type: 'error' });
}
} finally {
setIsCheckingUpdate(false);
}
}, [hasCheckedUpdate, isNewVersionAvailable, t]);
useEffect(() => {
const checkAuth = async () => {
// If we already have a config, we're authenticated
if (config) {
setIsCheckingAuth(false);
// 自动检查更新,但不显示对话框
if (!hasCheckedUpdate && !hasAutoCheckedUpdate.current) {
hasAutoCheckedUpdate.current = true;
checkForUpdates(false);
}
return;
}
// For empty API key, allow access without checking config
const apiKey = localStorage.getItem('apiKey');
if (!apiKey) {
setIsCheckingAuth(false);
return;
}
// If we don't have a config, try to fetch it
try {
await api.getConfig();
// If successful, we don't need to do anything special
// The ConfigProvider will handle setting the config
} catch (err) {
// If it's a 401, the API client will redirect to login
// For other errors, we still show the app to display the error
console.error('Error checking auth:', err);
// Redirect to login on authentication error
if ((err as Error).message === 'Unauthorized') {
navigate('/login');
}
} finally {
setIsCheckingAuth(false);
// 在获取配置完成后检查更新,但不显示对话框
if (!hasCheckedUpdate && !hasAutoCheckedUpdate.current) {
hasAutoCheckedUpdate.current = true;
checkForUpdates(false);
}
}
};
checkAuth();
// Listen for unauthorized events
const handleUnauthorized = () => {
navigate('/login');
};
window.addEventListener('unauthorized', handleUnauthorized);
return () => {
window.removeEventListener('unauthorized', handleUnauthorized);
};
}, [config, navigate, hasCheckedUpdate, checkForUpdates]);
// 执行更新函数
const performUpdate = async () => {
if (!newVersionInfo) return;
try {
const result = await api.performUpdate();
if (result.success) {
setToast({ message: t('app.update_successful'), type: 'success' });
setIsNewVersionAvailable(false);
setIsUpdateDialogOpen(false);
setHasCheckedUpdate(false); // 重置检查状态,以便下次重新检查
} else {
setToast({ message: t('app.update_failed') + ': ' + result.message, type: 'error' });
}
} catch (error) {
console.error('Failed to perform update:', error);
setToast({ message: t('app.update_failed') + ': ' + (error as Error).message, type: 'error' });
}
};
if (isCheckingAuth) {
@@ -215,6 +301,26 @@ function App() {
</div>
</PopoverContent>
</Popover>
{/* 更新版本按钮 */}
<Button
variant="ghost"
size="icon"
onClick={() => checkForUpdates(true)}
disabled={isCheckingUpdate}
className="transition-all-ease hover:scale-110 relative"
>
<div className="relative">
<CircleArrowUp className="h-5 w-5" />
{isNewVersionAvailable && !isCheckingUpdate && (
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white"></div>
)}
</div>
{isCheckingUpdate && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
</div>
)}
</Button>
<Button onClick={saveConfig} variant="outline" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
<Save className="mr-2 h-4 w-4" />
{t('app.save')}
@@ -244,6 +350,46 @@ function App() {
onOpenChange={setIsJsonEditorOpen}
showToast={(message, type) => setToast({ message, type })}
/>
{/* 版本更新对话框 */}
<Dialog open={isUpdateDialogOpen} onOpenChange={setIsUpdateDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
{t('app.new_version_available')}
{newVersionInfo && (
<span className="ml-2 text-sm font-normal text-muted-foreground">
v{newVersionInfo.version}
</span>
)}
</DialogTitle>
<DialogDescription>
{t('app.update_description')}
</DialogDescription>
</DialogHeader>
<div className="max-h-96 overflow-y-auto py-4">
{newVersionInfo?.changelog ? (
<div className="whitespace-pre-wrap text-sm">
{newVersionInfo.changelog}
</div>
) : (
<div className="text-muted-foreground">
{t('app.no_changelog_available')}
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsUpdateDialogOpen(false)}
>
{t('app.later')}
</Button>
<Button onClick={performUpdate}>
{t('app.update_now')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{toast && (
<Toast
message={toast.message}

View File

@@ -194,6 +194,16 @@ class ApiClient {
async restartService(): Promise<unknown> {
return this.post<void>('/restart', {});
}
// Check for updates
async checkForUpdates(): Promise<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }> {
return this.get<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }>('/update/check');
}
// Perform update
async performUpdate(): Promise<{ success: boolean; message: string }> {
return this.post<{ success: boolean; message: string }>('/api/update/perform', {});
}
}
// Create a default instance of the API client

View File

@@ -12,7 +12,16 @@
"config_saved_success": "Config saved successfully",
"config_saved_failed": "Failed to save config",
"config_saved_restart_success": "Config saved and service restarted successfully",
"config_saved_restart_failed": "Failed to save config and restart service"
"config_saved_restart_failed": "Failed to save config and restart service",
"new_version_available": "New Version Available",
"update_description": "A new version is available. Please review the changelog and update to get the latest features and improvements.",
"no_changelog_available": "No changelog available",
"later": "Later",
"update_now": "Update Now",
"no_updates_available": "No updates available",
"update_check_failed": "Failed to check for updates",
"update_successful": "Update successful",
"update_failed": "Update failed"
},
"login": {
"title": "Sign in to your account",

View File

@@ -12,7 +12,16 @@
"config_saved_success": "配置保存成功",
"config_saved_failed": "配置保存失败",
"config_saved_restart_success": "配置保存并服务重启成功",
"config_saved_restart_failed": "配置保存并服务重启失败"
"config_saved_restart_failed": "配置保存并服务重启失败",
"new_version_available": "有新版本可用",
"update_description": "发现新版本。请查看更新日志并更新以获取最新功能和改进。",
"no_changelog_available": "暂无更新日志",
"later": "稍后再说",
"update_now": "立即更新",
"no_updates_available": "当前已是最新版本",
"update_check_failed": "检查更新失败",
"update_successful": "更新成功",
"update_failed": "更新失败"
},
"login": {
"title": "登录到您的账户",