From b9570e903e4a0fc27ef6a4dd2767b3bb193811e6 Mon Sep 17 00:00:00 2001 From: DrMint Date: Sat, 2 Jul 2022 04:34:21 +0200 Subject: [PATCH] Added transcript tool --- .eslintrc.js | 2 +- src/components/AppLayout.tsx | 7 +- src/components/Panels/ContentPanel.tsx | 10 +- src/pages/dev/transcript.tsx | 421 +++++++++++++++++++++++++ 4 files changed, 434 insertions(+), 6 deletions(-) create mode 100644 src/pages/dev/transcript.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 9775f97..ee57632 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -88,7 +88,7 @@ module.exports = { "no-new-wrappers": "warn", "no-octal-escape": "warn", "no-param-reassign": "warn", - "no-plusplus": "warn", + // "no-plusplus": "warn", "no-proto": "warn", "no-restricted-exports": "warn", "no-restricted-globals": "warn", diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 07cb352..40c7d35 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -35,6 +35,7 @@ interface Props extends AppStaticProps { navTitle: string | null | undefined; thumbnail?: UploadImageFragment; description?: string; + contentPanelScroolbar?: boolean; } const SENSIBILITY_SWIPE = 1.1; @@ -52,6 +53,7 @@ export function AppLayout(props: Props): JSX.Element { navTitle, description, subPanelIcon = Icon.Tune, + contentPanelScroolbar = true, } = props; const { @@ -280,7 +282,10 @@ export function AppLayout(props: Props): JSX.Element { {/* Content panel */}
{isDefined(contentPanel) ? ( contentPanel diff --git a/src/components/Panels/ContentPanel.tsx b/src/components/Panels/ContentPanel.tsx index c3fbe8e..9896224 100644 --- a/src/components/Panels/ContentPanel.tsx +++ b/src/components/Panels/ContentPanel.tsx @@ -3,6 +3,7 @@ import { cJoin } from "helpers/className"; interface Props { children: React.ReactNode; width?: ContentPanelWidthSizes; + className?: string; } export enum ContentPanelWidthSizes { @@ -12,18 +13,19 @@ export enum ContentPanelWidthSizes { } export function ContentPanel(props: Props): JSX.Element { - const { width = ContentPanelWidthSizes.Default, children } = props; + const { width = ContentPanelWidthSizes.Default, children, className } = props; return ( -
+
{children} diff --git a/src/pages/dev/transcript.tsx b/src/pages/dev/transcript.tsx new file mode 100644 index 0000000..a2a2d49 --- /dev/null +++ b/src/pages/dev/transcript.tsx @@ -0,0 +1,421 @@ +import { AppLayout } from "components/AppLayout"; +import { Icon } from "components/Ico"; +import { Button } from "components/Inputs/Button"; +import { ButtonGroup } from "components/Inputs/ButtonGroup"; +import { + ContentPanel, + ContentPanelWidthSizes, +} from "components/Panels/ContentPanel"; +import { ToolTip } from "components/ToolTip"; +import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; +import { GetStaticPropsContext } from "next"; +import { useCallback, useMemo, useRef, useState } from "react"; + +interface Props extends AppStaticProps {} + +const SIZE_MULTIPLIER = 1000; + +function replaceSelection( + text: string, + selectionStart: number, + selectionEnd: number, + newSelectedText: string +) { + return ( + text.substring(0, selectionStart) + + newSelectedText + + text.substring(selectionEnd) + ); +} + +function swapChar(char: string, swaps: string[]): string { + for (let index = 0; index < swaps.length; index++) { + if (char === swaps[index]) { + console.log( + "found it", + char, + " returning", + swaps[(index + 1) % swaps.length] + ); + return swaps[(index + 1) % swaps.length]; + } + } + return char; +} + +export default function 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(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 convertPunctuation = useCallback(() => { + if (textAreaRef.current) { + textAreaRef.current.value = textAreaRef.current.value + .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( + () => ( + +
+ + +

+ {text.split("\n")[lineIndex]} +

+
+ +
+
+

Text offset: {xOffset}px

+ + setXOffset(parseInt(event.target.value, 10) / 10) + } + > +
+ +
+

Font size: {fontSize}x

+ + setFontSize(parseInt(event.target.value, 10) / SIZE_MULTIPLIER) + } + > +
+ +
+ } + > +
+ + ), + [ + convertPunctuation, + fontSize, + insert, + lineIndex, + text, + toggleDakuten, + toggleSmallForm, + updateDisplayedText, + updateLineIndex, + xOffset, + ] + ); + + return ( + + ); +} + +export async function getStaticProps( + context: GetStaticPropsContext +): Promise<{ notFound: boolean } | { props: Props }> { + const props: Props = { + ...(await getAppStaticProps(context)), + }; + return { + props: props, + }; +}