559 lines
21 KiB
TypeScript
559 lines
21 KiB
TypeScript
import { GetStaticProps } from "next";
|
||
import { useCallback, useMemo, useRef, useState } from "react";
|
||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||
import { Button } from "components/Inputs/Button";
|
||
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
||
import { ToolTip } from "components/ToolTip";
|
||
import { getOpenGraph } from "helpers/openGraph";
|
||
import { getLangui } from "graphql/fetchLocalData";
|
||
|
||
/*
|
||
* ╭─────────────╮
|
||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||
*/
|
||
|
||
const SIZE_MULTIPLIER = 1000;
|
||
|
||
/*
|
||
* ╭────────╮
|
||
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
||
*/
|
||
|
||
interface Props extends AppLayoutRequired {}
|
||
|
||
const replaceSelection = (
|
||
text: string,
|
||
selectionStart: number,
|
||
selectionEnd: number,
|
||
newSelectedText: string
|
||
) => text.substring(0, selectionStart) + newSelectedText + text.substring(selectionEnd);
|
||
|
||
const swapChar = (char: string, swaps: string[]): string => {
|
||
for (let index = 0; index < swaps.length; index++) {
|
||
if (char === swaps[index]) {
|
||
return swaps[(index + 1) % swaps.length];
|
||
}
|
||
}
|
||
return char;
|
||
};
|
||
|
||
const Transcript = (props: Props): JSX.Element => {
|
||
const [text, setText] = useState("");
|
||
const [fontSize, setFontSize] = useState(1);
|
||
const [xOffset, setXOffset] = useState(0);
|
||
const [lineIndex, setLineIndex] = useState(0);
|
||
|
||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||
|
||
const updateDisplayedText = useCallback(() => {
|
||
if (textAreaRef.current) {
|
||
setText(textAreaRef.current.value);
|
||
}
|
||
}, []);
|
||
|
||
const updateLineIndex = useCallback(() => {
|
||
if (textAreaRef.current) {
|
||
const subText = textAreaRef.current.value.substring(0, textAreaRef.current.selectionStart);
|
||
setLineIndex(subText.split("\n").length - 1);
|
||
}
|
||
}, []);
|
||
|
||
const convertFullWidth = useCallback(() => {
|
||
if (textAreaRef.current) {
|
||
textAreaRef.current.value = textAreaRef.current.value
|
||
// Numbers
|
||
.replaceAll("0", "0")
|
||
.replaceAll("1", "1")
|
||
.replaceAll("2", "2")
|
||
.replaceAll("3", "3")
|
||
.replaceAll("4", "4")
|
||
.replaceAll("5", "5")
|
||
.replaceAll("6", "6")
|
||
.replaceAll("7", "7")
|
||
.replaceAll("8", "8")
|
||
.replaceAll("9", "9")
|
||
// Uppercase letters
|
||
.replaceAll("A", "A")
|
||
.replaceAll("B", "B")
|
||
.replaceAll("C", "C")
|
||
.replaceAll("D", "D")
|
||
.replaceAll("E", "E")
|
||
.replaceAll("F", "F")
|
||
.replaceAll("G", "G")
|
||
.replaceAll("H", "H")
|
||
.replaceAll("I", "I")
|
||
.replaceAll("J", "J")
|
||
.replaceAll("K", "K")
|
||
.replaceAll("L", "L")
|
||
.replaceAll("M", "M")
|
||
.replaceAll("N", "N")
|
||
.replaceAll("O", "O")
|
||
.replaceAll("P", "P")
|
||
.replaceAll("Q", "Q")
|
||
.replaceAll("R", "R")
|
||
.replaceAll("S", "S")
|
||
.replaceAll("T", "T")
|
||
.replaceAll("U", "U")
|
||
.replaceAll("V", "V")
|
||
.replaceAll("W", "W")
|
||
.replaceAll("X", "X")
|
||
.replaceAll("Y", "Y")
|
||
.replaceAll("Z", "Z")
|
||
// Lowercase letters
|
||
.replaceAll("a", "a")
|
||
.replaceAll("b", "b")
|
||
.replaceAll("c", "c")
|
||
.replaceAll("d", "d")
|
||
.replaceAll("e", "e")
|
||
.replaceAll("f", "f")
|
||
.replaceAll("g", "g")
|
||
.replaceAll("h", "h")
|
||
.replaceAll("i", "i")
|
||
.replaceAll("j", "j")
|
||
.replaceAll("k", "k")
|
||
.replaceAll("l", "l")
|
||
.replaceAll("m", "m")
|
||
.replaceAll("n", "n")
|
||
.replaceAll("o", "o")
|
||
.replaceAll("p", "p")
|
||
.replaceAll("q", "q")
|
||
.replaceAll("r", "r")
|
||
.replaceAll("s", "s")
|
||
.replaceAll("t", "t")
|
||
.replaceAll("u", "u")
|
||
.replaceAll("v", "v")
|
||
.replaceAll("w", "w")
|
||
.replaceAll("x", "x")
|
||
.replaceAll("y", "y")
|
||
.replaceAll("z", "z")
|
||
// Others
|
||
.replaceAll(" ", " ")
|
||
.replaceAll(",", ",")
|
||
.replaceAll(".", ".")
|
||
.replaceAll(":", ":")
|
||
.replaceAll(";", ";")
|
||
.replaceAll("!", "!")
|
||
.replaceAll("?", "?")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'")
|
||
.replaceAll("`", "`")
|
||
.replaceAll("^", "^")
|
||
.replaceAll("~", "~")
|
||
.replaceAll("_", "_")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("@", "@")
|
||
.replaceAll("#", "#")
|
||
.replaceAll("%", "%")
|
||
.replaceAll("+", "+")
|
||
.replaceAll("-", "-")
|
||
.replaceAll("*", "*")
|
||
.replaceAll("=", "=")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll("(", "(")
|
||
.replaceAll(")", ")")
|
||
.replaceAll("[", "[")
|
||
.replaceAll("]", "]")
|
||
.replaceAll("{", "{")
|
||
.replaceAll("}", "}")
|
||
.replaceAll("|", "|")
|
||
.replaceAll("$", "$")
|
||
.replaceAll("£", "£")
|
||
.replaceAll("¢", "¢")
|
||
.replaceAll("₩", "₩")
|
||
.replaceAll("¥", "¥");
|
||
updateDisplayedText();
|
||
}
|
||
}, [updateDisplayedText]);
|
||
|
||
const convertPunctuation = useCallback(() => {
|
||
if (textAreaRef.current) {
|
||
textAreaRef.current.value = textAreaRef.current.value
|
||
.replaceAll("...", "⋯")
|
||
.replaceAll("…", "⋯")
|
||
.replaceAll(":::", "⋯⋯")
|
||
.replaceAll(".", "。")
|
||
.replaceAll(",", "、")
|
||
.replaceAll("?", "?")
|
||
.replaceAll("!", "!")
|
||
.replaceAll(":", ":")
|
||
.replaceAll("~", "~");
|
||
updateDisplayedText();
|
||
}
|
||
}, [updateDisplayedText]);
|
||
|
||
const toggleDakuten = useCallback(() => {
|
||
if (textAreaRef.current) {
|
||
const selectionStart = Math.min(
|
||
textAreaRef.current.selectionStart,
|
||
textAreaRef.current.selectionEnd
|
||
);
|
||
const selectionEnd = Math.max(
|
||
textAreaRef.current.selectionStart,
|
||
textAreaRef.current.selectionEnd
|
||
);
|
||
const selection = textAreaRef.current.value.substring(selectionStart, selectionEnd);
|
||
if (selection.length === 1) {
|
||
let newSelection = selection;
|
||
|
||
/*
|
||
* Hiragana
|
||
* a
|
||
*/
|
||
newSelection = swapChar(newSelection, ["か", "が"]);
|
||
newSelection = swapChar(newSelection, ["さ", "ざ"]);
|
||
newSelection = swapChar(newSelection, ["た", "だ"]);
|
||
newSelection = swapChar(newSelection, ["は", "ば", "ぱ"]);
|
||
// i
|
||
newSelection = swapChar(newSelection, ["き", "ぎ"]);
|
||
newSelection = swapChar(newSelection, ["し", "じ"]);
|
||
newSelection = swapChar(newSelection, ["ち", "ぢ"]);
|
||
newSelection = swapChar(newSelection, ["ひ", "び", "ぴ"]);
|
||
// u
|
||
newSelection = swapChar(newSelection, ["く", "ぐ"]);
|
||
newSelection = swapChar(newSelection, ["す", "ず"]);
|
||
newSelection = swapChar(newSelection, ["つ", "づ"]);
|
||
newSelection = swapChar(newSelection, ["ふ", "ぶ", "ぷ"]);
|
||
// e
|
||
newSelection = swapChar(newSelection, ["け", "げ"]);
|
||
newSelection = swapChar(newSelection, ["せ", "ぜ"]);
|
||
newSelection = swapChar(newSelection, ["て", "で"]);
|
||
newSelection = swapChar(newSelection, ["へ", "べ", "ぺ"]);
|
||
// o
|
||
newSelection = swapChar(newSelection, ["こ", "ご"]);
|
||
newSelection = swapChar(newSelection, ["そ", "ぞ"]);
|
||
newSelection = swapChar(newSelection, ["と", "ど"]);
|
||
newSelection = swapChar(newSelection, ["ほ", "ぼ", "ぽ"]);
|
||
// others
|
||
newSelection = swapChar(newSelection, ["う", "ゔ"]);
|
||
newSelection = swapChar(newSelection, ["ゝ", "ゞ"]);
|
||
|
||
/*
|
||
* Katakana
|
||
* a
|
||
*/
|
||
newSelection = swapChar(newSelection, ["カ", "ガ"]);
|
||
newSelection = swapChar(newSelection, ["サ", "ザ"]);
|
||
newSelection = swapChar(newSelection, ["タ", "ダ"]);
|
||
newSelection = swapChar(newSelection, ["ハ", "バ", "パ"]);
|
||
// i
|
||
newSelection = swapChar(newSelection, ["キ", "ギ"]);
|
||
newSelection = swapChar(newSelection, ["シ", "ジ"]);
|
||
newSelection = swapChar(newSelection, ["チ", "ヂ"]);
|
||
newSelection = swapChar(newSelection, ["ヒ", "ビ", "ピ"]);
|
||
// u
|
||
newSelection = swapChar(newSelection, ["ク", "グ"]);
|
||
newSelection = swapChar(newSelection, ["ス", "ズ"]);
|
||
newSelection = swapChar(newSelection, ["ツ", "ヅ"]);
|
||
newSelection = swapChar(newSelection, ["フ", "ブ", "プ"]);
|
||
// e
|
||
newSelection = swapChar(newSelection, ["ケ", "ゲ"]);
|
||
newSelection = swapChar(newSelection, ["セ", "ゼ"]);
|
||
newSelection = swapChar(newSelection, ["テ", "デ"]);
|
||
newSelection = swapChar(newSelection, ["ヘ", "ベ", "ペ"]);
|
||
// o
|
||
newSelection = swapChar(newSelection, ["コ", "ゴ"]);
|
||
newSelection = swapChar(newSelection, ["ソ", "ゾ"]);
|
||
newSelection = swapChar(newSelection, ["ト", "ド"]);
|
||
newSelection = swapChar(newSelection, ["ホ", "ボ", "ポ"]);
|
||
// others
|
||
newSelection = swapChar(newSelection, ["ゥ", "ヴ"]);
|
||
newSelection = swapChar(newSelection, ["ヽ", "ヾ"]);
|
||
|
||
if (newSelection !== selection) {
|
||
textAreaRef.current.value = replaceSelection(
|
||
textAreaRef.current.value,
|
||
selectionStart,
|
||
selectionEnd,
|
||
newSelection
|
||
);
|
||
|
||
textAreaRef.current.selectionStart = selectionStart;
|
||
textAreaRef.current.selectionEnd = selectionEnd;
|
||
textAreaRef.current.focus();
|
||
|
||
updateDisplayedText();
|
||
}
|
||
}
|
||
}
|
||
}, [updateDisplayedText]);
|
||
|
||
const toggleSmallForm = useCallback(() => {
|
||
if (textAreaRef.current) {
|
||
const selectionStart = Math.min(
|
||
textAreaRef.current.selectionStart,
|
||
textAreaRef.current.selectionEnd
|
||
);
|
||
const selectionEnd = Math.max(
|
||
textAreaRef.current.selectionStart,
|
||
textAreaRef.current.selectionEnd
|
||
);
|
||
const selection = textAreaRef.current.value.substring(selectionStart, selectionEnd);
|
||
if (selection.length === 1) {
|
||
let newSelection = selection;
|
||
|
||
// Hiragana
|
||
newSelection = swapChar(newSelection, ["あ", "ぁ"]);
|
||
newSelection = swapChar(newSelection, ["い", "ぃ"]);
|
||
newSelection = swapChar(newSelection, ["う", "ぅ"]);
|
||
newSelection = swapChar(newSelection, ["え", "ぇ"]);
|
||
newSelection = swapChar(newSelection, ["お", "ぉ"]);
|
||
newSelection = swapChar(newSelection, ["か", "ゕ"]);
|
||
newSelection = swapChar(newSelection, ["け", "ゖ"]);
|
||
newSelection = swapChar(newSelection, ["つ", "っ"]);
|
||
newSelection = swapChar(newSelection, ["や", "ゃ"]);
|
||
newSelection = swapChar(newSelection, ["ゆ", "ゅ"]);
|
||
newSelection = swapChar(newSelection, ["よ", "ょ"]);
|
||
newSelection = swapChar(newSelection, ["わ", "ゎ"]);
|
||
// Katakana
|
||
newSelection = swapChar(newSelection, ["ア", "ァ"]);
|
||
newSelection = swapChar(newSelection, ["イ", "ィ"]);
|
||
newSelection = swapChar(newSelection, ["ウ", "ゥ"]);
|
||
newSelection = swapChar(newSelection, ["エ", "ェ"]);
|
||
newSelection = swapChar(newSelection, ["オ", "ォ"]);
|
||
newSelection = swapChar(newSelection, ["ツ", "ッ"]);
|
||
newSelection = swapChar(newSelection, ["ヤ", "ャ"]);
|
||
newSelection = swapChar(newSelection, ["ユ", "ュ"]);
|
||
newSelection = swapChar(newSelection, ["ヨ", "ョ"]);
|
||
|
||
if (newSelection !== selection) {
|
||
textAreaRef.current.value = replaceSelection(
|
||
textAreaRef.current.value,
|
||
selectionStart,
|
||
selectionEnd,
|
||
newSelection
|
||
);
|
||
|
||
textAreaRef.current.selectionStart = selectionStart;
|
||
textAreaRef.current.selectionEnd = selectionEnd;
|
||
textAreaRef.current.focus();
|
||
|
||
updateDisplayedText();
|
||
}
|
||
}
|
||
}
|
||
}, [updateDisplayedText]);
|
||
|
||
const insert = useCallback(
|
||
(insertedText: string) => {
|
||
if (textAreaRef.current) {
|
||
const selectionEnd = Math.max(
|
||
textAreaRef.current.selectionStart,
|
||
textAreaRef.current.selectionEnd
|
||
);
|
||
textAreaRef.current.value = replaceSelection(
|
||
textAreaRef.current.value,
|
||
selectionEnd,
|
||
selectionEnd,
|
||
insertedText
|
||
);
|
||
|
||
textAreaRef.current.selectionStart = selectionEnd;
|
||
textAreaRef.current.selectionEnd = selectionEnd + insertedText.length;
|
||
textAreaRef.current.focus();
|
||
|
||
updateDisplayedText();
|
||
}
|
||
},
|
||
[updateDisplayedText]
|
||
);
|
||
|
||
const contentPanel = useMemo(
|
||
() => (
|
||
<ContentPanel width={ContentPanelWidthSizes.Full} className="overflow-hidden !pr-0 !pt-4">
|
||
<div className="grid grid-flow-col grid-cols-[1fr_5rem]">
|
||
<textarea
|
||
ref={textAreaRef}
|
||
onChange={updateDisplayedText}
|
||
onClick={updateLineIndex}
|
||
onKeyUp={updateLineIndex}
|
||
title="Input textarea"
|
||
className="whitespace-pre"
|
||
/>
|
||
|
||
<p
|
||
className="h-[80vh] whitespace-nowrap font-[initial] font-bold
|
||
[writing-mode:vertical-rl] [transform-origin:top_right]"
|
||
style={{
|
||
transform: `scale(${fontSize}) translateX(${fontSize * xOffset}px)`,
|
||
}}>
|
||
{text.split("\n")[lineIndex]}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap place-items-center gap-4 pr-24">
|
||
<div className="grid place-items-center">
|
||
<p>Text offset: {xOffset}px</p>
|
||
<input
|
||
title="Font size multiplier"
|
||
type="range"
|
||
min="0"
|
||
max="100"
|
||
value={xOffset * 10}
|
||
onChange={(event) => setXOffset(parseInt(event.target.value, 10) / 10)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid place-items-center">
|
||
<p>Font size: {fontSize}x</p>
|
||
<input
|
||
title="Font size multiplier"
|
||
type="range"
|
||
min="1000"
|
||
max="3000"
|
||
value={fontSize * SIZE_MULTIPLIER}
|
||
onChange={(event) => setFontSize(parseInt(event.target.value, 10) / SIZE_MULTIPLIER)}
|
||
/>
|
||
</div>
|
||
<ToolTip content="Automatically convert Western punctuations to Japanese ones.">
|
||
<Button text=". ⟹ 。" onClick={convertPunctuation} />
|
||
</ToolTip>
|
||
<ToolTip content="Swap a kana for one of its variant (different diacritics).">
|
||
<Button text="か ⟺ が" onClick={toggleDakuten} />
|
||
</ToolTip>
|
||
<ToolTip content="Toggle a kana's small form">
|
||
<Button text="つ ⟺ っ" onClick={toggleSmallForm} />
|
||
</ToolTip>
|
||
<ToolTip content="Convert standard characters to their full width variant.">
|
||
<Button text="123 ⟹ 123" onClick={convertFullWidth} />
|
||
</ToolTip>
|
||
|
||
<ToolTip
|
||
content={
|
||
<div className="grid gap-2">
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "「", onClick: () => insert("「") },
|
||
{ text: "」", onClick: () => insert("」") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "『", onClick: () => insert("『") },
|
||
{ text: "』", onClick: () => insert("』") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "【", onClick: () => insert("【") },
|
||
{ text: "】", onClick: () => insert("】") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "〖", onClick: () => insert("〖") },
|
||
{ text: "〗", onClick: () => insert("〗") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "〝", onClick: () => insert("〝") },
|
||
{ text: "〟", onClick: () => insert("〟") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "(", onClick: () => insert("(") },
|
||
{ text: ")", onClick: () => insert(")") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "⦅", onClick: () => insert("⦅") },
|
||
{ text: "⦆", onClick: () => insert("⦆") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "〈", onClick: () => insert("〈") },
|
||
{ text: "〉", onClick: () => insert("〉") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "《", onClick: () => insert("《") },
|
||
{ text: "》", onClick: () => insert("》") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "{", onClick: () => insert("{") },
|
||
{ text: "}", onClick: () => insert("}") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "[", onClick: () => insert("[") },
|
||
{ text: "]", onClick: () => insert("]") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "〔", onClick: () => insert("〔") },
|
||
{ text: "〕", onClick: () => insert("〕") },
|
||
]}
|
||
/>
|
||
<ButtonGroup
|
||
buttonsProps={[
|
||
{ text: "〘", onClick: () => insert("〘") },
|
||
{ text: "〙", onClick: () => insert("〙") },
|
||
]}
|
||
/>
|
||
</div>
|
||
}>
|
||
<Button text={"Quotations"} />
|
||
</ToolTip>
|
||
<ToolTip
|
||
content={
|
||
<div className="grid gap-2">
|
||
<Button text={"。"} onClick={() => insert("。")} />
|
||
<Button text={"?"} onClick={() => insert("?")} />
|
||
<Button text={"!"} onClick={() => insert("!")} />
|
||
<Button text={"⋯"} onClick={() => insert("⋯")} />
|
||
<Button text={"※"} onClick={() => insert("※")} />
|
||
<Button text={"♪"} onClick={() => insert("♪")} />
|
||
<Button text={"・"} onClick={() => insert("・")} />
|
||
<Button text={"〇"} onClick={() => insert("〇")} />
|
||
<Button text={'" "'} onClick={() => insert(" ")} />
|
||
</div>
|
||
}>
|
||
<Button text="Insert" />
|
||
</ToolTip>
|
||
</div>
|
||
</ContentPanel>
|
||
),
|
||
[
|
||
convertFullWidth,
|
||
convertPunctuation,
|
||
fontSize,
|
||
insert,
|
||
lineIndex,
|
||
text,
|
||
toggleDakuten,
|
||
toggleSmallForm,
|
||
updateDisplayedText,
|
||
updateLineIndex,
|
||
xOffset,
|
||
]
|
||
);
|
||
|
||
return <AppLayout contentPanel={contentPanel} {...props} contentPanelScroolbar={false} />;
|
||
};
|
||
export default Transcript;
|
||
|
||
/*
|
||
* ╭──────────────────────╮
|
||
* ───────────────────────────────────╯ NEXT DATA FETCHING ╰──────────────────────────────────────
|
||
*/
|
||
|
||
export const getStaticProps: GetStaticProps = (context) => {
|
||
const langui = getLangui(context.locale);
|
||
const props: Props = {
|
||
openGraph: getOpenGraph(langui, "Japanese Transcription Tool"),
|
||
};
|
||
return {
|
||
props: props,
|
||
};
|
||
};
|