import { AccessTimeOutlined, CancelOutlined, CheckCircleOutlined, HelpOutline, PendingOutlined, RefreshRounded, } from "@mui/icons-material"; import { Box, Button, Card, Chip, CircularProgress, Divider, Grid, Tooltip, Typography, alpha, useTheme, } from "@mui/material"; import { invoke } from "@tauri-apps/api/core"; import { useLockFn } from "ahooks"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { BaseEmpty, BasePage } from "@/components/base"; import { showNotice } from "@/services/noticeService"; interface UnlockItem { name: string; status: string; region?: string | null; check_time?: string | null; } const UNLOCK_RESULTS_STORAGE_KEY = "clash_verge_unlock_results"; const UNLOCK_RESULTS_TIME_KEY = "clash_verge_unlock_time"; const UnlockPage = () => { const { t } = useTranslation(); const theme = useTheme(); const [unlockItems, setUnlockItems] = useState([]); const [isCheckingAll, setIsCheckingAll] = useState(false); const [loadingItems, setLoadingItems] = useState([]); const sortItemsByName = useCallback((items: UnlockItem[]) => { return [...items].sort((a, b) => a.name.localeCompare(b.name)); }, []); const mergeUnlockItems = useCallback( (defaults: UnlockItem[], existing?: UnlockItem[] | null) => { if (!existing || existing.length === 0) { return defaults; } const existingMap = new Map(existing.map((item) => [item.name, item])); const merged = defaults.map((item) => existingMap.get(item.name) ?? item); const mergedNameSet = new Set(merged.map((item) => item.name)); existing.forEach((item) => { if (!mergedNameSet.has(item.name)) { merged.push(item); } }); return merged; }, [], ); // 保存测试结果到本地存储 const saveResultsToStorage = useCallback( (items: UnlockItem[], time: string | null) => { try { localStorage.setItem(UNLOCK_RESULTS_STORAGE_KEY, JSON.stringify(items)); if (time) { localStorage.setItem(UNLOCK_RESULTS_TIME_KEY, time); } } catch (err) { console.error("Failed to save results to storage:", err); } }, [], ); const loadResultsFromStorage = useCallback((): { items: UnlockItem[] | null; time: string | null; } => { try { const itemsJson = localStorage.getItem(UNLOCK_RESULTS_STORAGE_KEY); const time = localStorage.getItem(UNLOCK_RESULTS_TIME_KEY); if (itemsJson) { return { items: JSON.parse(itemsJson) as UnlockItem[], time, }; } } catch (err) { console.error("Failed to load results from storage:", err); } return { items: null, time: null }; }, []); const getUnlockItems = useCallback( async ( existingItems: UnlockItem[] | null = null, existingTime: string | null = null, ) => { try { const defaultItems = await invoke("get_unlock_items"); const mergedItems = mergeUnlockItems(defaultItems, existingItems); const sortedItems = sortItemsByName(mergedItems); setUnlockItems(sortedItems); saveResultsToStorage( sortedItems, existingItems && existingItems.length > 0 ? existingTime : null, ); } catch (err: any) { console.error("Failed to get unlock items:", err); } }, [mergeUnlockItems, saveResultsToStorage, sortItemsByName], ); useEffect(() => { void (async () => { const { items: storedItems, time: storedTime } = loadResultsFromStorage(); if (storedItems && storedItems.length > 0) { setUnlockItems(sortItemsByName(storedItems)); await getUnlockItems(storedItems, storedTime); } else { await getUnlockItems(); } })(); }, [getUnlockItems, loadResultsFromStorage, sortItemsByName]); const invokeWithTimeout = async ( cmd: string, args?: any, timeout = 15000, ): Promise => { return Promise.race([ invoke(cmd, args), new Promise((_, reject) => setTimeout( () => reject(new Error(t("Detection timeout or failed"))), timeout, ), ), ]); }; // 执行全部项目检测 const checkAllMedia = useLockFn(async () => { try { setIsCheckingAll(true); const result = await invokeWithTimeout("check_media_unlock"); const sortedItems = sortItemsByName(result); setUnlockItems(sortedItems); const currentTime = new Date().toLocaleString(); saveResultsToStorage(sortedItems, currentTime); setIsCheckingAll(false); } catch (err: any) { setIsCheckingAll(false); showNotice( "error", err?.message || err?.toString() || t("Detection timeout or failed"), ); console.error("Failed to check media unlock:", err); } }); // 检测单个流媒体服务 const checkSingleMedia = useLockFn(async (name: string) => { try { setLoadingItems((prev) => [...prev, name]); const result = await invokeWithTimeout("check_media_unlock"); const targetItem = result.find((item: UnlockItem) => item.name === name); if (targetItem) { const updatedItems = sortItemsByName( unlockItems.map((item: UnlockItem) => item.name === name ? targetItem : item, ), ); setUnlockItems(updatedItems); const currentTime = new Date().toLocaleString(); saveResultsToStorage(updatedItems, currentTime); } setLoadingItems((prev) => prev.filter((item) => item !== name)); } catch (err: any) { setLoadingItems((prev) => prev.filter((item) => item !== name)); showNotice( "error", err?.message || err?.toString() || t("Detection failed for {name}").replace("{name}", name), ); console.error(`Failed to check ${name}:`, err); } }); // 状态颜色 const getStatusColor = (status: string) => { if (status === "Pending") return "default"; if (status === "Yes") return "success"; if (status === "No") return "error"; if (status === "Soon") return "warning"; if (status.includes("Failed")) return "error"; if (status === "Completed") return "info"; if ( status === "Disallowed ISP" || status === "Blocked" || status === "Unsupported Country/Region" ) { return "error"; } return "default"; }; // 状态图标 const getStatusIcon = (status: string) => { if (status === "Pending") return ; if (status === "Yes") return ; if (status === "No") return ; if (status === "Soon") return ; if (status.includes("Failed")) return ; return ; }; // 边框色 const getStatusBorderColor = (status: string) => { if (status === "Yes") return theme.palette.success.main; if (status === "No") return theme.palette.error.main; if (status === "Soon") return theme.palette.warning.main; if (status.includes("Failed")) return theme.palette.error.main; if (status === "Completed") return theme.palette.info.main; return theme.palette.divider; }; const isDark = theme.palette.mode === "dark"; return ( } > {unlockItems.length === 0 ? ( ) : ( {unlockItems.map((item) => ( {item.name} {item.region && ( )} {item.check_time || "-- --"} ))} )} ); }; export default UnlockPage;