diff --git a/package.json b/package.json index 91eaa00e..9cf87bcb 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "prepare": "husky install" }, "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@juggle/resize-observer": "^3.4.0", @@ -42,8 +45,8 @@ "react-virtuoso": "^3.1.3", "recoil": "^0.7.6", "snarkdown": "^2.0.0", - "tar": "^6.2.0", - "swr": "^1.3.0" + "swr": "^1.3.0", + "tar": "^6.2.0" }, "devDependencies": { "@actions/github": "^5.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f55f381f..59c19753 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: + "@dnd-kit/core": + specifier: ^6.1.0 + version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + "@dnd-kit/sortable": + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) + "@dnd-kit/utilities": + specifier: ^3.2.2 + version: 3.2.2(react@18.2.0) "@emotion/react": specifier: ^11.11.1 version: 11.11.1(@types/react@18.2.37)(react@18.2.0) @@ -482,6 +491,61 @@ packages: "@babel/helper-validator-identifier": 7.22.20 to-fast-properties: 2.0.0 + /@dnd-kit/accessibility@3.1.0(react@18.2.0): + resolution: + { + integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==, + } + peerDependencies: + react: ">=16.8.0" + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: + { + integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==, + } + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + dependencies: + "@dnd-kit/accessibility": 3.1.0(react@18.2.0) + "@dnd-kit/utilities": 3.2.2(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + dev: false + + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: + { + integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==, + } + peerDependencies: + "@dnd-kit/core": ^6.1.0 + react: ">=16.8.0" + dependencies: + "@dnd-kit/core": 6.1.0(react-dom@18.2.0)(react@18.2.0) + "@dnd-kit/utilities": 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/utilities@3.2.2(react@18.2.0): + resolution: + { + integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==, + } + peerDependencies: + react: ">=16.8.0" + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + /@emotion/babel-plugin@11.11.0: resolution: { @@ -1656,6 +1720,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -1668,6 +1733,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -1680,6 +1746,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -1692,6 +1759,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -3797,7 +3865,6 @@ packages: { integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==, } - dev: true /tunnel@0.0.6: resolution: diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 2df5f995..67e22841 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -30,6 +30,11 @@ pub async fn import_profile(url: String, option: Option) -> CmdResult wrap_err!(Config::profiles().data().append_item(item)) } +#[tauri::command] +pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { + wrap_err!(Config::profiles().data().reorder(active_id, over_id)) +} + #[tauri::command] pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResult { let item = wrap_err!(PrfItem::from(item, file_data).await)?; @@ -229,7 +234,6 @@ pub fn open_web_url(url: String) -> CmdResult<()> { wrap_err!(open::that(url)) } - #[cfg(windows)] pub mod uwp { use super::*; @@ -299,4 +303,4 @@ pub mod uwp { pub async fn invoke_uwp_tool() -> CmdResult { Ok(()) } -} \ No newline at end of file +} diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 19f159de..c28858dd 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -55,7 +55,12 @@ impl IProfiles { pub fn template() -> Self { Self { - valid: Some(vec!["dns".into(), "sub-rules".into(), "unified-delay".into(), "tcp-concurrent".into()]), + valid: Some(vec![ + "dns".into(), + "sub-rules".into(), + "unified-delay".into(), + "tcp-concurrent".into(), + ]), items: Some(vec![]), ..Self::default() } @@ -151,6 +156,30 @@ impl IProfiles { self.save_file() } + /// reorder items + pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> { + let mut items = self.items.take().unwrap_or(vec![]); + let mut old_index = None; + let mut new_index = None; + + for i in 0..items.len() { + if items[i].uid == Some(active_id.clone()) { + old_index = Some(i); + } + if items[i].uid == Some(over_id.clone()) { + new_index = Some(i); + } + } + + if old_index.is_none() || new_index.is_none() { + return Ok(()); + } + let item = items.remove(old_index.unwrap()); + items.insert(new_index.unwrap(), item); + self.items = Some(items); + self.save_file() + } + /// update the item value pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { let mut items = self.items.take().unwrap_or(vec![]); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index aa954844..f47910ad 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -59,6 +59,7 @@ fn main() -> std::io::Result<()> { cmds::patch_profile, cmds::create_profile, cmds::import_profile, + cmds::reorder_profile, cmds::update_profile, cmds::delete_profile, cmds::read_profile_file, diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 6e8ef096..7f899560 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -4,6 +4,8 @@ import { useEffect, useState } from "react"; import { useLockFn } from "ahooks"; import { useRecoilState } from "recoil"; import { useTranslation } from "react-i18next"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { Box, Typography, @@ -14,7 +16,7 @@ import { Menu, CircularProgress, } from "@mui/material"; -import { RefreshRounded } from "@mui/icons-material"; +import { RefreshRounded, DragIndicator } from "@mui/icons-material"; import { atomLoadingCache } from "@/services/states"; import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds"; import { Notice } from "@/components/base"; @@ -28,6 +30,7 @@ const round = keyframes` `; interface Props { + id: string; selected: boolean; activating: boolean; itemData: IProfileItem; @@ -37,6 +40,8 @@ interface Props { export const ProfileItem = (props: Props) => { const { selected, activating, itemData, onSelect, onEdit } = props; + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: props.id }); const { t } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); @@ -183,7 +188,12 @@ export const ProfileItem = (props: Props) => { }; return ( - <> + onSelect(false)} @@ -212,17 +222,27 @@ export const ProfileItem = (props: Props) => { )} - - - {name} - + + + + + + + {name} + + {/* only if has url can it be updated */} {hasUrl && ( @@ -246,7 +266,6 @@ export const ProfileItem = (props: Props) => { )} - {/* the second line show url's info or description */} {hasUrl ? ( @@ -271,7 +290,6 @@ export const ProfileItem = (props: Props) => { )} - {/* the third line show extra info or last updated time */} {hasExtra ? ( @@ -285,7 +303,6 @@ export const ProfileItem = (props: Props) => { {parseExpire(updated)} )} - { mode="yaml" onClose={() => setFileOpen(false)} /> - + ); }; diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index be04f381..20650590 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -3,6 +3,19 @@ import { useMemo, useRef, useState } from "react"; import { useLockFn } from "ahooks"; import { useSetRecoilState } from "recoil"; import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + SortableContext, + sortableKeyboardCoordinates, +} from "@dnd-kit/sortable"; import { LoadingButton } from "@mui/lab"; import { ClearRounded, @@ -19,6 +32,7 @@ import { getRuntimeLogs, deleteProfile, updateProfile, + reorderProfile, } from "@/services/cmds"; import { atomLoadingCache } from "@/services/states"; import { closeAllConnections } from "@/services/api"; @@ -40,7 +54,12 @@ const ProfilePage = () => { const [disabled, setDisabled] = useState(false); const [activating, setActivating] = useState(""); const [loading, setLoading] = useState(false); - + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); const { profiles = {}, activateSelected, @@ -106,6 +125,16 @@ const ProfilePage = () => { } }; + const onDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + if (over) { + if (active.id !== over.id) { + await reorderProfile(active.id.toString(), over.id.toString()); + mutateProfiles(); + } + } + }; + const onSelect = useLockFn(async (current: string, force: boolean) => { if (!force && current === profiles.current) return; // 避免大多数情况下loading态闪烁 @@ -293,22 +322,34 @@ const ProfilePage = () => { {t("New")} - - - - {regularItems.map((item) => ( - - onSelect(item.uid, f)} - onEdit={() => viewerRef.current?.edit(item)} - /> - - ))} - - + + + + { + return x.uid; + })} + > + {regularItems.map((item) => ( + + onSelect(item.uid, f)} + onEdit={() => viewerRef.current?.edit(item)} + /> + + ))} + + + + {enhanceItems.length > 0 && ( @@ -330,7 +371,6 @@ const ProfilePage = () => { ))} )} - mutateProfiles()} /> diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 5637db7a..dc558bfe 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -64,6 +64,13 @@ export async function importProfile(url: string) { }); } +export async function reorderProfile(activeId: string, overId: string) { + return invoke("reorder_profile", { + activeId, + overId, + }); +} + export async function updateProfile(index: string, option?: IProfileOption) { return invoke("update_profile", { index, option }); }