From 11b413d71de16579c183483c5b52c938026048f0 Mon Sep 17 00:00:00 2001 From: Kotcrab <4594081+kotcrab@users.noreply.github.com> Date: Thu, 23 Mar 2023 20:09:46 +0100 Subject: [PATCH] Add popup position config --- CHANGES.md | 6 ++- components/JpdbPopupWrapper.tsx | 23 ++++++++++-- components/PopupPositionSelect.tsx | 17 +++++++++ components/SvgAnalysisFragment.tsx | 6 ++- components/SvgHighlight.tsx | 12 ++++-- components/SvgOverlay.tsx | 7 ++++ components/SvgParagraph.tsx | 4 +- model/AppSettings.ts | 5 ++- model/PopupPosition.ts | 6 +++ pages/read/[bookId]/[page].tsx | 9 +++++ pages/settings.tsx | 60 ++++++++++++++++++++++-------- pages/text-hooker.tsx | 14 +++++-- service/SettingsService.ts | 5 +++ util/OverlayUtil.ts | 2 +- util/SvgOverlayContext.ts | 3 ++ 15 files changed, 147 insertions(+), 32 deletions(-) create mode 100644 components/PopupPositionSelect.tsx create mode 100644 model/PopupPosition.ts diff --git a/CHANGES.md b/CHANGES.md index ae7c3dc..728bf8f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ +#### Version 1.3.2 +- JPDB popup placement can now be configured for horizontal and vertical text. +- Reorganized settings page. + #### Version 1.3.1 -- Fixed bug where missing frequency rank is not handled in the popup +- Fixed bug where missing frequency rank is not handled in the popup. #### Version 1.3 - JPDB integration was rewritten to use the new JPDB API. diff --git a/components/JpdbPopupWrapper.tsx b/components/JpdbPopupWrapper.tsx index b21f515..4c62fb3 100644 --- a/components/JpdbPopupWrapper.tsx +++ b/components/JpdbPopupWrapper.tsx @@ -5,27 +5,28 @@ import {Portal, usePopper} from "@chakra-ui/react" import {JpdbPopupDialog} from "./JpdbPopupDialog" import {JpdbRule} from "../model/JpdbRule" import {JpdbPopup} from "../model/JpdbPopup" +import {PopupPosition} from "../model/PopupPosition" interface Props { - placement: "right" | "bottom", rule: JpdbRule, vocabulary: JpdbVocabulary | undefined, miningDeckId: number, + position: PopupPosition, mouseOverReference: boolean, wrapper: (reference: React.Ref) => JSX.Element, } export default function JpdbPopupWrapper( { - wrapper, rule, vocabulary, miningDeckId, + position, mouseOverReference, - placement, + wrapper, }: Props ) { - const {popperRef, referenceRef} = usePopper({placement: placement}) + const {popperRef, referenceRef} = usePopper({placement: positionToPlacement(position)}) const [mouseOverPopup, setMouseOverPopup] = useState(false) @@ -46,3 +47,17 @@ export default function JpdbPopupWrapper( } } + +function positionToPlacement(position: PopupPosition) { + switch (position) { + case PopupPosition.BelowText: + return "bottom" + case PopupPosition.AboveText: + return "top" + case PopupPosition.LeftOfText: + return "left" + case PopupPosition.RightOfText: + return "right" + } + throw new Error("Unhandled popup position") +} diff --git a/components/PopupPositionSelect.tsx b/components/PopupPositionSelect.tsx new file mode 100644 index 0000000..ac9862e --- /dev/null +++ b/components/PopupPositionSelect.tsx @@ -0,0 +1,17 @@ +import {Select} from "@chakra-ui/react" +import * as React from "react" +import {PopupPosition} from "../model/PopupPosition" + +interface Props { + value: PopupPosition, + onChange: (popupPosition: PopupPosition) => void, +} + +export default function PopupPositionSelect({value, onChange}: Props) { + return +} diff --git a/components/SvgAnalysisFragment.tsx b/components/SvgAnalysisFragment.tsx index 4d35960..296cf70 100644 --- a/components/SvgAnalysisFragment.tsx +++ b/components/SvgAnalysisFragment.tsx @@ -1,4 +1,4 @@ -import {effectiveTextOrientation, scaleRectangle} from "../util/OverlayUtil" +import {getEffectiveTextOrientation, scaleRectangle} from "../util/OverlayUtil" import * as React from "react" import {useContext, useState} from "react" import {ImageAnalysisFragment} from "../model/ImageAnalysis" @@ -22,6 +22,7 @@ export default function SvgAnalysisFragment({fragment, rule, vocabulary}: Props) const debouncedMouseOver = useDebounce(mouseOver, 40) const bounds = scaleRectangle(fragment.bounds, scaleX, scaleY) + const effectiveTextOrientation = getEffectiveTextOrientation(textOrientation, fragment.orientation) return setMouseOver(true)} @@ -31,13 +32,14 @@ export default function SvgAnalysisFragment({fragment, rule, vocabulary}: Props) bounds={bounds} rule={rule} vocabulary={vocabulary} + textOrientation={effectiveTextOrientation} mouseOverGroup={debouncedMouseOver} />} {fragment.symbols.map((symbol, symbolIndex) => )} diff --git a/components/SvgHighlight.tsx b/components/SvgHighlight.tsx index 55033c3..3e8abb0 100644 --- a/components/SvgHighlight.tsx +++ b/components/SvgHighlight.tsx @@ -6,22 +6,28 @@ import {JpdbRule} from "../model/JpdbRule" import {SvgOverlayContext} from "../util/SvgOverlayContext" import {rgba} from "color2k" import JpdbPopupWrapper from "./JpdbPopupWrapper" +import {TextOrientation} from "../model/TextOrientation" interface Props { bounds: Rectangle, rule: JpdbRule, vocabulary: JpdbVocabulary | undefined, + textOrientation: TextOrientation, mouseOverGroup: boolean, } -export default function SvgHighlight({bounds, rule, vocabulary, mouseOverGroup}: Props) { - const {jpdbMiningDeckId} = useContext(SvgOverlayContext) +export default function SvgHighlight({bounds, rule, vocabulary, textOrientation, mouseOverGroup}: Props) { + const { + jpdbMiningDeckId, + jpdbHorizontalTextPopupPosition, + jpdbVerticalTextPopupPosition, + } = useContext(SvgOverlayContext) return }) )} diff --git a/model/AppSettings.ts b/model/AppSettings.ts index 47088f7..2b6eed8 100644 --- a/model/AppSettings.ts +++ b/model/AppSettings.ts @@ -1,10 +1,13 @@ import {jpdbRuleSchema} from "./JpdbRule" -import {array, boolean, InferType, number, object, string} from "yup" +import {array, boolean, InferType, mixed, number, object, string} from "yup" +import {PopupPosition} from "./PopupPosition" export const appSettingsSchema = object({ readingTimerEnabled: boolean().required(), jpdbApiKey: string().required(), jpdbMiningDeckId: number().integer().required(), + jpdbHorizontalTextPopupPosition: mixed().oneOf(Object.values(PopupPosition)).required(), + jpdbVerticalTextPopupPosition: mixed().oneOf(Object.values(PopupPosition)).required(), jpdbRules: array().of(jpdbRuleSchema).ensure().required(), textHookerWebSocketUrl: string().required(), }) diff --git a/model/PopupPosition.ts b/model/PopupPosition.ts new file mode 100644 index 0000000..988e295 --- /dev/null +++ b/model/PopupPosition.ts @@ -0,0 +1,6 @@ +export enum PopupPosition { + BelowText = "BelowText", + AboveText = "AboveText", + LeftOfText = "LeftOfText", + RightOfText = "RightOfText", +} diff --git a/pages/read/[bookId]/[page].tsx b/pages/read/[bookId]/[page].tsx index 4b37ed2..6f13b82 100644 --- a/pages/read/[bookId]/[page].tsx +++ b/pages/read/[bookId]/[page].tsx @@ -22,6 +22,7 @@ import {Api} from "../../../util/Api" import {Dimensions} from "../../../model/Dimensions" import {ImageAnalysis} from "../../../model/ImageAnalysis" import {JpdbRule} from "../../../model/JpdbRule" +import {PopupPosition} from "../../../model/PopupPosition" interface Props { title: string, @@ -31,6 +32,8 @@ interface Props { jpdbRules: readonly JpdbRule[], jpdbEnabled: boolean, jpdbMiningDeckId: number, + jpdbHorizontalTextPopupPosition: PopupPosition, + jpdbVerticalTextPopupPosition: PopupPosition, readingTimerEnabled: boolean, readerSettings: ReaderSettings, } @@ -52,6 +55,8 @@ export default function ReadBookPage( jpdbRules, jpdbEnabled, jpdbMiningDeckId, + jpdbHorizontalTextPopupPosition, + jpdbVerticalTextPopupPosition, readingTimerEnabled, readerSettings, }: Props @@ -204,6 +209,8 @@ export default function ReadBookPage( textOrientation={textOrientation} minimumConfidence={minimumConfidence} jpdbMiningDeckId={jpdbMiningDeckId} + jpdbHorizontalTextPopupPosition={jpdbHorizontalTextPopupPosition} + jpdbVerticalTextPopupPosition={jpdbVerticalTextPopupPosition} /> @@ -229,6 +236,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { jpdbEnabled: await services.jpdbService.isEnabled(), jpdbRules: appSettings.jpdbRules, jpdbMiningDeckId: appSettings.jpdbMiningDeckId, + jpdbHorizontalTextPopupPosition: appSettings.jpdbHorizontalTextPopupPosition, + jpdbVerticalTextPopupPosition: appSettings.jpdbVerticalTextPopupPosition, readingTimerEnabled: appSettings.readingTimerEnabled, readerSettings: readerSettings, }, diff --git a/pages/settings.tsx b/pages/settings.tsx index 44b2938..63e4a04 100644 --- a/pages/settings.tsx +++ b/pages/settings.tsx @@ -22,6 +22,7 @@ import {isValidWebSocketUrl} from "../util/Url" import RestoreDefaultValueButton from "../components/RestoreDefaultValueButton" import {Api} from "../util/Api" import JpdbRulesEditModal from "../components/JpdbRulesEditModal" +import PopupPositionSelect from "../components/PopupPositionSelect" interface Props { appSettings: AppSettings, @@ -32,10 +33,12 @@ export default function Settings({appSettings, defaultAppSettings}: Props) { const toast = useToast() const [readingTimerEnabled, setReadingTimerEnabled] = useState(appSettings.readingTimerEnabled) + const [textHookerWebSocketUrl, setTextHookerWebSocketUrl] = useState(appSettings.textHookerWebSocketUrl) const [jpdbApiKey, setJpdbApiKey] = useState(appSettings.jpdbApiKey) const [jpdbMiningDeckId, setJpdbMiningDeckId] = useState(appSettings.jpdbMiningDeckId) const [jpdbRules, setJpdbRules] = useState(appSettings.jpdbRules) - const [textHookerWebSocketUrl, setTextHookerWebSocketUrl] = useState(appSettings.textHookerWebSocketUrl) + const [jpdbHorizontalTextPopupPosition, setJpdbHorizontalTextPopupPosition] = useState(appSettings.jpdbHorizontalTextPopupPosition) + const [jpdbVerticalTextPopupPosition, setJpdbVerticalTextPopupPosition] = useState(appSettings.jpdbVerticalTextPopupPosition) const [jpdbRulesEditPending, setJpdbRulesEditPending] = useState(false) @@ -50,10 +53,12 @@ export default function Settings({appSettings, defaultAppSettings}: Props) { } await Api.updateAppSettings({ readingTimerEnabled: readingTimerEnabled, + textHookerWebSocketUrl: textHookerWebSocketUrl, jpdbApiKey: jpdbApiKey, jpdbMiningDeckId: jpdbMiningDeckId, jpdbRules: jpdbRules, - textHookerWebSocketUrl: textHookerWebSocketUrl, + jpdbHorizontalTextPopupPosition: jpdbHorizontalTextPopupPosition, + jpdbVerticalTextPopupPosition: jpdbVerticalTextPopupPosition, }) toast({ description: "Settings saved", @@ -81,6 +86,7 @@ export default function Settings({appSettings, defaultAppSettings}: Props) { Settings + General - Integrations + + Text hooker + + Text hooker WebSocket URL + + setTextHookerWebSocketUrl(event.target.value)}/> + setTextHookerWebSocketUrl(defaultAppSettings.textHookerWebSocketUrl)}/> + + WebSocket URL used for the text hooker page. + + + JPDB integration - JPDB API key + API key setJpdbApiKey(event.target.value)}/> setJpdbApiKey(defaultAppSettings.jpdbApiKey)}/> @@ -98,30 +117,41 @@ export default function Settings({appSettings, defaultAppSettings}: Props) { JPDB API key used for text parsing and words highlighting. - JPDB mining deck ID + Mining deck ID setJpdbMiningDeckId(parseInt(event.target.value))}/> setJpdbMiningDeckId(defaultAppSettings.jpdbMiningDeckId)}/> - Mined words will be added to JPDB deck with this ID number. + Mined words will be added to this JPDB deck. - JPDB rules - - Configure colors and which words should be highlighted. + Horizontal text popup position + + + setJpdbHorizontalTextPopupPosition(defaultAppSettings.jpdbHorizontalTextPopupPosition)}/> + - Text hooker WebSocket URL + Vertical text popup position - setTextHookerWebSocketUrl(event.target.value)}/> + setTextHookerWebSocketUrl(defaultAppSettings.textHookerWebSocketUrl)}/> + onClick={() => setJpdbVerticalTextPopupPosition(defaultAppSettings.jpdbVerticalTextPopupPosition)}/> - WebSocket URL used for the text hooker page. - + + Rules + + Configure colors and which words should be highlighted. + + + diff --git a/pages/text-hooker.tsx b/pages/text-hooker.tsx index c7bd449..9e82dc7 100644 --- a/pages/text-hooker.tsx +++ b/pages/text-hooker.tsx @@ -13,11 +13,13 @@ import {JpdbRule} from "../model/JpdbRule" import {JpdbVocabulary} from "../model/JpdbVocabulary" import JpdbPopupWrapper from "../components/JpdbPopupWrapper" import useDebounce from "../util/Debounce" +import {PopupPosition} from "../model/PopupPosition" interface Props { jpdbEnabled: boolean, jpdbRules: readonly JpdbRule[], jpdbMiningDeckId: number, + jpdbPopupPosition: PopupPosition, readingTimerEnabled: boolean, textHookerWebSocketUrl: string, } @@ -27,6 +29,7 @@ export default function TextHooker( jpdbEnabled, jpdbRules, jpdbMiningDeckId, + jpdbPopupPosition, readingTimerEnabled, textHookerWebSocketUrl, }: Props @@ -120,6 +123,7 @@ export default function TextHooker( analyze={analyze} jpdbRules={jpdbRules} jpdbMiningDeckId={jpdbMiningDeckId} + jpdbPopupPosition={jpdbPopupPosition} /> ))} @@ -135,9 +139,10 @@ interface AnalyzedTextProps { analyze: boolean, jpdbRules: readonly JpdbRule[], jpdbMiningDeckId: number, + jpdbPopupPosition: PopupPosition, } -function AnalyzedText({text, analyze, jpdbRules, jpdbMiningDeckId}: AnalyzedTextProps) { +function AnalyzedText({text, analyze, jpdbRules, jpdbMiningDeckId, jpdbPopupPosition}: AnalyzedTextProps) { const [wantsAnalyze, _] = useState(analyze) const [analysis, setAnalysis] = useState(undefined) @@ -166,6 +171,7 @@ function AnalyzedText({text, analyze, jpdbRules, jpdbMiningDeckId}: AnalyzedText rule={rule} vocabulary={vocabulary} miningDeckId={jpdbMiningDeckId} + popupPosition={jpdbPopupPosition} /> }) : text @@ -178,9 +184,10 @@ interface TextTokenProps { rule: JpdbRule | undefined, vocabulary: JpdbVocabulary | undefined, miningDeckId: number, + popupPosition: PopupPosition, } -function TextToken({text, rule, vocabulary, miningDeckId}: TextTokenProps) { +function TextToken({text, rule, vocabulary, miningDeckId, popupPosition}: TextTokenProps) { const [mouseOver, setMouseOver] = useState(false) const debouncedMouseOver = useDebounce(mouseOver, 40) @@ -192,8 +199,8 @@ function TextToken({text, rule, vocabulary, miningDeckId}: TextTokenProps) { rule={rule} vocabulary={vocabulary} miningDeckId={miningDeckId} + position={popupPosition} mouseOverReference={debouncedMouseOver} - placement="bottom" wrapper={(ref) =>