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/notice-service"; 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 STATUS_LABEL_KEYS: Record = { Pending: "tests.statuses.test.pending", Yes: "tests.statuses.test.yes", No: "tests.statuses.test.no", Failed: "tests.statuses.test.failed", Completed: "tests.statuses.test.completed", "Disallowed ISP": "tests.statuses.test.disallowedIsp", "Originals Only": "tests.statuses.test.originalsOnly", "No (IP Banned By Disney+)": "tests.statuses.test.noDisney", "Unsupported Country/Region": "tests.statuses.test.unsupportedRegion", "Failed (Network Connection)": "tests.statuses.test.failedNetwork", }; const normalizeUnlockName = (name: string) => name.trim().toLowerCase(); const getStatusPriority = (status: string) => (status === "Pending" ? 0 : 1); const mergeOptionalFields = (preferred: UnlockItem, fallback: UnlockItem) => ({ ...preferred, region: preferred.region ?? fallback.region, check_time: preferred.check_time ?? fallback.check_time, }); const dedupeUnlockItems = (items: UnlockItem[]) => { const map = new Map(); items.forEach((item) => { const key = normalizeUnlockName(item.name); const existing = map.get(key); if (!existing) { map.set(key, item); return; } const existingPriority = getStatusPriority(existing.status); const itemPriority = getStatusPriority(item.status); if (itemPriority > existingPriority) { map.set(key, mergeOptionalFields(item, existing)); return; } if (itemPriority < existingPriority) { map.set(key, mergeOptionalFields(existing, item)); return; } map.set(key, mergeOptionalFields(item, existing)); }); return Array.from(map.values()); }; 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 normalizedExisting = dedupeUnlockItems(existing); const existingMap = new Map( normalizedExisting.map((item) => [ normalizeUnlockName(item.name), item, ]), ); const merged = defaults.map((item) => { const normalizedName = normalizeUnlockName(item.name); const matchedItem = existingMap.get(normalizedName); if (matchedItem) { return { ...matchedItem, name: item.name }; } return item; }); const mergedNameSet = new Set( merged.map((item) => normalizeUnlockName(item.name)), ); normalizedExisting.forEach((item) => { const normalizedName = normalizeUnlockName(item.name); if (!mergedNameSet.has(normalizedName)) { merged.push(item); mergedNameSet.add(normalizedName); } }); 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) { const parsedItems = JSON.parse(itemsJson) as UnlockItem[]; return { items: dedupeUnlockItems(parsedItems), 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("tests.unlock.page.messages.detectionTimeout"))), timeout, ), ), ]); }; // 执行全部项目检测 const checkAllMedia = useLockFn(async () => { try { setIsCheckingAll(true); const result = await invokeWithTimeout("check_media_unlock"); const sortedItems = sortItemsByName(dedupeUnlockItems(result)); setUnlockItems(sortedItems); const currentTime = new Date().toLocaleString(); saveResultsToStorage(sortedItems, currentTime); setIsCheckingAll(false); } catch (err: any) { setIsCheckingAll(false); showNotice.error("tests.unlock.page.messages.detectionTimeout", err); 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 dedupedResult = dedupeUnlockItems(result); const normalizedTargetName = normalizeUnlockName(name); const targetItem = dedupedResult.find( (item: UnlockItem) => normalizeUnlockName(item.name) === normalizedTargetName, ); if (targetItem) { const updatedItems = sortItemsByName( dedupeUnlockItems( unlockItems.map((item: UnlockItem) => normalizeUnlockName(item.name) === normalizedTargetName ? 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( "tests.unlock.page.messages.detectionFailedWithName", { name }, err, ); 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;