feat: 增加复制粘贴功能
This commit is contained in:
+86
-12
@@ -10,13 +10,19 @@ import {
|
||||
import { Menu } from "antd";
|
||||
import state from "../store/state";
|
||||
import store from "../store/global";
|
||||
import { createNotation, isNote, notations as N } from "../util/notation";
|
||||
import {
|
||||
cloneNotation,
|
||||
createNotation,
|
||||
isNote,
|
||||
notations as N,
|
||||
} from "../util/notation";
|
||||
import {
|
||||
cloneParagraph,
|
||||
createParagraph,
|
||||
createParagraphWithNotations,
|
||||
} from "../util/paragraph";
|
||||
import { exportFile, loadFile, saveFile } from "../util/file";
|
||||
import { findParagraphAndNotation } from "../util/editor";
|
||||
import { findParagraphAndNotation, resetGlobalData } from "../util/editor";
|
||||
import {
|
||||
go,
|
||||
runInWrappedAction,
|
||||
@@ -27,15 +33,18 @@ import {
|
||||
const { SubMenu } = Menu;
|
||||
|
||||
const handleOpenFile = unwrappedAction((ev) => {
|
||||
// TODO: working
|
||||
const file = ev.target.files[0];
|
||||
loadFile(file);
|
||||
});
|
||||
const handleSaveFile = unwrappedAction(() => {
|
||||
const handleSaveFile = () => {
|
||||
saveFile();
|
||||
});
|
||||
const handleExportFile = () => {
|
||||
};
|
||||
const handleExportFile = unwrappedAction(() => {
|
||||
state.selectedNotationKey = null;
|
||||
exportFile();
|
||||
});
|
||||
const handleCreate = () => {
|
||||
resetGlobalData();
|
||||
};
|
||||
const handleEditMenu = wrappedAction(({ key }) => {
|
||||
switch (key) {
|
||||
@@ -149,7 +158,6 @@ const handleKeyPress = wrappedAction((ev) => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case inputKey === "l" && !ctrl && !shift:
|
||||
case inputKey === "enter" && !ctrl && !shift: {
|
||||
if (paragraph.notations[notationIndex + 1]) {
|
||||
state.selectedNotationKey =
|
||||
@@ -161,7 +169,15 @@ const handleKeyPress = wrappedAction((ev) => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case inputKey === "h" && !ctrl && !shift:
|
||||
case inputKey === "l" && !ctrl && !shift: {
|
||||
const nextNotation =
|
||||
paragraph.notations[notationIndex + 1] ||
|
||||
store.paragraphs[paragraphIndex + 1].notations?.at(0);
|
||||
if (nextNotation) {
|
||||
state.selectedNotationKey = nextNotation.key;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case inputKey === "enter" && !ctrl && shift: {
|
||||
if (paragraph.notations[notationIndex - 1]) {
|
||||
state.selectedNotationKey =
|
||||
@@ -169,6 +185,15 @@ const handleKeyPress = wrappedAction((ev) => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case inputKey === "h" && !ctrl && !shift: {
|
||||
const prevNotation =
|
||||
paragraph.notations[notationIndex - 1] ||
|
||||
store.paragraphs[paragraphIndex - 1].notations?.at(-1);
|
||||
if (prevNotation) {
|
||||
state.selectedNotationKey = prevNotation.key;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case inputKey === "enter" && ctrl && !shift: {
|
||||
const newNotation = createNotation();
|
||||
if (
|
||||
@@ -206,6 +231,7 @@ const handleKeyPress = wrappedAction((ev) => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case inputKey === "backspace" && !ctrl && !shift:
|
||||
case inputKey === "delete" && !ctrl && !shift: {
|
||||
paragraph.notations.splice(notationIndex, 1);
|
||||
const currNotation =
|
||||
@@ -214,6 +240,17 @@ const handleKeyPress = wrappedAction((ev) => {
|
||||
state.selectedNotationKey = currNotation?.key;
|
||||
break;
|
||||
}
|
||||
case inputKey === "backspace" && !ctrl && shift:
|
||||
case inputKey === "delete" && !ctrl && shift: {
|
||||
store.paragraphs.splice(paragraphIndex, 1);
|
||||
const currParagraph =
|
||||
store.paragraphs[paragraphIndex] || store.paragraphs.at(-1);
|
||||
const currNotation =
|
||||
currParagraph?.notations?.at(notationIndex) ||
|
||||
currParagraph?.notations?.at(-1);
|
||||
state.selectedNotationKey = currNotation?.key;
|
||||
break;
|
||||
}
|
||||
case inputKey === "=" && !ctrl && !shift: {
|
||||
if (typeof paragraph.alignJustify !== "boolean") {
|
||||
paragraph.alignJustify = !(
|
||||
@@ -224,6 +261,40 @@ const handleKeyPress = wrappedAction((ev) => {
|
||||
paragraph.alignJustify = !paragraph.alignJustify;
|
||||
break;
|
||||
}
|
||||
case inputKey === "c" && ctrl && !shift: {
|
||||
ev.preventDefault();
|
||||
state.clipboardContent = notation;
|
||||
break;
|
||||
}
|
||||
case inputKey === "c" && ctrl && shift: {
|
||||
ev.preventDefault();
|
||||
state.clipboardContent = paragraph;
|
||||
break;
|
||||
}
|
||||
case inputKey === "v" && ctrl && !shift: {
|
||||
ev.preventDefault();
|
||||
if (state.clipboardContent) {
|
||||
switch (state.clipboardContent.type) {
|
||||
case "notation":
|
||||
paragraph.notations.splice(
|
||||
notationIndex + 1,
|
||||
0,
|
||||
cloneNotation(state.clipboardContent)
|
||||
);
|
||||
break;
|
||||
case "paragraph":
|
||||
store.paragraphs.splice(
|
||||
paragraphIndex + 1,
|
||||
0,
|
||||
cloneParagraph(state.clipboardContent)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
state.clipboardContent = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -231,9 +302,12 @@ const handleKeyPress = wrappedAction((ev) => {
|
||||
// 仅未选中符号时
|
||||
switch (true) {
|
||||
case ["h", "j", "k", "l"].includes(inputKey): {
|
||||
state.selectedNotationKey =
|
||||
state.lastSelectedNotationKey ||
|
||||
store.paragraphs?.at(0)?.notations?.at(0).key;
|
||||
const { notation: n } = findParagraphAndNotation(
|
||||
state.lastSelectedNotationKey
|
||||
);
|
||||
state.selectedNotationKey = (
|
||||
n || store.paragraphs?.at(0)?.notations?.at(0)
|
||||
)?.key;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -279,7 +353,7 @@ const handleKeyPress = wrappedAction((ev) => {
|
||||
|
||||
const fileMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key="create" icon={<PlusOutlined />}>
|
||||
<Menu.Item key="create" icon={<PlusOutlined />} onClick={handleCreate}>
|
||||
新建
|
||||
</Menu.Item>
|
||||
<Menu.Item key="open" icon={<FolderOpenOutlined />}>
|
||||
|
||||
@@ -58,7 +58,7 @@ const getNotationContextMenu = (notation, paragraph) => {
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
||||
删除
|
||||
删除符号
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
+2
-6
@@ -14,12 +14,8 @@ let globalState = observable({
|
||||
helpDialogVisible: false,
|
||||
// 配置项弹窗是否可见
|
||||
configDialogVisible: false,
|
||||
// 持久化的文件
|
||||
file: null,
|
||||
// ref
|
||||
refs: {
|
||||
svg: null,
|
||||
},
|
||||
// 复制剪贴的内容
|
||||
clipboardContent: null,
|
||||
});
|
||||
|
||||
export default globalState;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { remove, transaction } from "mobx";
|
||||
import state from "../store/state";
|
||||
import store from "../store/global";
|
||||
import { clearHistory, unwrappedAction } from "../store/history";
|
||||
import { createParagraphWithNotations } from "../util/paragraph";
|
||||
import { VERSION } from "./version";
|
||||
|
||||
@@ -49,8 +52,23 @@ function findParagraphAndNotation(key) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const resetGlobalData = unwrappedAction((globalData) => {
|
||||
if (!globalData) {
|
||||
globalData = getDefaultGlobalDataWidthNotations();
|
||||
}
|
||||
state.lastSelectedNotationKey = state.selectedNotationKey = state.tieSourceKey = null;
|
||||
clearHistory();
|
||||
transaction(() => {
|
||||
for (const key of Object.keys(store)) {
|
||||
remove(store, key);
|
||||
}
|
||||
Object.assign(store, globalData);
|
||||
});
|
||||
});
|
||||
|
||||
export {
|
||||
getDefaultGlobalData,
|
||||
findParagraphAndNotation,
|
||||
getDefaultGlobalDataWidthNotations,
|
||||
resetGlobalData,
|
||||
};
|
||||
|
||||
+10
-17
@@ -1,11 +1,9 @@
|
||||
import { message } from "antd";
|
||||
import { remove, transaction } from "mobx";
|
||||
import { toJS } from "mobx";
|
||||
import globalState from "../store/state";
|
||||
import store from "../store/global";
|
||||
import { clearHistory } from "../store/history";
|
||||
import domtoimage from "./dom-to-image";
|
||||
import { VERSION } from "./version";
|
||||
import { resetGlobalData } from "./editor";
|
||||
|
||||
function read(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -23,14 +21,7 @@ async function loadFile(file) {
|
||||
message.error("不支持的文件格式,请使用最新版本");
|
||||
return;
|
||||
}
|
||||
globalState.lastSelectedNotationKey = globalState.selectedNotationKey = null;
|
||||
clearHistory();
|
||||
transaction(() => {
|
||||
for (const key of Object.keys(store)) {
|
||||
remove(store, key);
|
||||
}
|
||||
Object.assign(store, data);
|
||||
});
|
||||
resetGlobalData(data);
|
||||
}
|
||||
|
||||
function saveFile() {
|
||||
@@ -45,12 +36,14 @@ function saveFile() {
|
||||
}
|
||||
|
||||
function exportFile() {
|
||||
domtoimage.toPng(document.querySelector("#temp_svg")).then((dataUrl) => {
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.download = (store.title || "未标题") + ".png";
|
||||
downloadLink.href = dataUrl;
|
||||
downloadLink.click();
|
||||
});
|
||||
domtoimage
|
||||
.toPng(document.querySelector("#temp_svg"), { bgcolor: "white" })
|
||||
.then((dataUrl) => {
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.download = (store.title || "未标题") + ".png";
|
||||
downloadLink.href = dataUrl;
|
||||
downloadLink.click();
|
||||
});
|
||||
}
|
||||
|
||||
export { saveFile, loadFile, exportFile };
|
||||
|
||||
+10
-1
@@ -1,3 +1,5 @@
|
||||
import { toJS } from "mobx";
|
||||
|
||||
const notations = {
|
||||
zero: "0",
|
||||
do: "1",
|
||||
@@ -31,6 +33,7 @@ function isSeparator(notationOrNote) {
|
||||
// 创建符号
|
||||
function createNotation(initial) {
|
||||
const n = {
|
||||
type: "notation",
|
||||
key: `n_${String(Math.random())}`,
|
||||
note: "0",
|
||||
octave: 0,
|
||||
@@ -46,5 +49,11 @@ function createNotation(initial) {
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function cloneNotation(notation) {
|
||||
const origin = JSON.parse(JSON.stringify(toJS(notation)));
|
||||
delete origin.key;
|
||||
const n = createNotation(origin);
|
||||
return n;
|
||||
}
|
||||
|
||||
export { notations, isNote, isSeparator, createNotation };
|
||||
export { notations, isNote, isSeparator, createNotation, cloneNotation };
|
||||
|
||||
+10
-2
@@ -1,8 +1,10 @@
|
||||
import { createNotation, notations as N } from "./notation";
|
||||
import { toJS } from "mobx";
|
||||
import { cloneNotation, createNotation, notations as N } from "./notation";
|
||||
|
||||
// 新建段落
|
||||
function createParagraph(initial) {
|
||||
const p = {
|
||||
type: "paragraph",
|
||||
key: `p_${String(Math.random())}`,
|
||||
// 符号列表
|
||||
notations: [],
|
||||
@@ -25,5 +27,11 @@ function createParagraphWithNotations() {
|
||||
],
|
||||
});
|
||||
}
|
||||
function cloneParagraph(paragraph) {
|
||||
const origin = JSON.parse(JSON.stringify(toJS(paragraph)));
|
||||
delete origin.key;
|
||||
origin.notations = paragraph.notations.map((n) => cloneNotation(n));
|
||||
return createParagraph(origin);
|
||||
}
|
||||
|
||||
export { createParagraph, createParagraphWithNotations };
|
||||
export { createParagraph, createParagraphWithNotations, cloneParagraph };
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useRef } from "react";
|
||||
import state from "../../store/state";
|
||||
import store from "../../store/global";
|
||||
import Styles from "./index.module.css";
|
||||
|
||||
// ENHANCE: 增加整体缩放能力
|
||||
|
||||
// FIXME: 修改获取svg元素的方式
|
||||
function Canvas({ children, ...props }) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
state.refs.svg = ref;
|
||||
return () => {
|
||||
state.refs.svg = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
id="temp_svg"
|
||||
xmlns=" http://www.w3.org/2000/svg"
|
||||
className={Styles.svg}
|
||||
// viewBox={`0 0 ${store.canvasWidth} ${store.canvasHeight}`}
|
||||
width={store.canvasWidth}
|
||||
height={store.canvasHeight}
|
||||
// TODO: do this
|
||||
onContextMenu={(ev) => ev.preventDefault()}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -36,6 +36,7 @@ function HelpModal({ visible, onVisibleChange }) {
|
||||
<KeyboardDescItem kbd="?" desc="显示此帮助" />
|
||||
<KeyboardDescItem kbd="Ctrl+Z" desc="撤销" />
|
||||
<KeyboardDescItem kbd="Ctrl+Shift+Z" desc="重做" />
|
||||
<KeyboardDescItem kbd="Ctrl+O" desc="打开文件" />
|
||||
<KeyboardDescItem kbd="h j k l" desc="移动焦点" />
|
||||
</ul>
|
||||
<h3>仅选中符号时</h3>
|
||||
@@ -55,8 +56,12 @@ function HelpModal({ visible, onVisibleChange }) {
|
||||
<KeyboardDescItem kbd="Shift+Enter" desc="移动到上一个音符" />
|
||||
<KeyboardDescItem kbd="Ctrl+Enter" desc="在当前位置后插入音符" />
|
||||
<KeyboardDescItem kbd="Delete" desc="删除当前音符" />
|
||||
<KeyboardDescItem kbd="Shift+Delete" desc="删除当前段落" />
|
||||
<KeyboardDescItem kbd="=" desc="段落是否两端对齐" />
|
||||
<KeyboardDescItem kbd="h j k l" desc="Vi风格移动焦点" />
|
||||
<KeyboardDescItem kbd="Ctrl+C" desc="复制当前音符" />
|
||||
<KeyboardDescItem kbd="Ctrl+Shift+C" desc="复制当前段落" />
|
||||
<KeyboardDescItem kbd="Ctrl+V" desc="粘贴" />
|
||||
</ul>
|
||||
<h2>提示</h2>
|
||||
<ul className={Styles.list}>
|
||||
|
||||
Reference in New Issue
Block a user