diff --git a/src/components/profile/enhanced.tsx b/src/components/profile/enhanced.tsx index 324d179..ea3dfd2 100644 --- a/src/components/profile/enhanced.tsx +++ b/src/components/profile/enhanced.tsx @@ -21,12 +21,16 @@ const EnhancedMode = (props: Props) => { const { items, chain } = props; const { mutate: mutateProfiles } = useSWR("getProfiles", getProfiles); - const { data: chainLogs = {} } = useSWR("getRuntimeLogs", getRuntimeLogs); + const { data: chainLogs = {}, mutate: mutateLogs } = useSWR( + "getRuntimeLogs", + getRuntimeLogs + ); // handler const onEnhance = useLockFn(async () => { try { await enhanceProfiles(); + mutateLogs(); Notice.success("Refresh clash config", 1000); } catch (err: any) { Notice.error(err.message || err.toString()); @@ -39,6 +43,7 @@ const EnhancedMode = (props: Props) => { const newChain = [...chain, uid]; await changeProfileChain(newChain); mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); + mutateLogs(); }); const onEnhanceDisable = useLockFn(async (uid: string) => { @@ -47,6 +52,7 @@ const EnhancedMode = (props: Props) => { const newChain = chain.filter((i) => i !== uid); await changeProfileChain(newChain); mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); + mutateLogs(); }); const onEnhanceDelete = useLockFn(async (uid: string) => { @@ -54,6 +60,7 @@ const EnhancedMode = (props: Props) => { await onEnhanceDisable(uid); await deleteProfile(uid); mutateProfiles(); + mutateLogs(); } catch (err: any) { Notice.error(err?.message || err.toString()); } @@ -65,6 +72,7 @@ const EnhancedMode = (props: Props) => { const newChain = [uid].concat(chain.filter((i) => i !== uid)); await changeProfileChain(newChain); mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); + mutateLogs(); }); const onMoveEnd = useLockFn(async (uid: string) => { @@ -73,10 +81,11 @@ const EnhancedMode = (props: Props) => { const newChain = chain.filter((i) => i !== uid).concat([uid]); await changeProfileChain(newChain); mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); + mutateLogs(); }); return ( - + { - + diff --git a/src/components/profile/file-input.tsx b/src/components/profile/file-input.tsx index 476fa2c..d860709 100644 --- a/src/components/profile/file-input.tsx +++ b/src/components/profile/file-input.tsx @@ -1,5 +1,6 @@ import { useRef, useState } from "react"; import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; import { Box, Button, Typography } from "@mui/material"; interface Props { @@ -9,6 +10,7 @@ interface Props { const FileInput = (props: Props) => { const { onChange } = props; + const { t } = useTranslation(); // file input const inputRef = useRef(); const [loading, setLoading] = useState(false); @@ -40,7 +42,7 @@ const FileInput = (props: Props) => { sx={{ flex: "none" }} onClick={() => inputRef.current?.click()} > - Choose File + {t("Choose File")} { +const InfoEditor = (props: Props) => { const { open, itemData, onClose } = props; const { t } = useTranslation(); @@ -56,7 +56,6 @@ const ProfileEdit = (props: Props) => { } await patchProfile(uid, { uid, name, desc, url, option: option_ }); - setShowOpt(false); mutate("getProfiles"); onClose(); } catch (err: any) { @@ -133,7 +132,7 @@ const ProfileEdit = (props: Props) => { value={option.update_interval} onChange={(e) => { const str = e.target.value?.replace(/\D/, ""); - setOption({ update_interval: str != null ? +str : str }); + setOption({ update_interval: !!str ? +str : undefined }); }} onKeyDown={(e) => e.key === "Enter" && onUpdate()} /> @@ -144,6 +143,7 @@ const ProfileEdit = (props: Props) => { {form.type === "remote" && ( setShowOpt((o) => !o)} > @@ -151,13 +151,15 @@ const ProfileEdit = (props: Props) => { )} - + ); }; -export default ProfileEdit; +export default InfoEditor; diff --git a/src/components/profile/log-viewer.tsx b/src/components/profile/log-viewer.tsx new file mode 100644 index 0000000..949c57a --- /dev/null +++ b/src/components/profile/log-viewer.tsx @@ -0,0 +1,71 @@ +import { useTranslation } from "react-i18next"; +import { + Button, + Chip, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Typography, +} from "@mui/material"; +import BaseEmpty from "../base/base-empty"; +import { Fragment } from "react"; + +interface Props { + open: boolean; + logInfo: [string, string][]; + onClose: () => void; +} + +const LogViewer = (props: Props) => { + const { open, logInfo, onClose } = props; + + const { t } = useTranslation(); + + return ( + + {t("Script Console")} + + + {logInfo.map(([level, log], index) => ( + + + + {log} + + + + ))} + + {logInfo.length === 0 && } + + + + + + + ); +}; + +export default LogViewer; diff --git a/src/components/profile/profile-box.tsx b/src/components/profile/profile-box.tsx new file mode 100644 index 0000000..7b8d5e8 --- /dev/null +++ b/src/components/profile/profile-box.tsx @@ -0,0 +1,43 @@ +import { alpha, Box, styled } from "@mui/material"; + +const ProfileBox = styled(Box)(({ theme, "aria-selected": selected }) => { + const { mode, primary, text, grey, background } = theme.palette; + const key = `${mode}-${!!selected}`; + + const backgroundColor = { + "light-true": alpha(primary.main, 0.2), + "light-false": alpha(background.paper, 0.75), + "dark-true": alpha(primary.main, 0.45), + "dark-false": alpha(grey[700], 0.45), + }[key]!; + + const color = { + "light-true": text.secondary, + "light-false": text.secondary, + "dark-true": alpha(text.secondary, 0.85), + "dark-false": alpha(text.secondary, 0.65), + }[key]!; + + const h2color = { + "light-true": primary.main, + "light-false": text.primary, + "dark-true": primary.light, + "dark-false": text.primary, + }[key]!; + + return { + width: "100%", + display: "block", + cursor: "pointer", + textAlign: "left", + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shadows[2], + padding: "8px 16px", + boxSizing: "border-box", + backgroundColor, + color, + "& h2": { color: h2color }, + }; +}); + +export default ProfileBox; diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 94ded72..2de20cd 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -1,13 +1,11 @@ import dayjs from "dayjs"; +import { mutate } from "swr"; import { useEffect, useState } from "react"; import { useLockFn } from "ahooks"; -import { useSWRConfig } from "swr"; import { useRecoilState } from "recoil"; import { useTranslation } from "react-i18next"; import { - alpha, Box, - styled, Typography, LinearProgress, IconButton, @@ -19,21 +17,11 @@ import { RefreshRounded } from "@mui/icons-material"; import { atomLoadingCache } from "@/services/states"; import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds"; import parseTraffic from "@/utils/parse-traffic"; -import ProfileEdit from "./profile-edit"; +import ProfileBox from "./profile-box"; +import InfoEditor from "./info-editor"; import FileEditor from "./file-editor"; import Notice from "../base/base-notice"; -const Wrapper = styled(Box)(({ theme }) => ({ - width: "100%", - display: "block", - cursor: "pointer", - textAlign: "left", - borderRadius: theme.shape.borderRadius, - boxShadow: theme.shadows[2], - padding: "8px 16px", - boxSizing: "border-box", -})); - const round = keyframes` from { transform: rotate(0deg); } to { transform: rotate(360deg); } @@ -49,7 +37,6 @@ const ProfileItem = (props: Props) => { const { selected, itemData, onSelect } = props; const { t } = useTranslation(); - const { mutate } = useSWRConfig(); const [anchorEl, setAnchorEl] = useState(null); const [position, setPosition] = useState({ left: 0, top: 0 }); const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache); @@ -58,7 +45,6 @@ const ProfileItem = (props: Props) => { // local file mode // remote file mode - // subscription url mode const hasUrl = !!itemData.url; const hasExtra = !!extra; // only subscription url has extra info @@ -79,7 +65,6 @@ const ProfileItem = (props: Props) => { const handler = () => { const now = Date.now(); const lastUpdate = updated * 1000; - // 大于一天的不管 if (now - lastUpdate >= 24 * 36e5) return; @@ -152,13 +137,6 @@ const ProfileItem = (props: Props) => { } }); - const boxStyle = { - height: 26, - display: "flex", - alignItems: "center", - justifyContent: "space-between", - }; - const urlModeMenu = [ { label: "Select", handler: onForceSelect }, { label: "Edit Info", handler: onEditInfo }, @@ -176,36 +154,17 @@ const ProfileItem = (props: Props) => { { label: "Delete", handler: onDelete }, ]; + const boxStyle = { + height: 26, + display: "flex", + alignItems: "center", + justifyContent: "space-between", + }; + return ( <> - { - const { mode, primary, text, grey } = palette; - const key = `${mode}-${selected}`; - - const bgcolor = { - "light-true": alpha(primary.main, 0.15), - "light-false": palette.background.paper, - "dark-true": alpha(primary.main, 0.35), - "dark-false": alpha(grey[700], 0.35), - }[key]!; - - const color = { - "light-true": text.secondary, - "light-false": text.secondary, - "dark-true": alpha(text.secondary, 0.75), - "dark-false": alpha(text.secondary, 0.75), - }[key]!; - - const h2color = { - "light-true": primary.main, - "light-false": text.primary, - "dark-true": primary.light, - "dark-false": text.primary, - }[key]!; - - return { bgcolor, color, "& h2": { color: h2color } }; - }} + onSelect(false)} onContextMenu={(event) => { const { clientX, clientY } = event; @@ -214,9 +173,9 @@ const ProfileItem = (props: Props) => { event.preventDefault(); }} > - + { {hasUrl && ( { @@ -240,47 +202,47 @@ const ProfileItem = (props: Props) => { onUpdate(false); }} > - + )} {/* the second line show url's info or description */} - {hasUrl ? ( - - - {from} - + + {hasUrl ? ( + <> + + {from} + - - {updated > 0 ? dayjs(updated * 1000).fromNow() : ""} - - - ) : ( - + + {updated > 0 ? dayjs(updated * 1000).fromNow() : ""} + + + ) : ( {itemData.desc} - - )} + )} + {/* the third line show extra info or last updated time */} {hasExtra ? ( - + {parseTraffic(upload + download)} / {parseTraffic(total)} - {expire} + {expire} ) : ( - {parseExpire(updated)} + {parseExpire(updated)} )} @@ -289,7 +251,7 @@ const ProfileItem = (props: Props) => { value={progress} color="inherit" /> - + { ))} - {editOpen && ( - setEditOpen(false)} - /> - )} + setEditOpen(false)} + /> - {fileOpen && ( - setFileOpen(false)} - /> - )} + setFileOpen(false)} + /> ); }; diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx index 0258db6..027c692 100644 --- a/src/components/profile/profile-more.tsx +++ b/src/components/profile/profile-more.tsx @@ -1,32 +1,24 @@ import dayjs from "dayjs"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; import { - alpha, Box, + Badge, Chip, - styled, Typography, MenuItem, Menu, + IconButton, } from "@mui/material"; +import { FeaturedPlayListRounded } from "@mui/icons-material"; import { viewProfile } from "@/services/cmds"; -import ProfileEdit from "./profile-edit"; +import InfoEditor from "./info-editor"; import FileEditor from "./file-editor"; +import ProfileBox from "./profile-box"; +import LogViewer from "./log-viewer"; import Notice from "../base/base-notice"; -const Wrapper = styled(Box)(({ theme }) => ({ - width: "100%", - display: "block", - cursor: "pointer", - textAlign: "left", - borderRadius: theme.shape.borderRadius, - boxShadow: theme.shadows[2], - padding: "8px 16px", - boxSizing: "border-box", -})); - interface Props { selected: boolean; itemData: CmdType.ProfileItem; @@ -55,18 +47,11 @@ const ProfileMore = (props: Props) => { const { uid, type } = itemData; const { t } = useTranslation(); - const [anchorEl, setAnchorEl] = useState(null); const [position, setPosition] = useState({ left: 0, top: 0 }); const [editOpen, setEditOpen] = useState(false); const [fileOpen, setFileOpen] = useState(false); - // const [status, setStatus] = useState(enhance.status(uid)); - - // unlisten when unmount - // useEffect(() => enhance.listen(uid, setStatus), [uid]); - - // error during enhanced mode - const hasError = !!logInfo.find((e) => e[0] === "exception"); // selected && status?.status === "error"; + const [logOpen, setLogOpen] = useState(false); const onEditInfo = () => { setAnchorEl(null); @@ -92,6 +77,7 @@ const ProfileMore = (props: Props) => { return fn(); }; + const hasError = !!logInfo.find((e) => e[0] === "exception"); const showMove = enableNum > 1 && !hasError; const enableMenu = [ @@ -122,39 +108,8 @@ const ProfileMore = (props: Props) => { return ( <> - { - // todo - // 区分 selected 和 error 和 mode 下各种颜色的排列组合 - const { mode, primary, text, grey, error } = palette; - const key = `${mode}-${selected}`; - const bgkey = hasError ? `${mode}-err` : key; - - const bgcolor = { - "light-true": alpha(primary.main, 0.15), - "light-false": palette.background.paper, - "dark-true": alpha(primary.main, 0.35), - "dark-false": alpha(grey[700], 0.35), - "light-err": alpha(error.main, 0.12), - "dark-err": alpha(error.main, 0.3), - }[bgkey]!; - - const color = { - "light-true": text.secondary, - "light-false": text.secondary, - "dark-true": alpha(text.secondary, 0.6), - "dark-false": alpha(text.secondary, 0.6), - }[key]!; - - const h2color = { - "light-true": primary.main, - "light-false": text.primary, - "dark-true": primary.light, - "dark-false": text.primary, - }[key]!; - - return { bgcolor, color, "& h2": { color: h2color } }; - }} + onSelect(false)} onContextMenu={(event) => { const { clientX, clientY } = event; @@ -163,7 +118,12 @@ const ProfileMore = (props: Props) => { event.preventDefault(); }} > - + { color="primary" size="small" variant="outlined" - sx={{ textTransform: "capitalize" }} + sx={{ height: 20, textTransform: "capitalize" }} /> - {hasError ? ( - - {/* {status.message} */} - error - + {selected ? ( + hasError ? ( + + setLogOpen(true)} + > + + + + ) : ( + setLogOpen(true)} + > + + + ) ) : ( { - {parseExpire(itemData.updated)} + {!!itemData.updated + ? dayjs(itemData.updated! * 1000).fromNow() + : ""} - + { ))} - {editOpen && ( - setEditOpen(false)} - /> - )} + setEditOpen(false)} + /> - {fileOpen && ( - setFileOpen(false)} + setFileOpen(false)} + /> + + {selected && ( + setLogOpen(false)} /> )} diff --git a/src/components/profile/profile-new.tsx b/src/components/profile/profile-new.tsx index 48b3319..fdd9f10 100644 --- a/src/components/profile/profile-new.tsx +++ b/src/components/profile/profile-new.tsx @@ -1,5 +1,6 @@ import { useRef, useState } from "react"; -import { useSWRConfig } from "swr"; +import { mutate } from "swr"; +import { useTranslation } from "react-i18next"; import { useLockFn, useSetState } from "ahooks"; import { Button, @@ -29,7 +30,7 @@ interface Props { const ProfileNew = (props: Props) => { const { open, onClose } = props; - const { mutate } = useSWRConfig(); + const { t } = useTranslation(); const [form, setForm] = useSetState({ type: "remote", name: "", @@ -83,7 +84,7 @@ const ProfileNew = (props: Props) => { return ( - Create Profile + {t("Create Profile")} @@ -120,7 +121,7 @@ const ProfileNew = (props: Props) => { {form.type === "remote" && ( setForm({ url: e.target.value })} @@ -146,6 +147,7 @@ const ProfileNew = (props: Props) => { {form.type === "remote" && ( setShowOpt((o) => !o)} > @@ -153,9 +155,11 @@ const ProfileNew = (props: Props) => { )} - + diff --git a/src/locales/en.json b/src/locales/en.json index d852586..72d39bf 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -18,6 +18,8 @@ "Profile URL": "Profile URL", "Import": "Import", "New": "New", + "Create Profile": "Create Profile", + "Choose File": "Choose File", "Close All": "Close All", "Select": "Select", "Edit Info": "Edit Info", diff --git a/src/locales/zh.json b/src/locales/zh.json index b8f2c08..d9750a6 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -18,6 +18,8 @@ "Profile URL": "配置文件链接", "Import": "导入", "New": "新建", + "Create Profile": "新建配置", + "Choose File": "选择文件", "Close All": "关闭全部", "Select": "使用", "Edit Info": "编辑信息", diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 8beec8d..f04a210 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -1,8 +1,8 @@ -import useSWR, { useSWRConfig } from "swr"; +import useSWR, { mutate } from "swr"; import { useLockFn } from "ahooks"; import { useEffect, useMemo, useState } from "react"; import { useSetRecoilState } from "recoil"; -import { Box, Button, Grid, TextField } from "@mui/material"; +import { Button, Grid, Stack, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; import { getProfiles, @@ -20,7 +20,6 @@ import EnhancedMode from "@/components/profile/enhanced"; const ProfilePage = () => { const { t } = useTranslation(); - const { mutate } = useSWRConfig(); const [url, setUrl] = useState(""); const [disabled, setDisabled] = useState(false); @@ -110,10 +109,13 @@ const ProfilePage = () => { getProfiles().then((newProfiles) => { mutate("getProfiles", newProfiles); - if (!newProfiles.current && newProfiles.items?.length) { - const current = newProfiles.items[0].uid; + const remoteItem = newProfiles.items?.find((e) => e.type === "remote"); + + if (!newProfiles.current && remoteItem) { + const current = remoteItem.uid; selectProfile(current); mutate("getProfiles", { ...newProfiles, current }, true); + mutate("getRuntimeLogs"); } }); } catch { @@ -130,6 +132,7 @@ const ProfilePage = () => { await selectProfile(uid); setCurrentProfile(uid); mutate("getProfiles", { ...profiles, current: uid }, true); + mutate("getRuntimeLogs"); if (force) Notice.success("Refresh clash config", 1000); } catch (err: any) { Notice.error(err?.message || err.toString()); @@ -138,29 +141,34 @@ const ProfilePage = () => { return ( - + setUrl(e.target.value)} - sx={{ mr: 1 }} + sx={{ input: { py: 0.65, px: 1.25 } }} + placeholder={t("Profile URL")} /> - - + {regularItems.map((item) => (