8 Commits

11 changed files with 88 additions and 81 deletions

View File

@@ -424,7 +424,7 @@ jobs:
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_x64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Intel"></a><br> <a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_x64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Intel"></a><br>
> :warning: **Warning** > :warning: **Warning**
If you get a notification that the application is corrupted when you run it on macOS, run this command:<br> If you get a notification that the application is corrupted when you run it on macOS, run this command:<br>
`sudo xattr -r -c /Applications/Clash\ Verge\ Rev\ Lite.app` <code>sudo xattr -r -c /Applications/Clash\ Verge\ Rev\ Lite.app</code>
### Linux ### Linux
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_amd64.deb"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=debian&label=DEB"> </a><br> <a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Clash.Verge.Rev.Lite_${{ env.VERSION }}_amd64.deb"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=debian&label=DEB"> </a><br>

View File

@@ -1,3 +1,12 @@
## v0.2.3
- fixed problem with profile inactivation after adding via deeplink on windows
- corrected layout on the proxy page, now all cards are the same size
- corrected announe transposition by \n
- corrected side menu in compressed window
- added check at the main toggle switch, now it cannot be enabled if there are no profiles.
## v0.2.1 ## v0.2.1
- added headers "announce-url", "update-always" - added headers "announce-url", "update-always"

View File

@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "0.2.2", "version": "0.2.3",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",

2
src-tauri/Cargo.lock generated
View File

@@ -1061,7 +1061,7 @@ dependencies = [
[[package]] [[package]]
name = "clash-verge" name = "clash-verge"
version = "0.2.2" version = "0.2.3"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"aes-gcm", "aes-gcm",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "0.2.2" version = "0.2.3"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"] authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
license = "GPL-3.0-only" license = "GPL-3.0-only"

View File

@@ -565,7 +565,7 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
Some(url) => { Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}"); log::info!(target:"app", "decoded subscription url: {url}");
create_window(false); create_window(true);
match PrfItem::from_url(url.as_ref(), name, None, None).await { match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => { Ok(item) => {
let uid = item.uid.clone().unwrap(); let uid = item.uid.clone().unwrap();

View File

@@ -1,5 +1,5 @@
{ {
"version": "0.2.2", "version": "0.2.3",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": { "bundle": {
"active": true, "active": true,

View File

@@ -7,7 +7,7 @@ import {
SidebarHeader, SidebarHeader,
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem, useSidebar,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import { t } from 'i18next'; import { t } from 'i18next';
import { cn } from '@root/lib/utils'; import { cn } from '@root/lib/utils';
@@ -23,6 +23,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { UpdateButton } from "@/components/layout/update-button"; import { UpdateButton } from "@/components/layout/update-button";
import React from "react"; import React from "react";
import { SheetClose } from '@/components/ui/sheet';
const menuItems = [ const menuItems = [
{ title: 'Home', url: '/home', icon: Home }, { title: 'Home', url: '/home', icon: Home },
@@ -35,6 +36,7 @@ const menuItems = [
]; ];
export function AppSidebar() { export function AppSidebar() {
const { isMobile } = useSidebar();
return ( return (
<Sidebar variant="floating" collapsible="icon"> <Sidebar variant="floating" collapsible="icon">
<SidebarHeader> <SidebarHeader>
@@ -51,13 +53,8 @@ export function AppSidebar() {
<SidebarMenu className="gap-3"> <SidebarMenu className="gap-3">
{menuItems.map((item) => { {menuItems.map((item) => {
const isActive = location.pathname === item.url; const isActive = location.pathname === item.url;
return ( const linkElement = (
<SidebarMenuItem key={item.title} className="my-1"> <Link
<SidebarMenuButton
asChild
isActive={isActive}
tooltip={t(item.title)}>
<Link
key={item.title} key={item.title}
to={item.url} to={item.url}
className={cn( className={cn(
@@ -68,6 +65,20 @@ export function AppSidebar() {
<item.icon className="h-4 w-4 drop-shadow-md" /> <item.icon className="h-4 w-4 drop-shadow-md" />
{t(item.title)} {t(item.title)}
</Link> </Link>
)
return (
<SidebarMenuItem key={item.title} className="my-1">
<SidebarMenuButton
asChild
isActive={isActive}
tooltip={t(item.title)}>
{isMobile ? (
<SheetClose asChild>
{linkElement}
</SheetClose>
) : (
linkElement
)}
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
) )

View File

@@ -66,7 +66,7 @@ export const ProxyItemMini = (props: Props) => {
title={`${proxy.name}\n${proxy.now ?? ""}`} title={`${proxy.name}\n${proxy.now ?? ""}`}
className="group relative flex h-16 cursor-pointer items-center justify-between rounded-lg border bg-card p-3 shadow-sm transition-colors duration-200 hover:bg-accent data-[selected=true]:ring-2 data-[selected=true]:ring-primary" className="group relative flex h-16 cursor-pointer items-center justify-between rounded-lg border bg-card p-3 shadow-sm transition-colors duration-200 hover:bg-accent data-[selected=true]:ring-2 data-[selected=true]:ring-primary"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0 w-0">
<p className="truncate text-sm font-medium">{proxy.name}</p> <p className="truncate text-sm font-medium">{proxy.name}</p>
{showType && ( {showType && (

View File

@@ -32,21 +32,21 @@ dayjs.extend(relativeTime);
const OS = getSystem(); const OS = getSystem();
// 通知处理函数 // Notification Handler
const handleNoticeMessage = ( const handleNoticeMessage = (
status: string, status: string,
msg: string, msg: string,
t: (key: string) => string, t: (key: string) => string,
navigate: (path: string, options?: any) => void, navigate: (path: string, options?: any) => void,
) => { ) => {
console.log("[通知监听 V2] 收到消息:", status, msg); console.log("[Notification Listener V2] Receiving a message:", status, msg);
switch (status) { switch (status) {
case "import_sub_url::ok": case "import_sub_url::ok":
mutate("getProfiles"); mutate("getProfiles");
navigate("/", { state: { activateProfile: msg } }); navigate("/");
showNotice("success", t("Import Subscription Successful")); showNotice("success", t("Import Subscription Successful"));
window.dispatchEvent(new CustomEvent('activate-profile', { detail: msg })); sessionStorage.setItem('activateProfile', msg);
break; break;
case "import_sub_url::error": case "import_sub_url::error":
showNotice("error", msg); showNotice("error", msg);
@@ -136,7 +136,7 @@ const handleNoticeMessage = (
showNotice("error", `${t("Failed to Change Core")}: ${msg}`); showNotice("error", `${t("Failed to Change Core")}: ${msg}`);
break; break;
default: // Optional: Log unhandled statuses default: // Optional: Log unhandled statuses
console.warn(`[通知监听 V2] 未处理的状态: ${status}`); console.warn(`[Notification Listener V2] Unprocessed state: ${status}`);
break; break;
} }
}; };
@@ -163,14 +163,14 @@ const Layout = () => {
try { try {
handleNoticeMessage(status, msg, t, navigate); handleNoticeMessage(status, msg, t, navigate);
} catch (error) { } catch (error) {
console.error("[Layout] 处理通知消息失败:", error); console.error("[Layout] Failure to process a notification message:", error);
} }
}, 0); }, 0);
}, },
[t, navigate], [t, navigate],
); );
// 初始化全局日志服务 // Initialize the global logging service
useEffect(() => { useEffect(() => {
if (clashInfo) { if (clashInfo) {
const { server = "", secret = "" } = clashInfo; const { server = "", secret = "" } = clashInfo;
@@ -178,7 +178,7 @@ const Layout = () => {
} }
}, [clashInfo, enableLog]); }, [clashInfo, enableLog]);
// 设置监听器 // Setting up a listener
useEffect(() => { useEffect(() => {
const listeners = [ const listeners = [
addListener("verge://refresh-clash-config", async () => { addListener("verge://refresh-clash-config", async () => {
@@ -224,11 +224,11 @@ const Layout = () => {
try { try {
unlisten(); unlisten();
} catch (error) { } catch (error) {
console.error("[Layout] 清理事件监听器失败:", error); console.error("[Layout] Failed to clear event listener:", error);
} }
}) })
.catch((error) => { .catch((error) => {
console.error("[Layout] 获取unlisten函数失败:", error); console.error("[Layout] Failed to get unlisten function:", error);
}); });
} }
}); });
@@ -238,11 +238,11 @@ const Layout = () => {
try { try {
cleanup(); cleanup();
} catch (error) { } catch (error) {
console.error("[Layout] 清理窗口监听器失败:", error); console.error("[Layout] Failed to clear window listener:", error);
} }
}) })
.catch((error) => { .catch((error) => {
console.error("[Layout] 获取cleanup函数失败:", error); console.error("[Layout] Failed to get cleanup function:", error);
}); });
}, 0); }, 0);
}; };
@@ -250,10 +250,10 @@ const Layout = () => {
useEffect(() => { useEffect(() => {
if (initRef.current) { if (initRef.current) {
console.log("[Layout] 初始化代码已执行过,跳过"); console.log("[Layout] Initialization code has already been executed, skip");
return; return;
} }
console.log("[Layout] 开始执行初始化代码"); console.log("[Layout] Begin executing initialization code");
initRef.current = true; initRef.current = true;
let isInitialized = false; let isInitialized = false;
@@ -263,27 +263,27 @@ const Layout = () => {
const notifyBackend = async (action: string, stage?: string) => { const notifyBackend = async (action: string, stage?: string) => {
try { try {
if (stage) { if (stage) {
console.log(`[Layout] 通知后端 ${action}: ${stage}`); console.log(`[Layout] Notification Backend ${action}: ${stage}`);
await invoke("update_ui_stage", { stage }); await invoke("update_ui_stage", { stage });
} else { } else {
console.log(`[Layout] 通知后端 ${action}`); console.log(`[Layout] Notification Backend ${action}`);
await invoke("notify_ui_ready"); await invoke("notify_ui_ready");
} }
} catch (err) { } catch (err) {
console.error(`[Layout] 通知失败 ${action}:`, err); console.error(`[Layout] Notification failure ${action}:`, err);
} }
}; };
const removeLoadingOverlay = () => { const removeLoadingOverlay = () => {
const initialOverlay = document.getElementById("initial-loading-overlay"); const initialOverlay = document.getElementById("initial-loading-overlay");
if (initialOverlay) { if (initialOverlay) {
console.log("[Layout] 移除加载指示器"); console.log("[Layout] Remove loading indicator");
initialOverlay.style.opacity = "0"; initialOverlay.style.opacity = "0";
setTimeout(() => { setTimeout(() => {
try { try {
initialOverlay.remove(); initialOverlay.remove();
} catch (e) { } catch (e) {
console.log("[Layout] 加载指示器已被移除"); console.log("[Layout] Load indicator has been removed");
} }
}, 300); }, 300);
} }
@@ -291,23 +291,23 @@ const Layout = () => {
const performInitialization = async () => { const performInitialization = async () => {
if (isInitialized) { if (isInitialized) {
console.log("[Layout] 已经初始化过,跳过"); console.log("[Layout] Already initialized, skip");
return; return;
} }
initializationAttempts++; initializationAttempts++;
console.log(`[Layout] 开始第 ${initializationAttempts} 次初始化尝试`); console.log(`[Layout] Start ${initializationAttempts} for the first time`);
try { try {
removeLoadingOverlay(); removeLoadingOverlay();
await notifyBackend("加载阶段", "Loading"); await notifyBackend("Loading phase", "Loading");
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
const checkReactMount = () => { const checkReactMount = () => {
const rootElement = document.getElementById("root"); const rootElement = document.getElementById("root");
if (rootElement && rootElement.children.length > 0) { if (rootElement && rootElement.children.length > 0) {
console.log("[Layout] React组件已挂载"); console.log("[Layout] React components are mounted");
resolve(); resolve();
} else { } else {
setTimeout(checkReactMount, 50); setTimeout(checkReactMount, 50);
@@ -317,43 +317,43 @@ const Layout = () => {
checkReactMount(); checkReactMount();
setTimeout(() => { setTimeout(() => {
console.log("[Layout] React组件挂载检查超时,继续执行"); console.log("[Layout] React components mount check timeout, continue execution");
resolve(); resolve();
}, 2000); }, 2000);
}); });
await notifyBackend("DOM就绪", "DomReady"); await notifyBackend("DOM ready", "DomReady");
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve()); requestAnimationFrame(() => resolve());
}); });
await notifyBackend("资源加载完成", "ResourcesLoaded"); await notifyBackend("Resource loading completed", "ResourcesLoaded");
await notifyBackend("UI就绪"); await notifyBackend("UI ready");
isInitialized = true; isInitialized = true;
console.log(`[Layout] ${initializationAttempts} 次初始化完成`); console.log(`[Layout] The ${initializationAttempts} initialization is complete`);
} catch (error) { } catch (error) {
console.error( console.error(
`[Layout] ${initializationAttempts} 次初始化失败:`, `[Layout] Initialization failure at ${initializationAttempts}:`,
error, error,
); );
if (initializationAttempts < maxAttempts) { if (initializationAttempts < maxAttempts) {
console.log( console.log(
`[Layout] 将在500ms后进行第 ${initializationAttempts + 1} 次重试`, `[Layout] The first ${initializationAttempts + 1} retry will be made after 500ms`,
); );
setTimeout(performInitialization, 500); setTimeout(performInitialization, 500);
} else { } else {
console.error("[Layout] 所有初始化尝试都失败,执行紧急初始化"); console.error("[Layout] All initialization attempts fail, perform emergency initialization");
removeLoadingOverlay(); removeLoadingOverlay();
try { try {
await notifyBackend("UI就绪"); await notifyBackend("UI ready");
isInitialized = true; isInitialized = true;
} catch (e) { } catch (e) {
console.error("[Layout] 紧急初始化也失败:", e); console.error("[Layout] Emergency initialization also failed:", e);
} }
} }
} }
@@ -363,39 +363,39 @@ const Layout = () => {
const setupEventListener = async () => { const setupEventListener = async () => {
try { try {
console.log("[Layout] 开始监听启动完成事件"); console.log("[Layout] Start listening for startup completion events");
const unlisten = await listen("verge://startup-completed", () => { const unlisten = await listen("verge://startup-completed", () => {
if (!hasEventTriggered) { if (!hasEventTriggered) {
console.log("[Layout] 收到启动完成事件,开始初始化"); console.log("[Layout] Receive startup completion event, start initialization");
hasEventTriggered = true; hasEventTriggered = true;
performInitialization(); performInitialization();
} }
}); });
return unlisten; return unlisten;
} catch (err) { } catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err); console.error("[Layout] Failed to listen for startup completion event:", err);
return () => {}; return () => {};
} }
}; };
const checkImmediateInitialization = async () => { const checkImmediateInitialization = async () => {
try { try {
console.log("[Layout] 检查后端是否已就绪"); console.log("[Layout] Check if the backend is ready");
await invoke("update_ui_stage", { stage: "Loading" }); await invoke("update_ui_stage", { stage: "Loading" });
if (!hasEventTriggered && !isInitialized) { if (!hasEventTriggered && !isInitialized) {
console.log("[Layout] 后端已就绪,立即开始初始化"); console.log("[Layout] Backend is ready, start initialization immediately");
hasEventTriggered = true; hasEventTriggered = true;
performInitialization(); performInitialization();
} }
} catch (err) { } catch (err) {
console.log("[Layout] 后端尚未就绪,等待启动完成事件"); console.log("[Layout] Backend not yet ready, waiting for startup completion event");
} }
}; };
const backupInitialization = setTimeout(() => { const backupInitialization = setTimeout(() => {
if (!hasEventTriggered && !isInitialized) { if (!hasEventTriggered && !isInitialized) {
console.warn("[Layout] 备用初始化触发1.5秒内未开始初始化"); console.warn("[Layout] Standby initialization trigger: initialization not started within 1.5 seconds");
hasEventTriggered = true; hasEventTriggered = true;
performInitialization(); performInitialization();
} }
@@ -403,9 +403,9 @@ const Layout = () => {
const emergencyInitialization = setTimeout(() => { const emergencyInitialization = setTimeout(() => {
if (!isInitialized) { if (!isInitialized) {
console.error("[Layout] 紧急初始化触发5秒内未完成初始化"); console.error("[Layout] Emergency initialization trigger: initialization not completed within 5 seconds");
removeLoadingOverlay(); removeLoadingOverlay();
notifyBackend("UI就绪").catch(() => {}); notifyBackend("UI ready").catch(() => {});
isInitialized = true; isInitialized = true;
} }
}, 5000); }, 5000);
@@ -421,10 +421,10 @@ const Layout = () => {
}; };
}, []); }, []);
// 语言和起始页设置 // Language and start page settings
useEffect(() => { useEffect(() => {
if (language) { if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language); dayjs.locale(language === "ru" ? "ru-ru" : language);
i18next.changeLanguage(language); i18next.changeLanguage(language);
} }
}, [language]); }, [language]);

View File

@@ -78,26 +78,13 @@ const MinimalHomePage: React.FC = () => {
); );
useEffect(() => { useEffect(() => {
const handleActivationEvent = (event: Event) => { const uidToActivate = sessionStorage.getItem('activateProfile');
const customEvent = event as CustomEvent<string>;
const profileId = customEvent.detail;
if (profileId) {
setUidToActivate(profileId);
}
};
window.addEventListener('activate-profile', handleActivationEvent);
return () => {
window.removeEventListener('activate-profile', handleActivationEvent);
};
}, []);
useEffect(() => {
if (uidToActivate && profileItems.some(p => p.uid === uidToActivate)) { if (uidToActivate && profileItems.some(p => p.uid === uidToActivate)) {
activateProfile(uidToActivate, false); activateProfile(uidToActivate, false);
setUidToActivate(null); sessionStorage.removeItem('activateProfile');
} }
}, [uidToActivate, profileItems, activateProfile]); }, [profileItems, activateProfile]);
const handleProfileChange = useLockFn(async (uid: string) => { const handleProfileChange = useLockFn(async (uid: string) => {
if (profiles?.current === uid) return; if (profiles?.current === uid) return;
@@ -240,14 +227,14 @@ const MinimalHomePage: React.FC = () => {
href={currentProfile.announce_url} href={currentProfile.announce_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-base font-semibold text-foreground hover:underline hover:opacity-80 transition-all" className="inline-flex items-center gap-2 text-base font-semibold text-foreground hover:underline hover:opacity-80 transition-all whitespace-pre-wrap"
title={currentProfile.announce_url} title={currentProfile.announce_url.replace(/\\n/g, '\n')}
> >
<span>{currentProfile.announce}</span> <span>{currentProfile.announce.replace(/\\n/g, '\n')}</span>
<ExternalLink className="h-4 w-4 flex-shrink-0" /> <ExternalLink className="h-4 w-4 flex-shrink-0" />
</a> </a>
) : ( ) : (
<p className="text-base font-semibold text-foreground"> <p className="text-base font-semibold text-foreground whitespace-pre-wrap">
{currentProfile.announce} {currentProfile.announce}
</p> </p>
)} )}
@@ -268,7 +255,7 @@ const MinimalHomePage: React.FC = () => {
<div className="scale-[7] my-16"> <div className="scale-[7] my-16">
<Switch <Switch
disabled={showTunAlert || isToggling} disabled={showTunAlert || isToggling || profileItems.length === 0}
checked={!!isProxyEnabled} checked={!!isProxyEnabled}
onCheckedChange={handleToggleProxy} onCheckedChange={handleToggleProxy}
aria-label={t("Toggle Proxy")} aria-label={t("Toggle Proxy")}