9 Commits

Author SHA1 Message Date
GyDi
1a7b3c7294 v0.0.25 2022-03-22 01:51:06 +08:00
GyDi
4678fc7dde chore: update log 2022-03-22 01:50:24 +08:00
GyDi
9f492fad49 chore: update clash core 2022-03-22 01:50:15 +08:00
GyDi
bd0a959e18 fix: windows style 2022-03-22 01:36:06 +08:00
GyDi
dd605e2610 fix: update state 2022-03-22 01:27:22 +08:00
inRm3D
9910f6b817 chore: readme (#43) 2022-03-21 21:55:45 +08:00
GyDi
991897aff4 chore: tauri feature 2022-03-21 11:40:52 +08:00
GyDi
3cde019208 fix: profile item loading state 2022-03-21 11:40:27 +08:00
GyDi
2f6efbed63 chore: tauri allowList 2022-03-20 13:16:50 +08:00
12 changed files with 86 additions and 39 deletions

View File

@@ -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+

View File

@@ -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

View File

@@ -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",

View File

@@ -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 = {

View File

@@ -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" }

View File

@@ -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": [

View File

@@ -80,8 +80,12 @@
} }
} }
.windows.layout { .linux,
.layout__right .the-content { .windows,
top: 30px; .unknown {
&.layout {
.layout__right .the-content {
top: 30px;
}
} }
} }

View File

@@ -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

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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
View 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";
}