pretty big changes: added highlighted keys refactored Octave component to use a PianoKeyComponent and updated TS types to enums
This commit is contained in:
@@ -4,11 +4,10 @@ import {
|
|||||||
NoteNameBehavior,
|
NoteNameBehavior,
|
||||||
octaveKeys,
|
octaveKeys,
|
||||||
Accidental,
|
Accidental,
|
||||||
|
HighlightedKey,
|
||||||
} from "../../models/Piano";
|
} from "../../models/Piano";
|
||||||
import { Box, Row, Pressable, ZStack, Text } from "native-base";
|
import { Box, Row, Pressable, Text } from "native-base";
|
||||||
|
import PianoKeyComp from "./PianoKeyComp";
|
||||||
const notesList: Array<Note> = ["C", "D", "E", "F", "G", "A", "B"];
|
|
||||||
const accidentalsList: Array<Accidental> = ["#", "b", "##", "bb"];
|
|
||||||
|
|
||||||
const getKeyIndex = (n: Note, keys: PianoKey[]) => {
|
const getKeyIndex = (n: Note, keys: PianoKey[]) => {
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
@@ -25,14 +24,14 @@ const isNoteVisible = (
|
|||||||
isHovered: boolean,
|
isHovered: boolean,
|
||||||
isHighlighted: boolean
|
isHighlighted: boolean
|
||||||
) => {
|
) => {
|
||||||
if (showNoteNamesPolicy === "always") return true;
|
if (showNoteNamesPolicy === NoteNameBehavior.always) return true;
|
||||||
if (showNoteNamesPolicy === "never") return false;
|
if (showNoteNamesPolicy === NoteNameBehavior.never) return false;
|
||||||
|
|
||||||
if (showNoteNamesPolicy === "onpress") {
|
if (showNoteNamesPolicy === NoteNameBehavior.onpress) {
|
||||||
return isPressed;
|
return isPressed;
|
||||||
} else if (showNoteNamesPolicy === "onhover") {
|
} else if (showNoteNamesPolicy === NoteNameBehavior.onhover) {
|
||||||
return isHovered;
|
return isHovered;
|
||||||
} else if (showNoteNamesPolicy === "onhighlight") {
|
} else if (showNoteNamesPolicy === NoteNameBehavior.onhighlight) {
|
||||||
return isHighlighted;
|
return isHighlighted;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -44,13 +43,36 @@ type OctaveProps = Parameters<typeof Box>[0] & {
|
|||||||
endNote: Note;
|
endNote: Note;
|
||||||
showNoteNames: NoteNameBehavior;
|
showNoteNames: NoteNameBehavior;
|
||||||
showOctaveNumber: boolean;
|
showOctaveNumber: boolean;
|
||||||
|
whiteKeyBg: string;
|
||||||
|
whiteKeyBgPressed: string;
|
||||||
|
whiteKeyBgHovered: string;
|
||||||
|
blackKeyBg: string;
|
||||||
|
blackKeyBgPressed: string;
|
||||||
|
blackKeyBgHovered: string;
|
||||||
|
highlightedNotes: Array<HighlightedKey>;
|
||||||
|
defaultHighlightColor: string;
|
||||||
onNoteDown: (note: PianoKey) => void;
|
onNoteDown: (note: PianoKey) => void;
|
||||||
onNoteUp: (note: PianoKey) => void;
|
onNoteUp: (note: PianoKey) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Octave = (props: OctaveProps) => {
|
const Octave = (props: OctaveProps) => {
|
||||||
const { number, startNote, endNote, showNoteNames, showOctaveNumber, onNoteDown, onNoteUp } =
|
const {
|
||||||
props;
|
number,
|
||||||
|
startNote,
|
||||||
|
endNote,
|
||||||
|
showNoteNames,
|
||||||
|
showOctaveNumber,
|
||||||
|
whiteKeyBg,
|
||||||
|
whiteKeyBgPressed,
|
||||||
|
whiteKeyBgHovered,
|
||||||
|
blackKeyBg,
|
||||||
|
blackKeyBgPressed,
|
||||||
|
blackKeyBgHovered,
|
||||||
|
highlightedNotes,
|
||||||
|
defaultHighlightColor,
|
||||||
|
onNoteDown,
|
||||||
|
onNoteUp,
|
||||||
|
} = props;
|
||||||
const oK: PianoKey[] = octaveKeys.map((k) => {
|
const oK: PianoKey[] = octaveKeys.map((k) => {
|
||||||
return new PianoKey(k.note, k.accidental, number);
|
return new PianoKey(k.note, k.accidental, number);
|
||||||
});
|
});
|
||||||
@@ -71,102 +93,61 @@ const Octave = (props: OctaveProps) => {
|
|||||||
<Box {...props}>
|
<Box {...props}>
|
||||||
<Row height={"100%"} width={"100%"}>
|
<Row height={"100%"} width={"100%"}>
|
||||||
{whiteKeys.map((key, i) => {
|
{whiteKeys.map((key, i) => {
|
||||||
|
const highlightedKey = highlightedNotes.find(
|
||||||
|
(h) =>
|
||||||
|
h.key.note === key.note && h.key.accidental === key.accidental
|
||||||
|
);
|
||||||
|
const isHighlighted = highlightedKey !== undefined;
|
||||||
|
const highlightColor =
|
||||||
|
highlightedKey?.bgColor ?? defaultHighlightColor;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<PianoKeyComp
|
||||||
width={whiteKeyWidthExpr}
|
key={i}
|
||||||
height={whiteKeyHeightExpr}
|
pianoKey={key}
|
||||||
key={i}
|
bg={isHighlighted ? highlightColor :whiteKeyBg}
|
||||||
onPressIn={() => onNoteDown(key)}
|
bgPressed={isHighlighted ? highlightColor : whiteKeyBgPressed}
|
||||||
onPressOut={() => onNoteUp(key)}
|
bgHovered={isHighlighted ? highlightColor : whiteKeyBgHovered}
|
||||||
>
|
onKeyDown={() => onNoteDown(key)}
|
||||||
{({ isHovered, isPressed }) => (
|
onKeyUp={() => onNoteUp(key)}
|
||||||
<Box
|
style={{
|
||||||
bg={
|
width: whiteKeyWidthExpr,
|
||||||
isHovered ? (isPressed ? "gray.300" : "gray.100") : "white"
|
height: whiteKeyHeightExpr,
|
||||||
}
|
}}
|
||||||
w="100%"
|
|
||||||
h="100%"
|
/>
|
||||||
borderWidth="1px"
|
|
||||||
borderColor="black"
|
|
||||||
justifyContent="flex-end"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
{isNoteVisible(
|
|
||||||
showNoteNames,
|
|
||||||
isPressed,
|
|
||||||
isHovered,
|
|
||||||
false
|
|
||||||
) && (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
userSelect: "none",
|
|
||||||
WebkitUserSelect: "none",
|
|
||||||
MozUserSelect: "none",
|
|
||||||
msUserSelect: "none",
|
|
||||||
}}
|
|
||||||
fontSize="xl"
|
|
||||||
>
|
|
||||||
{key.note}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{blackKeys.map((key, i) => {
|
{blackKeys.map((key, i) => {
|
||||||
|
const highlightedKey = highlightedNotes.find(
|
||||||
|
(h) =>
|
||||||
|
h.key.note === key.note && h.key.accidental === key.accidental
|
||||||
|
);
|
||||||
|
const isHighlighted = highlightedKey !== undefined;
|
||||||
|
const highlightColor =
|
||||||
|
highlightedKey?.bgColor ?? defaultHighlightColor;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<PianoKeyComp
|
||||||
key={i}
|
key={i}
|
||||||
onPressIn={() => onNoteDown(key)}
|
pianoKey={key}
|
||||||
onPressOut={() => onNoteUp(key)}
|
bg={isHighlighted ? highlightColor : blackKeyBg}
|
||||||
width={blackKeyWidthExpr}
|
bgPressed={isHighlighted ? highlightColor : blackKeyBgPressed}
|
||||||
height={blackKeyHeightExpr}
|
bgHovered={isHighlighted ? highlightColor : blackKeyBgHovered}
|
||||||
|
onKeyDown={() => onNoteDown(key)}
|
||||||
|
onKeyUp={() => onNoteUp(key)}
|
||||||
style={{
|
style={{
|
||||||
|
width: blackKeyWidthExpr,
|
||||||
|
height: blackKeyHeightExpr,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: `calc(calc(${whiteKeyWidthExpr} * ${
|
left: `calc(calc(${whiteKeyWidthExpr} * ${
|
||||||
i + ((i > 1) as unknown as number) + 1
|
i + ((i > 1) as unknown as number) + 1
|
||||||
}) - calc(${blackKeyWidthExpr} / 2))`,
|
}) - calc(${blackKeyWidthExpr} / 2))`,
|
||||||
top: "0px",
|
top: "0px",
|
||||||
}}
|
}}
|
||||||
>
|
text={{
|
||||||
{({ isHovered, isPressed }) => (
|
color: "white",
|
||||||
<Box
|
fontSize: "xs",
|
||||||
bg={
|
}}
|
||||||
isHovered ? (isPressed ? "gray.700" : "gray.800") : "black"
|
/>
|
||||||
}
|
|
||||||
w="100%"
|
|
||||||
h="100%"
|
|
||||||
borderWidth="1px"
|
|
||||||
borderColor="black"
|
|
||||||
color="white"
|
|
||||||
style={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isNoteVisible(
|
|
||||||
showNoteNames,
|
|
||||||
isPressed,
|
|
||||||
isHovered,
|
|
||||||
false
|
|
||||||
) && (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
userSelect: "none",
|
|
||||||
WebkitUserSelect: "none",
|
|
||||||
MozUserSelect: "none",
|
|
||||||
msUserSelect: "none",
|
|
||||||
}}
|
|
||||||
fontSize="xs"
|
|
||||||
color="white"
|
|
||||||
>
|
|
||||||
{key.note + key.accidental}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Row>
|
</Row>
|
||||||
@@ -195,6 +176,16 @@ const Octave = (props: OctaveProps) => {
|
|||||||
Octave.defaultProps = {
|
Octave.defaultProps = {
|
||||||
startNote: "C",
|
startNote: "C",
|
||||||
endNote: "B",
|
endNote: "B",
|
||||||
|
showNoteNames: "onpress",
|
||||||
|
showOctaveNumber: false,
|
||||||
|
whiteKeyBg: "white",
|
||||||
|
whiteKeyBgPressed: "gray.200",
|
||||||
|
whiteKeyBgHovered: "gray.100",
|
||||||
|
blackKeyBg: "black",
|
||||||
|
blackKeyBgPressed: "gray.600",
|
||||||
|
blackKeyBgHovered: "gray.700",
|
||||||
|
highlightedNotes: [],
|
||||||
|
defaultHighlightColor: "#FF0000",
|
||||||
onNoteDown: () => {},
|
onNoteDown: () => {},
|
||||||
onNoteUp: () => {},
|
onNoteUp: () => {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { Box, Pressable, Text } from "native-base";
|
||||||
|
import { Key } from "react";
|
||||||
|
import { StyleProp, ViewStyle } from "react-native";
|
||||||
|
import {
|
||||||
|
Note,
|
||||||
|
PianoKey,
|
||||||
|
NoteNameBehavior,
|
||||||
|
octaveKeys,
|
||||||
|
Accidental,
|
||||||
|
HighlightedKey,
|
||||||
|
keyToStr,
|
||||||
|
} from "../../models/Piano";
|
||||||
|
|
||||||
|
type PianoKeyProps = {
|
||||||
|
key?: Key;
|
||||||
|
pianoKey: PianoKey;
|
||||||
|
showNoteNames: NoteNameBehavior;
|
||||||
|
bg: string;
|
||||||
|
bgPressed: string;
|
||||||
|
bgHovered: string;
|
||||||
|
onKeyDown: () => void;
|
||||||
|
onKeyUp: () => void;
|
||||||
|
text: Parameters<typeof Text>[0];
|
||||||
|
style: StyleProp<ViewStyle>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNoteVisible = (
|
||||||
|
noteNameBehavior: NoteNameBehavior,
|
||||||
|
isPressed: boolean,
|
||||||
|
isHovered: boolean
|
||||||
|
) => {
|
||||||
|
if (noteNameBehavior === NoteNameBehavior.always) return true;
|
||||||
|
if (noteNameBehavior === NoteNameBehavior.never) return false;
|
||||||
|
|
||||||
|
if (noteNameBehavior === NoteNameBehavior.onpress) {
|
||||||
|
return isPressed;
|
||||||
|
} else if (noteNameBehavior === NoteNameBehavior.onhover) {
|
||||||
|
return isHovered;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PianoKeyComp = ({
|
||||||
|
key,
|
||||||
|
pianoKey,
|
||||||
|
showNoteNames,
|
||||||
|
bg,
|
||||||
|
bgPressed,
|
||||||
|
bgHovered,
|
||||||
|
onKeyDown,
|
||||||
|
onKeyUp,
|
||||||
|
text,
|
||||||
|
style,
|
||||||
|
}: PianoKeyProps) => {
|
||||||
|
const textDefaultProps = {
|
||||||
|
style: {
|
||||||
|
userSelect: "none",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
MozUserSelect: "none",
|
||||||
|
msUserSelect: "none",
|
||||||
|
},
|
||||||
|
fontSize: "xl",
|
||||||
|
color: "black",
|
||||||
|
} as Parameters<typeof Text>[0];
|
||||||
|
|
||||||
|
const textProps = { ...textDefaultProps, ...text };
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={key}
|
||||||
|
onPressIn={onKeyDown}
|
||||||
|
onPressOut={onKeyUp}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{({ isHovered, isPressed }) => (
|
||||||
|
<Box
|
||||||
|
bg={(() => {
|
||||||
|
if (isPressed) return bgPressed;
|
||||||
|
if (isHovered) return bgHovered;
|
||||||
|
return bg;
|
||||||
|
})()}
|
||||||
|
w="100%"
|
||||||
|
h="100%"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="black"
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{isNoteVisible(showNoteNames, isPressed, isHovered) && (
|
||||||
|
<Text {...textProps}>{keyToStr(pianoKey)}</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PianoKeyComp.defaultProps = {
|
||||||
|
key: octaveKeys[0],
|
||||||
|
showNoteNames: NoteNameBehavior.onhover,
|
||||||
|
keyBg: "white",
|
||||||
|
keyBgPressed: "gray.200",
|
||||||
|
keyBgHovered: "gray.100",
|
||||||
|
onKeyDown: () => {},
|
||||||
|
onKeyUp: () => {},
|
||||||
|
text: {},
|
||||||
|
style: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PianoKeyComp;
|
||||||
@@ -36,7 +36,15 @@ const VirtualPiano = ({
|
|||||||
keyPressStyle,
|
keyPressStyle,
|
||||||
vividKeyPressColor,
|
vividKeyPressColor,
|
||||||
}: VirtualPianoProps) => {
|
}: VirtualPianoProps) => {
|
||||||
const notesList: Array<Note> = ["C", "D", "E", "F", "G", "A", "B"];
|
const notesList: Array<Note> = [
|
||||||
|
Note.C,
|
||||||
|
Note.D,
|
||||||
|
Note.E,
|
||||||
|
Note.F,
|
||||||
|
Note.G,
|
||||||
|
Note.A,
|
||||||
|
Note.B,
|
||||||
|
];
|
||||||
const octaveList = [];
|
const octaveList = [];
|
||||||
|
|
||||||
for (let octaveNum = startOctave; octaveNum <= endOctave; octaveNum++) {
|
for (let octaveNum = startOctave; octaveNum <= endOctave; octaveNum++) {
|
||||||
@@ -73,9 +81,9 @@ VirtualPiano.defaultProps = {
|
|||||||
console.log("Note up: " + n);
|
console.log("Note up: " + n);
|
||||||
},
|
},
|
||||||
startOctave: 2,
|
startOctave: 2,
|
||||||
startNote: "C",
|
startNote: Note.C,
|
||||||
endOctave: 2,
|
endOctave: 2,
|
||||||
endNote: "B",
|
endNote: Note.B,
|
||||||
showNoteNames: "onhover",
|
showNoteNames: "onhover",
|
||||||
highlightedNotes: [],
|
highlightedNotes: [],
|
||||||
highlightColor: "red",
|
highlightColor: "red",
|
||||||
|
|||||||
+108
-27
@@ -1,38 +1,119 @@
|
|||||||
|
export enum Note {
|
||||||
|
"C",
|
||||||
|
"D",
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"G",
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Accidental {
|
||||||
|
"#",
|
||||||
|
"b",
|
||||||
|
"##",
|
||||||
|
"bb",
|
||||||
|
}
|
||||||
|
|
||||||
export type Note = "C" | "D" | "E" | "F" | "G" | "A" | "B";
|
export enum NoteNameBehavior {
|
||||||
export type Accidental = "#" | "b" | "##" | "bb";
|
"always",
|
||||||
|
"onpress",
|
||||||
export type NoteNameBehavior = "always" | "onpress" | "onhighlight" | "onhover" | "never";
|
"onhighlight",
|
||||||
export type KeyPressStyle = "subtle" | "vivid";
|
"onhover",
|
||||||
|
"never",
|
||||||
|
}
|
||||||
|
export enum KeyPressStyle {
|
||||||
|
"subtle",
|
||||||
|
"vivid",
|
||||||
|
};
|
||||||
|
export type HighlightedKey = {
|
||||||
|
key: PianoKey;
|
||||||
|
// if not specified, the default color for highlighted notes will be used
|
||||||
|
bgColor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class PianoKey {
|
export class PianoKey {
|
||||||
public note: Note;
|
public note: Note;
|
||||||
public accidental?: Accidental;
|
public accidental?: Accidental;
|
||||||
public octave?: number;
|
public octave?: number;
|
||||||
|
|
||||||
constructor(note: Note, accidental?: Accidental, octave?: number) {
|
constructor(note: Note, accidental?: Accidental, octave?: number) {
|
||||||
this.note = note;
|
this.note = note;
|
||||||
this.accidental = accidental;
|
this.accidental = accidental;
|
||||||
this.octave = octave;
|
this.octave = octave;
|
||||||
};
|
}
|
||||||
|
|
||||||
public toString = () => {
|
public toString = () => {
|
||||||
return this.note + (this.accidental || "") + (this.octave || "");
|
return this.note as unknown as string + (this.accidental || "") + (this.octave || "");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const strToKey = (str: string): PianoKey => {
|
||||||
|
let note : Note;
|
||||||
|
switch (str[0]) {
|
||||||
|
case "C": note = Note.C; break;
|
||||||
|
case "D": note = Note.D; break;
|
||||||
|
case "E": note = Note.E; break;
|
||||||
|
case "F": note = Note.F; break;
|
||||||
|
case "G": note = Note.G; break;
|
||||||
|
case "A": note = Note.A; break;
|
||||||
|
case "B": note = Note.B; break;
|
||||||
|
default: throw new Error("Invalid note name");
|
||||||
}
|
}
|
||||||
|
if (str.length === 1) {
|
||||||
|
return new PianoKey(note);
|
||||||
|
}
|
||||||
|
let accidental : Accidental;
|
||||||
|
switch (str[1]) {
|
||||||
|
case "#": accidental = Accidental["#"]; break;
|
||||||
|
case "b": accidental = Accidental["b"]; break;
|
||||||
|
case "x": accidental = Accidental["##"]; break;
|
||||||
|
case "n": accidental = Accidental["bb"]; break;
|
||||||
|
default: throw new Error("Invalid accidental");
|
||||||
|
}
|
||||||
|
if (str.length === 2) {
|
||||||
|
return new PianoKey(note, accidental);
|
||||||
|
}
|
||||||
|
const octave = parseInt(str[2] as unknown as string);
|
||||||
|
return new PianoKey(note, accidental, octave);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const keyToStr = (key: PianoKey): string => {
|
||||||
|
let s = "";
|
||||||
|
switch (key.note) {
|
||||||
|
case Note.C: s += "C"; break;
|
||||||
|
case Note.D: s += "D"; break;
|
||||||
|
case Note.E: s += "E"; break;
|
||||||
|
case Note.F: s += "F"; break;
|
||||||
|
case Note.G: s += "G"; break;
|
||||||
|
case Note.A: s += "A"; break;
|
||||||
|
case Note.B: s += "B"; break;
|
||||||
|
}
|
||||||
|
if (key.accidental) {
|
||||||
|
switch (key.accidental) {
|
||||||
|
case Accidental["#"]: s += "#"; break;
|
||||||
|
case Accidental["b"]: s += "b"; break;
|
||||||
|
case Accidental["##"]: s += "x"; break;
|
||||||
|
case Accidental["bb"]: s += "n"; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key.octave) {
|
||||||
|
s += key.octave;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const octaveKeys: Array<PianoKey> = [
|
export const octaveKeys: Array<PianoKey> = [
|
||||||
new PianoKey("C", undefined),
|
new PianoKey(Note.C),
|
||||||
new PianoKey("C", "#"),
|
new PianoKey(Note.C, Accidental["#"]),
|
||||||
new PianoKey("D", undefined),
|
new PianoKey(Note.D),
|
||||||
new PianoKey("D", "#"),
|
new PianoKey(Note.D, Accidental["#"]),
|
||||||
new PianoKey("E", undefined),
|
new PianoKey(Note.E),
|
||||||
new PianoKey("F", undefined),
|
new PianoKey(Note.F),
|
||||||
new PianoKey("F", "#"),
|
new PianoKey(Note.F, Accidental["#"]),
|
||||||
new PianoKey("G", undefined),
|
new PianoKey(Note.G),
|
||||||
new PianoKey("G", "#"),
|
new PianoKey(Note.G, Accidental["#"]),
|
||||||
new PianoKey("A", undefined),
|
new PianoKey(Note.A),
|
||||||
new PianoKey("A", "#"),
|
new PianoKey(Note.A, Accidental["#"]),
|
||||||
new PianoKey("B", undefined),
|
new PianoKey(Note.B),
|
||||||
];
|
];
|
||||||
Reference in New Issue
Block a user