Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
greenhat616 committed Dec 13, 2023
1 parent c7afb45 commit f047b7c
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 52 deletions.
67 changes: 40 additions & 27 deletions backend/tauri/src/core/hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ pub struct Hotkey {

app_handle: Arc<Mutex<Option<AppHandle>>>,
}
// (hotkey, func)
type HotKeyOp<'a> = (&'a str, HotKeyOpType<'a>);

enum HotKeyOpType<'a> {
Unbind(&'a str),
Change(&'a str, &'a str),
Bind(&'a str),
}

impl Hotkey {
pub fn global() -> &'static Hotkey {
Expand Down Expand Up @@ -97,7 +105,7 @@ impl Hotkey {
}

fn unregister(&self, hotkey: &str) -> Result<()> {
self.get_manager()?.unregister(&hotkey)?;
self.get_manager()?.unregister(hotkey)?;
log::info!(target: "app", "unregister hotkey {hotkey}");
Ok(())
}
Expand All @@ -107,26 +115,31 @@ impl Hotkey {
let old_map = Self::get_map_from_vec(&current);
let new_map = Self::get_map_from_vec(&new_hotkeys);

let (del, add) = Self::get_diff(old_map, new_map);
let ops = Self::get_ops(old_map, new_map);

// 先检查一遍所有新的热键是不是可以用的
for (hotkey, _) in add.iter() {
Self::check_key(hotkey)?;
for (hotkey, op) in ops.iter() {
if let HotKeyOpType::Bind(_) = op {
Self::check_key(hotkey)?
}
}

del.iter().for_each(|key| {
let _ = self.unregister(key);
});

add.iter().for_each(|(key, func)| {
log_err!(self.register(key, func));
});
for (hotkey, op) in ops.iter() {
match op {
HotKeyOpType::Unbind(_) => self.unregister(hotkey)?,
HotKeyOpType::Change(_, new_func) => {
self.unregister(hotkey)?;
self.register(hotkey, new_func)?;
}
HotKeyOpType::Bind(func) => self.register(hotkey, func)?,
}
}

*current = new_hotkeys;
Ok(())
}

fn get_map_from_vec<'a>(hotkeys: &'a Vec<String>) -> HashMap<&'a str, &'a str> {
fn get_map_from_vec(hotkeys: &[String]) -> HashMap<&str, &str> {
let mut map = HashMap::new();

hotkeys.iter().for_each(|hotkey| {
Expand All @@ -143,32 +156,32 @@ impl Hotkey {
map
}

fn get_diff<'a>(
fn get_ops<'a>(
old_map: HashMap<&'a str, &'a str>,
new_map: HashMap<&'a str, &'a str>,
) -> (Vec<&'a str>, Vec<(&'a str, &'a str)>) {
let mut del_list = vec![];
let mut add_list = vec![];

old_map.iter().for_each(|(&key, func)| {
) -> Vec<HotKeyOp<'a>> {
let mut list = Vec::<HotKeyOp<'a>>::new();
old_map.iter().for_each(|(key, func)| {
match new_map.get(key) {
Some(new_func) => {
if new_func != func {
del_list.push(key);
add_list.push((key, *new_func));
list.push((*key, HotKeyOpType::Change(func, new_func)))
}

// 无变化,无需操作
}
None => del_list.push(key),
};
None => {
list.push((*key, HotKeyOpType::Unbind(func)));
}
}
});

new_map.iter().for_each(|(&key, &func)| {
if old_map.get(key).is_none() {
add_list.push((key, func));
new_map.iter().for_each(|(key, func)| {
if !old_map.contains_key(key) {
list.push((*key, HotKeyOpType::Bind(func)));
}
});

(del_list, add_list)
list
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/components/base/base-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Props {
title: ReactNode;
open: boolean;
okBtn?: ReactNode;
okBtnDisabled?: boolean;
cancelBtn?: ReactNode;
disableOk?: boolean;
disableCancel?: boolean;
Expand All @@ -37,6 +38,7 @@ export function BaseDialog(props: Props) {
title,
children,
okBtn,
okBtnDisabled,
cancelBtn,
contentSx,
disableCancel,
Expand All @@ -60,6 +62,7 @@ export function BaseDialog(props: Props) {
)}
{!disableOk && (
<LoadingButton
disabled={loading || okBtnDisabled}
loading={loading}
variant="contained"
onClick={props.onOk}
Expand Down
21 changes: 21 additions & 0 deletions src/components/common/kbd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { styled } from "@mui/material/styles";
const Kbd = styled("kbd")(({ theme }) => ({
backgroundColor:
theme.palette.mode === "dark" ? "rgb(255 255 255 / 0.06)" : "#edf2f7;",
borderColor:
theme.palette.mode === "dark" ? "rgb(255 255 255 / 0.16)" : "#e2e8f0",
paddingRight: "0.4em",
paddingLeft: "0.4em",
fontFamily: "SFMono-Regular, Menlo, Monaco, Consolas, monospace",
// font-size: 1em,
fontSize: "0.8em",
fontWeight: "bold",
lineHeight: "normal",
whiteSpace: "nowrap",
borderWidth: "1px",
borderBottomWidth: "3px",
borderRadius: "0.375rem",
borderStyle: "solid",
}));

export default Kbd;
40 changes: 20 additions & 20 deletions src/components/setting/mods/hotkey-input.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useRef, useState } from "react";
import { alpha, Box, IconButton, styled } from "@mui/material";
import { DeleteRounded } from "@mui/icons-material";
import Kbd from "@/components/common/kbd";
import { parseHotkey } from "@/utils/parse-hotkey";
import { DeleteRounded } from "@mui/icons-material";
import { Box, IconButton, alpha, styled } from "@mui/material";
import { FocusEvent, useRef, useState } from "react";

const KeyWrapper = styled("div")(({ theme }) => ({
const KeyWrapper = styled("div")<{
isDuplicate?: boolean;
}>(({ theme, isDuplicate }) => ({
position: "relative",
width: 165,
minHeight: 36,
Expand All @@ -28,39 +31,36 @@ const KeyWrapper = styled("div")(({ theme }) => ({
height: "100%",
minHeight: 36,
boxSizing: "border-box",
padding: "3px 4px",
padding: "2px 5px",
border: "1px solid",
borderRadius: 4,
borderColor: alpha(theme.palette.text.secondary, 0.15),
gap: 4,
borderColor: isDuplicate
? theme.palette.error.main
: alpha(theme.palette.text.secondary, 0.15),
"&:last-child": {
marginRight: 0,
},
},
".item": {
color: theme.palette.text.primary,
border: "1px solid",
borderColor: alpha(theme.palette.text.secondary, 0.2),
borderRadius: "2px",
padding: "1px 1px",
margin: "2px 0",
marginRight: 8,
},
}));

interface Props {
func: string;
isDuplicate: boolean;
value: string[];
onChange: (value: string[]) => void;
onBlur?: (e: FocusEvent, func: string) => void;
}

export const HotkeyInput = (props: Props) => {
const { value, onChange } = props;
const { value, onChange, func, isDuplicate } = props;

const changeRef = useRef<string[]>([]);
const [keys, setKeys] = useState(value);

return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<KeyWrapper>
<KeyWrapper isDuplicate={isDuplicate}>
<input
onKeyUp={() => {
const ret = changeRef.current.slice();
Expand All @@ -80,13 +80,12 @@ export const HotkeyInput = (props: Props) => {
changeRef.current = [...new Set([...changeRef.current, key])];
setKeys(changeRef.current);
}}
onBlur={(e) => props.onBlur && props.onBlur(e, func)}
/>

<div className="list">
{keys.map((key) => (
<div key={key} className="item">
{key}
</div>
<Kbd key={key}>{key}</Kbd>
))}
</div>
</KeyWrapper>
Expand All @@ -98,6 +97,7 @@ export const HotkeyInput = (props: Props) => {
onClick={() => {
onChange([]);
setKeys([]);
props.onBlur && props.onBlur({} as never, func);
}}
>
<DeleteRounded fontSize="inherit" />
Expand Down
51 changes: 46 additions & 5 deletions src/components/setting/mods/hotkey-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import { BaseDialog, DialogRef } from "@/components/base";
import { useNotification } from "@/hooks/use-notification";
import { useVerge } from "@/hooks/use-verge";
import { Typography, styled } from "@mui/material";
import { useLockFn } from "ahooks";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLatest, useLockFn } from "ahooks";
import {
FocusEvent,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { HotkeyInput } from "./hotkey-input";

Expand Down Expand Up @@ -35,6 +41,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
const { verge, patchVerge } = useVerge();

const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
const hotkeyMapRef = useLatest(hotkeyMap);

useImperativeHandle(ref, () => ({
open: () => {
Expand All @@ -54,11 +61,26 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
});

setHotkeyMap(map);
setDuplicateItems([]);
},
close: () => setOpen(false),
}));

const onSave = useLockFn(async () => {
// 检查是否有快捷键重复
const [duplicateItems, setDuplicateItems] = useState<string[]>([]);
const isDuplicate = !!duplicateItems.length;
const onBlur = (e: FocusEvent, func: string) => {
console.log(func);
const keys = Object.values(hotkeyMapRef.current).flat().filter(Boolean);
const set = new Set(keys);
if (keys.length !== set.size) {
setDuplicateItems([...duplicateItems, func]);
} else {
setDuplicateItems(duplicateItems.filter((e) => e !== func));
}
};

const saveState = useLockFn(async () => {
const hotkeys = Object.entries(hotkeyMap)
.map(([func, keys]) => {
if (!func || !keys?.length) return "";
Expand All @@ -76,18 +98,30 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {

try {
await patchVerge({ hotkeys });
setOpen(false);
} catch (err: any) {
useNotification(t("Error"), err.message || err.toString());
}
});

useEffect(() => {
if (!duplicateItems.length && open) {
saveState();
}
}, [hotkeyMap, duplicateItems, open]);

const onSave = () => {
saveState().then(() => {
setOpen(false);
});
};

return (
<BaseDialog
open={open}
title={t("Hotkey Viewer")}
contentSx={{ width: 450, maxHeight: 330 }}
okBtn={t("Save")}
okBtnDisabled={isDuplicate}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
Expand All @@ -97,8 +131,15 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
<ItemWrapper key={func}>
<Typography>{t(func)}</Typography>
<HotkeyInput
func={func}
isDuplicate={duplicateItems.includes(func)}
onBlur={onBlur}
value={hotkeyMap[func] ?? []}
onChange={(v) => setHotkeyMap((m) => ({ ...m, [func]: v }))}
onChange={(v) => {
const map = { ...hotkeyMapRef.current, [func]: v };
hotkeyMapRef.current = map;
setHotkeyMap(map);
}}
/>
</ItemWrapper>
))}
Expand Down

0 comments on commit f047b7c

Please sign in to comment.