Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7b3c7294 | ||
|
|
4678fc7dde | ||
|
|
9f492fad49 | ||
|
|
bd0a959e18 | ||
|
|
dd605e2610 | ||
|
|
9910f6b817 | ||
|
|
991897aff4 | ||
|
|
3cde019208 | ||
|
|
2f6efbed63 |
@@ -17,7 +17,7 @@ A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="h
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64 and macOS 11+
|
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
||||||
|
|
||||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
||||||
|
|
||||||
|
|||||||
17
UPDATELOG.md
17
UPDATELOG.md
@@ -1,3 +1,20 @@
|
|||||||
|
## v0.0.25
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash core version
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- app updater error
|
||||||
|
- display window controllers on Linux
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
If you can't update the app properly, please consider downloading the latest version from github release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.0.24
|
## v0.0.24
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "0.0.24",
|
"version": "0.0.25",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function resolveClash() {
|
|||||||
|
|
||||||
const CLASH_URL_PREFIX =
|
const CLASH_URL_PREFIX =
|
||||||
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
||||||
const CLASH_LATEST_DATE = "2022.03.19";
|
const CLASH_LATEST_DATE = "2022.03.21";
|
||||||
|
|
||||||
// todo
|
// todo
|
||||||
const map = {
|
const map = {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ chrono = "0.4.19"
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.0.0-rc.4", features = ["shell-all", "system-tray", "updater", "window-all"] }
|
tauri = { version = "1.0.0-rc.4", features = ["process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||||
window-shadows = { git = "https://github.com/tauri-apps/window-shadows" }
|
window-shadows = { git = "https://github.com/tauri-apps/window-shadows" }
|
||||||
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy" }
|
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy" }
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Clash Verge",
|
"productName": "Clash Verge",
|
||||||
"version": "0.0.24"
|
"version": "0.0.25"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
@@ -64,6 +64,9 @@
|
|||||||
},
|
},
|
||||||
"window": {
|
"window": {
|
||||||
"all": true
|
"all": true
|
||||||
|
},
|
||||||
|
"process": {
|
||||||
|
"all": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"windows": [
|
"windows": [
|
||||||
|
|||||||
@@ -80,8 +80,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.windows.layout {
|
.linux,
|
||||||
.layout__right .the-content {
|
.windows,
|
||||||
top: 30px;
|
.unknown {
|
||||||
|
&.layout {
|
||||||
|
.layout__right .the-content {
|
||||||
|
top: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import snarkdown from "snarkdown";
|
import snarkdown from "snarkdown";
|
||||||
import { useState, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useRecoilState } from "recoil";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
import { relaunch } from "@tauri-apps/api/process";
|
import { relaunch } from "@tauri-apps/api/process";
|
||||||
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
|
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
|
||||||
import { killSidecars, restartSidecar } from "../../services/cmds";
|
import { killSidecars, restartSidecar } from "../../services/cmds";
|
||||||
|
import { atomUpdateState } from "../../services/states";
|
||||||
import Notice from "../base/base-notice";
|
import Notice from "../base/base-notice";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,8 +26,6 @@ const UpdateLog = styled(Box)(() => ({
|
|||||||
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
|
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let uploadingState = false;
|
|
||||||
|
|
||||||
const UpdateDialog = (props: Props) => {
|
const UpdateDialog = (props: Props) => {
|
||||||
const { open, onClose } = props;
|
const { open, onClose } = props;
|
||||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||||
@@ -33,22 +33,22 @@ const UpdateDialog = (props: Props) => {
|
|||||||
revalidateIfStale: false,
|
revalidateIfStale: false,
|
||||||
focusThrottleInterval: 36e5, // 1 hour
|
focusThrottleInterval: 36e5, // 1 hour
|
||||||
});
|
});
|
||||||
const [uploading, setUploading] = useState(uploadingState);
|
|
||||||
|
const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
|
||||||
|
|
||||||
const onUpdate = async () => {
|
const onUpdate = async () => {
|
||||||
setUploading(true);
|
if (updateState) return;
|
||||||
uploadingState = true;
|
setUpdateState(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await installUpdate();
|
|
||||||
await killSidecars();
|
await killSidecars();
|
||||||
|
await installUpdate();
|
||||||
await relaunch();
|
await relaunch();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await restartSidecar();
|
await restartSidecar();
|
||||||
Notice.error(err?.message || err.toString());
|
Notice.error(err?.message || err.toString());
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUpdateState(false);
|
||||||
uploadingState = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ const UpdateDialog = (props: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
autoFocus
|
autoFocus
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={uploading}
|
disabled={updateState}
|
||||||
onClick={onUpdate}
|
onClick={onUpdate}
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { useState } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import { useEffect, useState } from "react";
|
import { useRecoilState } from "recoil";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { RefreshRounded } from "@mui/icons-material";
|
import { RefreshRounded } from "@mui/icons-material";
|
||||||
import { CmdType } from "../../services/types";
|
import { CmdType } from "../../services/types";
|
||||||
|
import { atomLoadingCache } from "../../services/states";
|
||||||
import { updateProfile, deleteProfile, viewProfile } from "../../services/cmds";
|
import { updateProfile, deleteProfile, viewProfile } from "../../services/cmds";
|
||||||
import parseTraffic from "../../utils/parse-traffic";
|
import parseTraffic from "../../utils/parse-traffic";
|
||||||
import ProfileEdit from "./profile-edit";
|
import ProfileEdit from "./profile-edit";
|
||||||
@@ -37,9 +39,6 @@ const round = keyframes`
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// save the state of each item loading
|
|
||||||
const loadingCache: Record<string, boolean> = {};
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
itemData: CmdType.ProfileItem;
|
itemData: CmdType.ProfileItem;
|
||||||
@@ -51,9 +50,9 @@ const ProfileItem = (props: Props) => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
const [loading, setLoading] = useState(loadingCache[itemData.uid] ?? false);
|
|
||||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||||
|
const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache);
|
||||||
|
|
||||||
const { name = "Profile", extra, updated = 0 } = itemData;
|
const { name = "Profile", extra, updated = 0 } = itemData;
|
||||||
const { upload = 0, download = 0, total = 0 } = extra ?? {};
|
const { upload = 0, download = 0, total = 0 } = extra ?? {};
|
||||||
@@ -68,9 +67,7 @@ const ProfileItem = (props: Props) => {
|
|||||||
const hasUrl = !!itemData.url;
|
const hasUrl = !!itemData.url;
|
||||||
const hasExtra = !!extra; // only subscription url has extra info
|
const hasExtra = !!extra; // only subscription url has extra info
|
||||||
|
|
||||||
useEffect(() => {
|
const loading = loadingCache[itemData.uid] ?? false;
|
||||||
loadingCache[itemData.uid] = loading;
|
|
||||||
}, [itemData, loading]);
|
|
||||||
|
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const onEdit = () => {
|
const onEdit = () => {
|
||||||
@@ -92,19 +89,19 @@ const ProfileItem = (props: Props) => {
|
|||||||
onSelect(true);
|
onSelect(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUpdateWrapper = (withProxy: boolean) => async () => {
|
const onUpdate = useLockFn(async (withProxy: boolean) => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
if (loading) return;
|
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
await updateProfile(itemData.uid, { with_proxy: withProxy });
|
await updateProfile(itemData.uid, { with_proxy: withProxy });
|
||||||
setLoading(false);
|
|
||||||
mutate("getProfiles");
|
mutate("getProfiles");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setLoading(false);
|
|
||||||
Notice.error(err?.message || err.toString());
|
Notice.error(err?.message || err.toString());
|
||||||
|
} finally {
|
||||||
|
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const onDelete = useLockFn(async () => {
|
const onDelete = useLockFn(async () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
@@ -127,8 +124,8 @@ const ProfileItem = (props: Props) => {
|
|||||||
{ label: "Select", handler: onForceSelect },
|
{ label: "Select", handler: onForceSelect },
|
||||||
{ label: "Edit", handler: onEdit },
|
{ label: "Edit", handler: onEdit },
|
||||||
{ label: "File", handler: onView },
|
{ label: "File", handler: onView },
|
||||||
{ label: "Update", handler: onUpdateWrapper(false) },
|
{ label: "Update", handler: () => onUpdate(false) },
|
||||||
{ label: "Update(Proxy)", handler: onUpdateWrapper(true) },
|
{ label: "Update(Proxy)", handler: () => onUpdate(true) },
|
||||||
{ label: "Delete", handler: onDelete },
|
{ label: "Delete", handler: onDelete },
|
||||||
];
|
];
|
||||||
const fileModeMenu = [
|
const fileModeMenu = [
|
||||||
@@ -199,7 +196,7 @@ const ProfileItem = (props: Props) => {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onUpdateWrapper(false)();
|
onUpdate(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RefreshRounded />
|
<RefreshRounded />
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ import LayoutItem from "../components/layout/layout-item";
|
|||||||
import LayoutControl from "../components/layout/layout-control";
|
import LayoutControl from "../components/layout/layout-control";
|
||||||
import LayoutTraffic from "../components/layout/layout-traffic";
|
import LayoutTraffic from "../components/layout/layout-traffic";
|
||||||
import UpdateButton from "../components/layout/update-button";
|
import UpdateButton from "../components/layout/update-button";
|
||||||
|
import getSystem from "../utils/get-system";
|
||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const isWinOs = /win64|win32/i.test(navigator.userAgent);
|
const OS = getSystem();
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -85,11 +86,11 @@ const Layout = () => {
|
|||||||
<Paper
|
<Paper
|
||||||
square
|
square
|
||||||
elevation={0}
|
elevation={0}
|
||||||
className={`${isWinOs ? "windows " : ""}layout`}
|
className={`${OS} layout`}
|
||||||
onPointerDown={onDragging}
|
onPointerDown={onDragging}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
// only prevent it on Windows
|
// only prevent it on Windows
|
||||||
if (isWinOs) e.preventDefault();
|
if (OS === "windows") e.preventDefault();
|
||||||
}}
|
}}
|
||||||
sx={[
|
sx={[
|
||||||
(theme) => ({
|
(theme) => ({
|
||||||
@@ -118,7 +119,7 @@ const Layout = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="layout__right" data-windrag>
|
<div className="layout__right" data-windrag>
|
||||||
{isWinOs && (
|
{OS === "windows" && (
|
||||||
<div className="the-bar">
|
<div className="the-bar">
|
||||||
<LayoutControl />
|
<LayoutControl />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,3 +10,15 @@ export const atomLogData = atom<ApiType.LogItem[]>({
|
|||||||
key: "atomLogData",
|
key: "atomLogData",
|
||||||
default: [],
|
default: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// save the state of each profile item loading
|
||||||
|
export const atomLoadingCache = atom<Record<string, boolean>>({
|
||||||
|
key: "atomLoadingCache",
|
||||||
|
default: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// save update state
|
||||||
|
export const atomUpdateState = atom<boolean>({
|
||||||
|
key: "atomUpdateState",
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|||||||
13
src/utils/get-system.ts
Normal file
13
src/utils/get-system.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// get the system os
|
||||||
|
// according to UA
|
||||||
|
export default function getSystem() {
|
||||||
|
const ua = navigator.userAgent;
|
||||||
|
|
||||||
|
if (ua.includes("Mac OS X")) return "macos";
|
||||||
|
|
||||||
|
if (/win64|win32/i.test(ua)) return "windows";
|
||||||
|
|
||||||
|
if (/linux/i.test(ua)) return "linux";
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user