Skip to content

Commit

Permalink
Add popup position config
Browse files Browse the repository at this point in the history
  • Loading branch information
kotcrab committed Mar 23, 2023
1 parent e2e86cf commit 11b413d
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 32 deletions.
6 changes: 5 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
23 changes: 19 additions & 4 deletions components/JpdbPopupWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>) => 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)

Expand All @@ -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")
}
17 changes: 17 additions & 0 deletions components/PopupPositionSelect.tsx
Original file line number Diff line number Diff line change
@@ -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 <Select value={value} onChange={event => onChange(event.target.value as PopupPosition)}>
<option value={PopupPosition.BelowText}>Below text</option>
<option value={PopupPosition.AboveText}>Above text</option>
<option value={PopupPosition.LeftOfText}>Left of text</option>
<option value={PopupPosition.RightOfText}>Right of text</option>
</Select>
}
6 changes: 4 additions & 2 deletions components/SvgAnalysisFragment.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 <g
onMouseEnter={() => setMouseOver(true)}
Expand All @@ -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) =>
<SvgSymbol
key={symbolIndex}
packedSymbol={symbol}
textOrientation={effectiveTextOrientation(textOrientation, fragment.orientation)}
textOrientation={effectiveTextOrientation}
/>
)}
</g>
Expand Down
12 changes: 9 additions & 3 deletions components/SvgHighlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <JpdbPopupWrapper
placement="right"
rule={rule}
vocabulary={vocabulary}
miningDeckId={jpdbMiningDeckId}
position={textOrientation == TextOrientation.Horizontal ? jpdbHorizontalTextPopupPosition : jpdbVerticalTextPopupPosition}
mouseOverReference={mouseOverGroup}
wrapper={(ref) => <rect
x={bounds.x}
Expand Down
7 changes: 7 additions & 0 deletions components/SvgOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {ImageAnalysis} from "../model/ImageAnalysis"
import {SvgOverlayContext} from "../util/SvgOverlayContext"
import SvgParagraph from "./SvgParagraph"
import {JpdbRule} from "../model/JpdbRule"
import {PopupPosition} from "../model/PopupPosition"

interface Props {
ocr: OcrPage,
Expand All @@ -24,6 +25,8 @@ interface Props {
textOrientation: TextOrientationSetting,
minimumConfidence: number,
jpdbMiningDeckId: number,
jpdbHorizontalTextPopupPosition: PopupPosition,
jpdbVerticalTextPopupPosition: PopupPosition,
}

export default function SvgOverlay(
Expand All @@ -40,6 +43,8 @@ export default function SvgOverlay(
textOrientation,
minimumConfidence,
jpdbMiningDeckId,
jpdbHorizontalTextPopupPosition,
jpdbVerticalTextPopupPosition,
}: Props
) {
const sizeDiv = 1000
Expand All @@ -66,6 +71,8 @@ export default function SvgOverlay(
textOrientation: textOrientation,
chromiumBased: chromiumBased,
jpdbMiningDeckId: jpdbMiningDeckId,
jpdbHorizontalTextPopupPosition: jpdbHorizontalTextPopupPosition,
jpdbVerticalTextPopupPosition: jpdbVerticalTextPopupPosition,
}}>
<svg width="100%"
height="100%"
Expand Down
4 changes: 2 additions & 2 deletions components/SvgParagraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {useContext} from "react"
import {OcrLine} from "../model/OcrPage"
import SvgSymbol from "./SvgSymbol"
import {SvgOverlayContext} from "../util/SvgOverlayContext"
import {effectiveTextOrientation} from "../util/OverlayUtil"
import {getEffectiveTextOrientation} from "../util/OverlayUtil"

interface Props {
lines: readonly OcrLine[],
Expand All @@ -18,7 +18,7 @@ export default function SvgParagraph({lines}: Props) {
return <SvgSymbol
key={`${lineIndex}-${index}`}
packedSymbol={packedSymbol}
textOrientation={effectiveTextOrientation(textOrientation, line.orientation)}
textOrientation={getEffectiveTextOrientation(textOrientation, line.orientation)}
/>
})
)}</>
Expand Down
5 changes: 4 additions & 1 deletion model/AppSettings.ts
Original file line number Diff line number Diff line change
@@ -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<PopupPosition>().oneOf(Object.values(PopupPosition)).required(),
jpdbVerticalTextPopupPosition: mixed<PopupPosition>().oneOf(Object.values(PopupPosition)).required(),
jpdbRules: array().of(jpdbRuleSchema).ensure().required(),
textHookerWebSocketUrl: string().required(),
})
Expand Down
6 changes: 6 additions & 0 deletions model/PopupPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum PopupPosition {
BelowText = "BelowText",
AboveText = "AboveText",
LeftOfText = "LeftOfText",
RightOfText = "RightOfText",
}
9 changes: 9 additions & 0 deletions pages/read/[bookId]/[page].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -31,6 +32,8 @@ interface Props {
jpdbRules: readonly JpdbRule[],
jpdbEnabled: boolean,
jpdbMiningDeckId: number,
jpdbHorizontalTextPopupPosition: PopupPosition,
jpdbVerticalTextPopupPosition: PopupPosition,
readingTimerEnabled: boolean,
readerSettings: ReaderSettings,
}
Expand All @@ -52,6 +55,8 @@ export default function ReadBookPage(
jpdbRules,
jpdbEnabled,
jpdbMiningDeckId,
jpdbHorizontalTextPopupPosition,
jpdbVerticalTextPopupPosition,
readingTimerEnabled,
readerSettings,
}: Props
Expand Down Expand Up @@ -204,6 +209,8 @@ export default function ReadBookPage(
textOrientation={textOrientation}
minimumConfidence={minimumConfidence}
jpdbMiningDeckId={jpdbMiningDeckId}
jpdbHorizontalTextPopupPosition={jpdbHorizontalTextPopupPosition}
jpdbVerticalTextPopupPosition={jpdbVerticalTextPopupPosition}
/>
</div>
</Flex>
Expand All @@ -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,
},
Expand Down
60 changes: 45 additions & 15 deletions pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)

Expand All @@ -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",
Expand Down Expand Up @@ -81,47 +86,72 @@ export default function Settings({appSettings, defaultAppSettings}: Props) {
<Container maxW="xl">
<VStack alignItems="center" spacing="8">
<Text fontSize="2xl">Settings</Text>

<Text fontSize="xl">General</Text>
<VStack alignSelf="start">
<Checkbox isChecked={readingTimerEnabled}
onChange={event => setReadingTimerEnabled(event.target.checked)}>
Enable reading timer
</Checkbox>
</VStack>
<Text fontSize="xl">Integrations</Text>

<Text fontSize="xl">Text hooker</Text>
<FormControl>
<FormLabel>Text hooker WebSocket URL</FormLabel>
<HStack>
<Input value={textHookerWebSocketUrl}
onChange={event => setTextHookerWebSocketUrl(event.target.value)}/>
<RestoreDefaultValueButton
onClick={() => setTextHookerWebSocketUrl(defaultAppSettings.textHookerWebSocketUrl)}/>
</HStack>
<FormHelperText>WebSocket URL used for the text hooker page.</FormHelperText>
</FormControl>

<Text fontSize="xl">JPDB integration</Text>
<FormControl>
<FormLabel>JPDB API key</FormLabel>
<FormLabel>API key</FormLabel>
<HStack>
<Input type="password" value={jpdbApiKey} onChange={event => setJpdbApiKey(event.target.value)}/>
<RestoreDefaultValueButton onClick={() => setJpdbApiKey(defaultAppSettings.jpdbApiKey)}/>
</HStack>
<FormHelperText>JPDB API key used for text parsing and words highlighting.</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>JPDB mining deck ID</FormLabel>
<FormLabel>Mining deck ID</FormLabel>
<HStack>
<Input type="number" value={jpdbMiningDeckId}
onChange={event => setJpdbMiningDeckId(parseInt(event.target.value))}/>
<RestoreDefaultValueButton onClick={() => setJpdbMiningDeckId(defaultAppSettings.jpdbMiningDeckId)}/>
</HStack>
<FormHelperText>Mined words will be added to JPDB deck with this ID number.</FormHelperText>
<FormHelperText>Mined words will be added to this JPDB deck.</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>JPDB rules</FormLabel>
<Button onClick={() => setJpdbRulesEditPending(true)}>Edit</Button>
<FormHelperText>Configure colors and which words should be highlighted.</FormHelperText>
<FormLabel>Horizontal text popup position</FormLabel>
<HStack>
<PopupPositionSelect
value={jpdbHorizontalTextPopupPosition}
onChange={setJpdbHorizontalTextPopupPosition}/>
<RestoreDefaultValueButton
onClick={() => setJpdbHorizontalTextPopupPosition(defaultAppSettings.jpdbHorizontalTextPopupPosition)}/>
</HStack>
</FormControl>
<FormControl>
<FormLabel>Text hooker WebSocket URL</FormLabel>
<FormLabel>Vertical text popup position</FormLabel>
<HStack>
<Input value={textHookerWebSocketUrl}
onChange={event => setTextHookerWebSocketUrl(event.target.value)}/>
<PopupPositionSelect
value={jpdbVerticalTextPopupPosition}
onChange={setJpdbVerticalTextPopupPosition}/>
<RestoreDefaultValueButton
onClick={() => setTextHookerWebSocketUrl(defaultAppSettings.textHookerWebSocketUrl)}/>
onClick={() => setJpdbVerticalTextPopupPosition(defaultAppSettings.jpdbVerticalTextPopupPosition)}/>
</HStack>
<FormHelperText>WebSocket URL used for the text hooker page.</FormHelperText>
</FormControl>
<Box pt={4}>
<FormControl>
<FormLabel>Rules</FormLabel>
<Button onClick={() => setJpdbRulesEditPending(true)}>Edit</Button>
<FormHelperText>Configure colors and which words should be highlighted.</FormHelperText>
</FormControl>

<Box pt={8}>
<Button variant="solid" colorScheme="blue" onClick={saveSettings}>
Save settings
</Button>
Expand Down
Loading

0 comments on commit 11b413d

Please sign in to comment.