revert(know-your-world): undo premature extractions, restore working state
Reverts previous extraction attempts that went down a problematic path: - Remove DebugAutoZoomPanel component - Remove OtherPlayerCursors component - Remove useUserPreferences hook - Restore inline implementations in MapRenderer MapRenderer is at 3912 lines. Will proceed with a more careful extraction strategy based on updated deep dive analysis. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
022ee0256a
commit
f0bf2050d3
File diff suppressed because it is too large
Load Diff
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
**Last Updated:** December 2024
|
**Last Updated:** December 2024
|
||||||
|
|
||||||
## Current State: 3593 lines (was 4679)
|
## Current State: 3912 lines (was 4679)
|
||||||
|
|
||||||
### Progress So Far
|
### Progress So Far
|
||||||
- ✅ CompassCrosshair extracted (~80 lines)
|
- ✅ CompassCrosshair extracted (~80 lines)
|
||||||
|
|
@ -12,11 +12,8 @@
|
||||||
- ✅ CursorOverlay extracted (~104 lines)
|
- ✅ CursorOverlay extracted (~104 lines)
|
||||||
- ✅ hotColdStyles utilities extracted (~234 lines)
|
- ✅ hotColdStyles utilities extracted (~234 lines)
|
||||||
- ✅ RegionLayer extracted (~119 lines)
|
- ✅ RegionLayer extracted (~119 lines)
|
||||||
- ✅ OtherPlayerCursors extracted (~148 lines) - `features/multiplayer/`
|
|
||||||
- ✅ DebugAutoZoomPanel extracted (~128 lines) - `features/debug/`
|
|
||||||
- ✅ useUserPreferences hook extracted (~43 lines) - `features/user-preferences/`
|
|
||||||
|
|
||||||
**Total reduction: 1086 lines (23.2%)**
|
**Total reduction: 767 lines (16.4%)**
|
||||||
|
|
||||||
### Hook/State Explosion
|
### Hook/State Explosion
|
||||||
|
|
||||||
|
|
@ -348,11 +345,11 @@ Lines 3278-3548. The magnifier is already using extracted components internally
|
||||||
|
|
||||||
### Recommended Priority
|
### Recommended Priority
|
||||||
|
|
||||||
1. ~~**OtherPlayerCursors** - 5 min, -145 lines~~ ✅ DONE (-148 lines)
|
1. **OtherPlayerCursors** - 5 min, -145 lines
|
||||||
2. ~~**DebugAutoZoomPanel** - 10 min, -123 lines~~ ✅ DONE (-128 lines)
|
2. **DebugAutoZoomPanel** - 10 min, -123 lines
|
||||||
3. ~~**useUserPreferences** - 15 min, -90 lines~~ ✅ DONE (-43 lines)
|
3. **useUserPreferences** - 15 min, -90 lines
|
||||||
4. **useCelebration** - SKIPPED (too intertwined with click handling, puzzle piece animation, assistance levels)
|
4. **useCelebration** - 20 min, -130 lines
|
||||||
5. **useMapInteraction** - 2 hours, -600 lines (biggest impact but most complex) - FUTURE WORK
|
5. **useMapInteraction** - 2 hours, -600 lines (biggest impact but most complex)
|
||||||
|
|
||||||
### Target Architecture
|
### Target Architecture
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
/**
|
|
||||||
* DebugAutoZoomPanel Component
|
|
||||||
*
|
|
||||||
* Debug visualization for the auto-zoom detection system.
|
|
||||||
* Shows detected regions, zoom decisions, and threshold information.
|
|
||||||
* Only visible when debug mode is enabled.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { memo } from 'react'
|
|
||||||
import { useMapRendererContext } from '../map-renderer'
|
|
||||||
import type { findOptimalZoom } from '../../utils/adaptiveZoomSearch'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface DetectedRegion {
|
|
||||||
id: string
|
|
||||||
pixelWidth: number
|
|
||||||
pixelHeight: number
|
|
||||||
isVerySmall: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DetectionResult {
|
|
||||||
detectedRegions: DetectedRegion[]
|
|
||||||
hasSmallRegion: boolean
|
|
||||||
detectedSmallestSize: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DebugAutoZoomPanelProps {
|
|
||||||
/** Whether panel is visible */
|
|
||||||
visible: boolean
|
|
||||||
/** Magnifier left position (for positioning panel on opposite side) */
|
|
||||||
magnifierLeft: number
|
|
||||||
/** Function to detect regions at cursor position */
|
|
||||||
detectRegions: (x: number, y: number) => DetectionResult
|
|
||||||
/** Function to get current zoom level */
|
|
||||||
getCurrentZoom: () => number
|
|
||||||
/** Target zoom level */
|
|
||||||
targetZoom: number
|
|
||||||
/** Debug info from zoom search algorithm */
|
|
||||||
zoomSearchDebugInfo: ReturnType<typeof findOptimalZoom> | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Component
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders debug visualization for auto-zoom detection.
|
|
||||||
* Shows a detection box around cursor and detailed information panel.
|
|
||||||
*/
|
|
||||||
export const DebugAutoZoomPanel = memo(function DebugAutoZoomPanel({
|
|
||||||
visible,
|
|
||||||
magnifierLeft,
|
|
||||||
detectRegions,
|
|
||||||
getCurrentZoom,
|
|
||||||
targetZoom,
|
|
||||||
zoomSearchDebugInfo,
|
|
||||||
}: DebugAutoZoomPanelProps) {
|
|
||||||
// Get shared state from context
|
|
||||||
const { cursorPosition, containerRef } = useMapRendererContext()
|
|
||||||
|
|
||||||
if (!visible || !cursorPosition || !containerRef.current) return null
|
|
||||||
|
|
||||||
const { detectedRegions, hasSmallRegion, detectedSmallestSize } = detectRegions(
|
|
||||||
cursorPosition.x,
|
|
||||||
cursorPosition.y
|
|
||||||
)
|
|
||||||
|
|
||||||
// Position on opposite side from magnifier
|
|
||||||
const containerWidth = containerRef.current.getBoundingClientRect().width
|
|
||||||
const magnifierOnLeft = magnifierLeft < containerWidth / 2
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Detection box - 50px box around cursor */}
|
|
||||||
<div
|
|
||||||
data-element="debug-detection-box"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: `${cursorPosition.x - 25}px`,
|
|
||||||
top: `${cursorPosition.y - 25}px`,
|
|
||||||
width: '50px',
|
|
||||||
height: '50px',
|
|
||||||
border: '2px dashed yellow',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: 150,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Detection info overlay - opposite side from magnifier */}
|
|
||||||
<div
|
|
||||||
data-element="debug-auto-zoom-panel"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: '10px',
|
|
||||||
left: magnifierOnLeft ? undefined : '10px',
|
|
||||||
right: magnifierOnLeft ? '10px' : undefined,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
||||||
color: 'white',
|
|
||||||
padding: '10px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '12px',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: 150,
|
|
||||||
maxWidth: '300px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<strong>Detection Box (50px)</strong>
|
|
||||||
</div>
|
|
||||||
<div>Regions detected: {detectedRegions.length}</div>
|
|
||||||
<div>Has small region: {hasSmallRegion ? 'YES' : 'NO'}</div>
|
|
||||||
<div>
|
|
||||||
Smallest size:{' '}
|
|
||||||
{detectedSmallestSize === Infinity ? '∞' : `${detectedSmallestSize.toFixed(1)}px`}
|
|
||||||
</div>
|
|
||||||
{/* Zoom Decision Details */}
|
|
||||||
{zoomSearchDebugInfo && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: '8px',
|
|
||||||
paddingTop: '8px',
|
|
||||||
borderTop: '1px solid #444',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>Zoom Decision:</strong>
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '10px', marginLeft: '8px' }}>
|
|
||||||
Final zoom: <strong>{zoomSearchDebugInfo.zoom.toFixed(1)}×</strong>
|
|
||||||
{!zoomSearchDebugInfo.foundGoodZoom && ' (fallback to min)'}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '10px', marginLeft: '8px' }}>
|
|
||||||
Accepted: <strong>{zoomSearchDebugInfo.acceptedRegionId || 'none'}</strong>
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '10px', marginLeft: '8px' }}>
|
|
||||||
Thresholds: {(zoomSearchDebugInfo.acceptanceThresholds.min * 100).toFixed(0)}% -{' '}
|
|
||||||
{(zoomSearchDebugInfo.acceptanceThresholds.max * 100).toFixed(0)}% of magnifier
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '8px' }}>
|
|
||||||
<strong>Region Analysis (top 3):</strong>
|
|
||||||
</div>
|
|
||||||
{Array.from(
|
|
||||||
new Map(zoomSearchDebugInfo.regionDecisions.map((d) => [d.regionId, d])).values()
|
|
||||||
)
|
|
||||||
.sort((a, b) => b.importance - a.importance)
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((decision) => {
|
|
||||||
const marker = decision.wasAccepted ? '✓' : '✗'
|
|
||||||
const color = decision.wasAccepted ? '#0f0' : '#888'
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`decision-${decision.regionId}`}
|
|
||||||
style={{
|
|
||||||
fontSize: '9px',
|
|
||||||
marginLeft: '8px',
|
|
||||||
color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{marker} {decision.regionId}: {decision.currentSize.width.toFixed(0)}×
|
|
||||||
{decision.currentSize.height.toFixed(0)}px
|
|
||||||
{decision.rejectionReason && ` (${decision.rejectionReason})`}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: '8px',
|
|
||||||
paddingTop: '8px',
|
|
||||||
borderTop: '1px solid #444',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>Detected Regions ({detectedRegions.length}):</strong>
|
|
||||||
</div>
|
|
||||||
{detectedRegions.map((region) => (
|
|
||||||
<div key={region.id} style={{ fontSize: '10px', marginLeft: '8px' }}>
|
|
||||||
• {region.id}: {region.pixelWidth.toFixed(1)}×{region.pixelHeight.toFixed(1)}px
|
|
||||||
{region.isVerySmall ? ' (SMALL)' : ''}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div style={{ marginTop: '8px' }}>
|
|
||||||
<strong>Current Zoom:</strong> {getCurrentZoom().toFixed(1)}×
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Target Zoom:</strong> {targetZoom.toFixed(1)}×
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -4,13 +4,6 @@
|
||||||
* Debug panels and visualization tools for MapRenderer development.
|
* Debug panels and visualization tools for MapRenderer development.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type {
|
|
||||||
DebugAutoZoomPanelProps,
|
|
||||||
DetectedRegion,
|
|
||||||
DetectionResult,
|
|
||||||
} from './DebugAutoZoomPanel'
|
|
||||||
export { DebugAutoZoomPanel } from './DebugAutoZoomPanel'
|
|
||||||
|
|
||||||
export type { HotColdDebugPanelProps } from './HotColdDebugPanel'
|
export type { HotColdDebugPanelProps } from './HotColdDebugPanel'
|
||||||
export { HotColdDebugPanel } from './HotColdDebugPanel'
|
export { HotColdDebugPanel } from './HotColdDebugPanel'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forceCollide, forceSimulation, forceX, forceY, type SimulationNodeDatum } from 'd3-force'
|
import { forceCollide, forceSimulation, forceX, forceY, type SimulationNodeDatum } from 'd3-force'
|
||||||
import { type RefObject, useEffect, useState } from 'react'
|
import { RefObject, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { MapData, MapRegion } from '../../types'
|
import type { MapData, MapRegion } from '../../types'
|
||||||
import { getArrowStartPoint, getRenderedViewport } from './labelUtils'
|
import { getArrowStartPoint, getRenderedViewport } from './labelUtils'
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,15 @@ export const LetterDisplay = memo(function LetterDisplay({
|
||||||
const isSpace = char === ' '
|
const isSpace = char === ' '
|
||||||
|
|
||||||
if (isSpace) {
|
if (isSpace) {
|
||||||
return <StyledLetter key={index} char={char} status="space" isDark={isDark} index={index} />
|
return (
|
||||||
|
<StyledLetter
|
||||||
|
key={index}
|
||||||
|
char={char}
|
||||||
|
status="space"
|
||||||
|
isDark={isDark}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current index before incrementing
|
// Get current index before incrementing
|
||||||
|
|
@ -105,7 +113,15 @@ export const LetterDisplay = memo(function LetterDisplay({
|
||||||
isComplete
|
isComplete
|
||||||
)
|
)
|
||||||
|
|
||||||
return <StyledLetter key={index} char={char} status={status} isDark={isDark} index={index} />
|
return (
|
||||||
|
<StyledLetter
|
||||||
|
key={index}
|
||||||
|
char={char}
|
||||||
|
status={status}
|
||||||
|
isDark={isDark}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}, [regionName, confirmedCount, requiredLetters, isComplete, isDark])
|
}, [regionName, confirmedCount, requiredLetters, isComplete, isDark])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,10 @@ export function getLetterStatus(
|
||||||
* @param isDark - Whether dark mode is active
|
* @param isDark - Whether dark mode is active
|
||||||
* @returns CSS properties for the letter
|
* @returns CSS properties for the letter
|
||||||
*/
|
*/
|
||||||
export function getLetterStyles(status: LetterStatus, isDark: boolean): React.CSSProperties {
|
export function getLetterStyles(
|
||||||
|
status: LetterStatus,
|
||||||
|
isDark: boolean
|
||||||
|
): React.CSSProperties {
|
||||||
const baseStyles: React.CSSProperties = {
|
const baseStyles: React.CSSProperties = {
|
||||||
transition: 'all 0.15s ease-out',
|
transition: 'all 0.15s ease-out',
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +129,10 @@ export function getLetterStyles(status: LetterStatus, isDark: boolean): React.CS
|
||||||
* @param requiredLetters - Number of letters required
|
* @param requiredLetters - Number of letters required
|
||||||
* @returns Progress value (0 = none, 1 = complete)
|
* @returns Progress value (0 = none, 1 = complete)
|
||||||
*/
|
*/
|
||||||
export function calculateProgress(confirmedCount: number, requiredLetters: number): number {
|
export function calculateProgress(
|
||||||
|
confirmedCount: number,
|
||||||
|
requiredLetters: number
|
||||||
|
): number {
|
||||||
if (requiredLetters === 0) return 1
|
if (requiredLetters === 0) return 1
|
||||||
return Math.min(1, confirmedCount / requiredLetters)
|
return Math.min(1, confirmedCount / requiredLetters)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,12 @@ export function useLetterConfirmation({
|
||||||
// Get letter status for display
|
// Get letter status for display
|
||||||
const getLetterStatus = useCallback(
|
const getLetterStatus = useCallback(
|
||||||
(nonSpaceIndex: number): LetterStatus => {
|
(nonSpaceIndex: number): LetterStatus => {
|
||||||
return getLetterStatusUtil(nonSpaceIndex, confirmedCount, requiredLetters, isComplete)
|
return getLetterStatusUtil(
|
||||||
|
nonSpaceIndex,
|
||||||
|
confirmedCount,
|
||||||
|
requiredLetters,
|
||||||
|
isComplete
|
||||||
|
)
|
||||||
},
|
},
|
||||||
[confirmedCount, requiredLetters, isComplete]
|
[confirmedCount, requiredLetters, isComplete]
|
||||||
)
|
)
|
||||||
|
|
@ -104,7 +109,10 @@ export function useLetterConfirmation({
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
// Ignore if typing in an input or textarea
|
// Ignore if typing in an input or textarea
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
if (
|
||||||
|
e.target instanceof HTMLInputElement ||
|
||||||
|
e.target instanceof HTMLTextAreaElement
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,11 @@ export const MagnifierControls = memo(function MagnifierControls({
|
||||||
<>
|
<>
|
||||||
{/* Expand button - top-right corner (when not expanded and not pointer locked) */}
|
{/* Expand button - top-right corner (when not expanded and not pointer locked) */}
|
||||||
{!isExpanded && !pointerLocked && (
|
{!isExpanded && !pointerLocked && (
|
||||||
<ControlButton position="top-right" style="icon" onClick={onExpand}>
|
<ControlButton
|
||||||
|
position="top-right"
|
||||||
|
style="icon"
|
||||||
|
onClick={onExpand}
|
||||||
|
>
|
||||||
<ExpandIcon isDark={isDark} />
|
<ExpandIcon isDark={isDark} />
|
||||||
</ControlButton>
|
</ControlButton>
|
||||||
)}
|
)}
|
||||||
|
|
@ -230,7 +234,11 @@ export const MagnifierControls = memo(function MagnifierControls({
|
||||||
|
|
||||||
{/* Full Map button - bottom-left corner (when expanded) */}
|
{/* Full Map button - bottom-left corner (when expanded) */}
|
||||||
{isExpanded && showSelectButton && (
|
{isExpanded && showSelectButton && (
|
||||||
<ControlButton position="bottom-left" style="secondary" onClick={onExitExpanded}>
|
<ControlButton
|
||||||
|
position="bottom-left"
|
||||||
|
style="secondary"
|
||||||
|
onClick={onExitExpanded}
|
||||||
|
>
|
||||||
Full Map
|
Full Map
|
||||||
</ControlButton>
|
</ControlButton>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,7 @@ export interface MagnifierRegionsProps {
|
||||||
/** Get player ID who found a region */
|
/** Get player ID who found a region */
|
||||||
getPlayerWhoFoundRegion: (regionId: string) => string | null
|
getPlayerWhoFoundRegion: (regionId: string) => string | null
|
||||||
/** Get region fill color */
|
/** Get region fill color */
|
||||||
getRegionColor: (
|
getRegionColor: (regionId: string, isFound: boolean, isHovered: boolean, isDark: boolean) => string
|
||||||
regionId: string,
|
|
||||||
isFound: boolean,
|
|
||||||
isHovered: boolean,
|
|
||||||
isDark: boolean
|
|
||||||
) => string
|
|
||||||
/** Get region stroke color */
|
/** Get region stroke color */
|
||||||
getRegionStroke: (isFound: boolean, isDark: boolean) => string
|
getRegionStroke: (isFound: boolean, isDark: boolean) => string
|
||||||
/** Whether to show region outline */
|
/** Whether to show region outline */
|
||||||
|
|
@ -85,8 +80,13 @@ export const MagnifierRegions = memo(function MagnifierRegions({
|
||||||
getRegionStroke,
|
getRegionStroke,
|
||||||
showOutline,
|
showOutline,
|
||||||
}: MagnifierRegionsProps) {
|
}: MagnifierRegionsProps) {
|
||||||
const { regionsFound, hoveredRegion, celebrationRegionId, giveUpRegionId, isGiveUpAnimating } =
|
const {
|
||||||
regionState
|
regionsFound,
|
||||||
|
hoveredRegion,
|
||||||
|
celebrationRegionId,
|
||||||
|
giveUpRegionId,
|
||||||
|
isGiveUpAnimating,
|
||||||
|
} = regionState
|
||||||
const { celebrationFlash, giveUpFlash } = flashProgress
|
const { celebrationFlash, giveUpFlash } = flashProgress
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@
|
||||||
|
|
||||||
import { memo, useMemo } from 'react'
|
import { memo, useMemo } from 'react'
|
||||||
import { getRenderedViewport } from '../labels'
|
import { getRenderedViewport } from '../labels'
|
||||||
import {
|
import { getAdjustedMagnifiedDimensions, getMagnifierDimensions } from '../../utils/magnifierDimensions'
|
||||||
getAdjustedMagnifiedDimensions,
|
|
||||||
getMagnifierDimensions,
|
|
||||||
} from '../../utils/magnifierDimensions'
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
|
|
@ -127,7 +124,12 @@ export const ZoomLines = memo(function ZoomLines({
|
||||||
isDark,
|
isDark,
|
||||||
}: ZoomLinesProps) {
|
}: ZoomLinesProps) {
|
||||||
// Memoize all the calculated values
|
// Memoize all the calculated values
|
||||||
const { paths, visibleCorners, lineColor, glowColor } = useMemo(() => {
|
const {
|
||||||
|
paths,
|
||||||
|
visibleCorners,
|
||||||
|
lineColor,
|
||||||
|
glowColor,
|
||||||
|
} = useMemo(() => {
|
||||||
// Calculate leftover rectangle dimensions (area not covered by UI elements)
|
// Calculate leftover rectangle dimensions (area not covered by UI elements)
|
||||||
const leftoverWidth = containerRect.width - safeZoneMargins.left - safeZoneMargins.right
|
const leftoverWidth = containerRect.width - safeZoneMargins.left - safeZoneMargins.right
|
||||||
const leftoverHeight = containerRect.height - safeZoneMargins.top - safeZoneMargins.bottom
|
const leftoverHeight = containerRect.height - safeZoneMargins.top - safeZoneMargins.bottom
|
||||||
|
|
@ -208,7 +210,14 @@ export const ZoomLines = memo(function ZoomLines({
|
||||||
magTop + magnifierHeight
|
magTop + magnifierHeight
|
||||||
)
|
)
|
||||||
// Check if line passes through indicator
|
// Check if line passes through indicator
|
||||||
const passesThroughInd = linePassesThroughRect(from, to, indTL.x, indTL.y, indBR.x, indBR.y)
|
const passesThroughInd = linePassesThroughRect(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
indTL.x,
|
||||||
|
indTL.y,
|
||||||
|
indBR.x,
|
||||||
|
indBR.y
|
||||||
|
)
|
||||||
return !passesThroughMag && !passesThroughInd
|
return !passesThroughMag && !passesThroughInd
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -224,7 +233,9 @@ export const ZoomLines = memo(function ZoomLines({
|
||||||
: isDark
|
: isDark
|
||||||
? '#60a5fa'
|
? '#60a5fa'
|
||||||
: '#3b82f6' // blue
|
: '#3b82f6' // blue
|
||||||
const calculatedGlowColor = isHighZoom ? 'rgba(251, 191, 36, 0.6)' : 'rgba(96, 165, 250, 0.6)'
|
const calculatedGlowColor = isHighZoom
|
||||||
|
? 'rgba(251, 191, 36, 0.6)'
|
||||||
|
: 'rgba(96, 165, 250, 0.6)'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths: calculatedPaths,
|
paths: calculatedPaths,
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,7 @@
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, {
|
import React, { useCallback, useRef, type RefObject, type TouchEvent as ReactTouchEvent } from 'react'
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
type RefObject,
|
|
||||||
type TouchEvent as ReactTouchEvent,
|
|
||||||
} from 'react'
|
|
||||||
|
|
||||||
import type { UseMagnifierStateReturn } from './useMagnifierState'
|
import type { UseMagnifierStateReturn } from './useMagnifierState'
|
||||||
|
|
||||||
|
|
@ -228,10 +223,8 @@ export function useMagnifierTouch(options: UseMagnifierTouchOptions): UseMagnifi
|
||||||
totalDeltaRef.current.y += deltaY
|
totalDeltaRef.current.y += deltaY
|
||||||
|
|
||||||
// Track if user has moved significantly
|
// Track if user has moved significantly
|
||||||
if (
|
if (Math.abs(totalDeltaRef.current.x) > moveThreshold ||
|
||||||
Math.abs(totalDeltaRef.current.x) > moveThreshold ||
|
Math.abs(totalDeltaRef.current.y) > moveThreshold) {
|
||||||
Math.abs(totalDeltaRef.current.y) > moveThreshold
|
|
||||||
) {
|
|
||||||
magnifierState.didMoveRef.current = true
|
magnifierState.didMoveRef.current = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
/**
|
|
||||||
* OtherPlayerCursors Component
|
|
||||||
*
|
|
||||||
* Renders cursors for other players in multiplayer mode.
|
|
||||||
* Shows crosshairs and player emojis at cursor positions.
|
|
||||||
* Uses MapRendererContext for shared refs and viewport info.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { memo } from 'react'
|
|
||||||
import type { Player } from '@/types/player'
|
|
||||||
import { useMapRendererContext } from '../map-renderer'
|
|
||||||
import { getRenderedViewport } from '../labels'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface CursorPosition {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
playerId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OtherPlayerCursorsProps {
|
|
||||||
/** Map of user ID to cursor position */
|
|
||||||
otherPlayerCursors: Record<string, CursorPosition | null>
|
|
||||||
/** Current viewer's user ID (to filter out own cursor) */
|
|
||||||
viewerId: string | null
|
|
||||||
/** Game mode */
|
|
||||||
gameMode: 'cooperative' | 'race' | 'turn-based' | undefined
|
|
||||||
/** Current player ID (for turn-based mode) */
|
|
||||||
currentPlayer: string | null | undefined
|
|
||||||
/** Local player ID */
|
|
||||||
localPlayerId: string | null | undefined
|
|
||||||
/** Player metadata by player ID */
|
|
||||||
playerMetadata: Record<string, Player>
|
|
||||||
/** Member players by user ID (for looking up remote players) */
|
|
||||||
memberPlayers: Record<string, Player[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Component
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders cursors for other players in multiplayer sessions.
|
|
||||||
* Shows in cooperative mode (always) and turn-based mode (when not our turn).
|
|
||||||
*/
|
|
||||||
export const OtherPlayerCursors = memo(function OtherPlayerCursors({
|
|
||||||
otherPlayerCursors,
|
|
||||||
viewerId,
|
|
||||||
gameMode,
|
|
||||||
currentPlayer,
|
|
||||||
localPlayerId,
|
|
||||||
playerMetadata,
|
|
||||||
memberPlayers,
|
|
||||||
}: OtherPlayerCursorsProps) {
|
|
||||||
// Get shared state from context
|
|
||||||
const { svgRef, containerRef, parsedViewBox } = useMapRendererContext()
|
|
||||||
|
|
||||||
if (!svgRef.current || !containerRef.current) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{Object.entries(otherPlayerCursors).map(([cursorUserId, position]) => {
|
|
||||||
// Skip our own cursor (by viewerId) and null positions
|
|
||||||
if (cursorUserId === viewerId || !position) return null
|
|
||||||
|
|
||||||
// In turn-based mode, only show other cursors when it's not our turn
|
|
||||||
if (gameMode === 'turn-based' && currentPlayer === localPlayerId) return null
|
|
||||||
|
|
||||||
// Get player metadata for emoji and color (playerId is in position data)
|
|
||||||
// First check playerMetadata, then fall back to memberPlayers (for remote players)
|
|
||||||
let player = playerMetadata[position.playerId]
|
|
||||||
if (!player) {
|
|
||||||
// Player not in local playerMetadata - look through memberPlayers
|
|
||||||
// memberPlayers is keyed by userId and contains arrays of players
|
|
||||||
for (const players of Object.values(memberPlayers)) {
|
|
||||||
const found = players.find((p) => p.id === position.playerId)
|
|
||||||
if (found) {
|
|
||||||
player = found
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!player) {
|
|
||||||
console.log(
|
|
||||||
'[CursorShare] ⚠️ No player found in playerMetadata or memberPlayers for playerId:',
|
|
||||||
position.playerId
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// In collaborative mode, find all players from the same session and show all their emojis
|
|
||||||
// Use memberPlayers (from roomData) which is the canonical source of player ownership
|
|
||||||
const sessionPlayers =
|
|
||||||
gameMode === 'cooperative' && cursorUserId && memberPlayers[cursorUserId]
|
|
||||||
? memberPlayers[cursorUserId]
|
|
||||||
: [player]
|
|
||||||
|
|
||||||
// Convert SVG coordinates to screen coordinates (accounting for preserveAspectRatio letterboxing)
|
|
||||||
const svgRect = svgRef.current!.getBoundingClientRect()
|
|
||||||
const containerRect = containerRef.current!.getBoundingClientRect()
|
|
||||||
const viewport = getRenderedViewport(
|
|
||||||
svgRect,
|
|
||||||
parsedViewBox.x,
|
|
||||||
parsedViewBox.y,
|
|
||||||
parsedViewBox.width,
|
|
||||||
parsedViewBox.height
|
|
||||||
)
|
|
||||||
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
|
|
||||||
const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY
|
|
||||||
const screenX = (position.x - parsedViewBox.x) * viewport.scale + svgOffsetX
|
|
||||||
const screenY = (position.y - parsedViewBox.y) * viewport.scale + svgOffsetY
|
|
||||||
|
|
||||||
// Check if cursor is within rendered viewport bounds
|
|
||||||
if (
|
|
||||||
screenX < svgOffsetX ||
|
|
||||||
screenX > svgOffsetX + viewport.renderedWidth ||
|
|
||||||
screenY < svgOffsetY ||
|
|
||||||
screenY > svgOffsetY + viewport.renderedHeight
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`cursor-${cursorUserId}`}
|
|
||||||
data-element="other-player-cursor"
|
|
||||||
data-player-id={position.playerId}
|
|
||||||
data-user-id={cursorUserId}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: screenX,
|
|
||||||
top: screenY,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: 100,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Crosshair - centered on the cursor position */}
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: -12, // Half of width to center
|
|
||||||
top: -12, // Half of height to center
|
|
||||||
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.5))',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Outer ring */}
|
|
||||||
<circle
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="8"
|
|
||||||
fill="none"
|
|
||||||
stroke={player.color}
|
|
||||||
strokeWidth="2"
|
|
||||||
opacity="0.8"
|
|
||||||
/>
|
|
||||||
{/* Cross lines */}
|
|
||||||
<line
|
|
||||||
x1="12"
|
|
||||||
y1="2"
|
|
||||||
x2="12"
|
|
||||||
y2="8"
|
|
||||||
stroke={player.color}
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="12"
|
|
||||||
y1="16"
|
|
||||||
x2="12"
|
|
||||||
y2="22"
|
|
||||||
stroke={player.color}
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="2"
|
|
||||||
y1="12"
|
|
||||||
x2="8"
|
|
||||||
y2="12"
|
|
||||||
stroke={player.color}
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="16"
|
|
||||||
y1="12"
|
|
||||||
x2="22"
|
|
||||||
y2="12"
|
|
||||||
stroke={player.color}
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
{/* Center dot */}
|
|
||||||
<circle cx="12" cy="12" r="2" fill={player.color} />
|
|
||||||
</svg>
|
|
||||||
{/* Player emoji label(s) - positioned below crosshair */}
|
|
||||||
{/* In collaborative mode, show all emojis from the same session */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: '50%',
|
|
||||||
top: 14, // Below the crosshair (12px half-height + 2px gap)
|
|
||||||
transform: 'translateX(-50%)',
|
|
||||||
fontSize: '16px',
|
|
||||||
textShadow: '0 1px 2px rgba(0,0,0,0.5)',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sessionPlayers.map((p) => p.emoji).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
/**
|
|
||||||
* Multiplayer Feature Module
|
|
||||||
*
|
|
||||||
* Components and utilities for multiplayer functionality in Know Your World.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type { CursorPosition, OtherPlayerCursorsProps } from './OtherPlayerCursors'
|
|
||||||
export { OtherPlayerCursors } from './OtherPlayerCursors'
|
|
||||||
|
|
@ -103,7 +103,9 @@ export function MapRendererProvider({ children, value }: MapRendererProviderProp
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return <MapRendererContext.Provider value={memoizedValue}>{children}</MapRendererContext.Provider>
|
return (
|
||||||
|
<MapRendererContext.Provider value={memoizedValue}>{children}</MapRendererContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,14 @@ export interface FlashProgress {
|
||||||
/**
|
/**
|
||||||
* Hot/cold feedback type for visual indicators
|
* Hot/cold feedback type for visual indicators
|
||||||
*/
|
*/
|
||||||
export type HotColdFeedbackType = 'freezing' | 'cold' | 'cool' | 'warm' | 'hot' | 'burning' | null
|
export type HotColdFeedbackType =
|
||||||
|
| 'freezing'
|
||||||
|
| 'cold'
|
||||||
|
| 'cool'
|
||||||
|
| 'warm'
|
||||||
|
| 'hot'
|
||||||
|
| 'burning'
|
||||||
|
| null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Heat-based styling for borders and glows
|
* Heat-based styling for borders and glows
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,13 @@ export function screenToSVG(
|
||||||
svgRect: DOMRect,
|
svgRect: DOMRect,
|
||||||
viewBox: ViewBoxComponents
|
viewBox: ViewBoxComponents
|
||||||
): SVGPosition {
|
): SVGPosition {
|
||||||
const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height)
|
const viewport = getRenderedViewport(
|
||||||
|
svgRect,
|
||||||
|
viewBox.x,
|
||||||
|
viewBox.y,
|
||||||
|
viewBox.width,
|
||||||
|
viewBox.height
|
||||||
|
)
|
||||||
|
|
||||||
// Calculate offset from container origin to SVG rendered content
|
// Calculate offset from container origin to SVG rendered content
|
||||||
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
|
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
|
||||||
|
|
@ -124,7 +130,13 @@ export function svgToScreen(
|
||||||
svgRect: DOMRect,
|
svgRect: DOMRect,
|
||||||
viewBox: ViewBoxComponents
|
viewBox: ViewBoxComponents
|
||||||
): CursorPosition {
|
): CursorPosition {
|
||||||
const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height)
|
const viewport = getRenderedViewport(
|
||||||
|
svgRect,
|
||||||
|
viewBox.x,
|
||||||
|
viewBox.y,
|
||||||
|
viewBox.width,
|
||||||
|
viewBox.height
|
||||||
|
)
|
||||||
|
|
||||||
// Calculate offset from container origin to SVG rendered content
|
// Calculate offset from container origin to SVG rendered content
|
||||||
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
|
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
/**
|
|
||||||
* User Preferences Feature Module
|
|
||||||
*
|
|
||||||
* User preference settings with localStorage persistence for Know Your World.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type {
|
|
||||||
UserPreferences,
|
|
||||||
UserPreferencesHandlers,
|
|
||||||
UseUserPreferencesOptions,
|
|
||||||
UseUserPreferencesReturn,
|
|
||||||
} from './useUserPreferences'
|
|
||||||
export { useUserPreferences } from './useUserPreferences'
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
/**
|
|
||||||
* useUserPreferences Hook
|
|
||||||
*
|
|
||||||
* Manages user preference settings persisted in localStorage for Know Your World game.
|
|
||||||
* Handles auto-speak, accent, auto-hint, and hot/cold audio feedback settings.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface UserPreferences {
|
|
||||||
/** Whether hints are automatically spoken when revealed */
|
|
||||||
autoSpeak: boolean
|
|
||||||
/** Whether to use the target region's accent when speaking hints */
|
|
||||||
withAccent: boolean
|
|
||||||
/** Whether hints are automatically opened on region advance */
|
|
||||||
autoHint: boolean
|
|
||||||
/** Whether hot/cold audio feedback is enabled */
|
|
||||||
hotColdEnabled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserPreferencesHandlers {
|
|
||||||
/** Toggle auto-speak setting */
|
|
||||||
handleAutoSpeakChange: (enabled: boolean) => void
|
|
||||||
/** Toggle with-accent setting */
|
|
||||||
handleWithAccentChange: (enabled: boolean) => void
|
|
||||||
/** Toggle auto-hint setting */
|
|
||||||
handleAutoHintChange: (enabled: boolean) => void
|
|
||||||
/** Toggle hot/cold audio setting */
|
|
||||||
handleHotColdChange: (enabled: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseUserPreferencesOptions {
|
|
||||||
/** Current assistance level */
|
|
||||||
assistanceLevel?: string
|
|
||||||
/** Whether hot/cold is allowed by assistance settings */
|
|
||||||
assistanceAllowsHotCold?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseUserPreferencesReturn extends UserPreferences, UserPreferencesHandlers {}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LocalStorage Keys
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
|
||||||
autoSpeak: 'knowYourWorld.autoSpeakHint',
|
|
||||||
withAccent: 'knowYourWorld.withAccent',
|
|
||||||
autoHint: 'knowYourWorld.autoHint',
|
|
||||||
hotColdAudio: 'knowYourWorld.hotColdAudio',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Hook
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook for managing user preferences with localStorage persistence.
|
|
||||||
* Auto-enables hot/cold for learning assistance level.
|
|
||||||
*/
|
|
||||||
export function useUserPreferences({
|
|
||||||
assistanceLevel,
|
|
||||||
assistanceAllowsHotCold = false,
|
|
||||||
}: UseUserPreferencesOptions = {}): UseUserPreferencesReturn {
|
|
||||||
// Auto-speak setting persisted in localStorage
|
|
||||||
const [autoSpeak, setAutoSpeak] = useState(() => {
|
|
||||||
if (typeof window === 'undefined') return false
|
|
||||||
return localStorage.getItem(STORAGE_KEYS.autoSpeak) === 'true'
|
|
||||||
})
|
|
||||||
|
|
||||||
// With accent setting persisted in localStorage (default false - use user's locale for consistent pronunciation)
|
|
||||||
const [withAccent, setWithAccent] = useState(() => {
|
|
||||||
if (typeof window === 'undefined') return false
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEYS.withAccent)
|
|
||||||
return stored === null ? false : stored === 'true'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Auto-hint setting persisted in localStorage (auto-opens hint on region advance)
|
|
||||||
const [autoHint, setAutoHint] = useState(() => {
|
|
||||||
if (typeof window === 'undefined') return false
|
|
||||||
return localStorage.getItem(STORAGE_KEYS.autoHint) === 'true'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Hot/cold audio feedback setting persisted in localStorage
|
|
||||||
const [hotColdEnabled, setHotColdEnabled] = useState(() => {
|
|
||||||
if (typeof window === 'undefined') return false
|
|
||||||
return localStorage.getItem(STORAGE_KEYS.hotColdAudio) === 'true'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Auto-enable hot/cold for learning mode (highest assistance level)
|
|
||||||
// This ensures all players in a learning game get hot/cold feedback enabled
|
|
||||||
useEffect(() => {
|
|
||||||
if (assistanceLevel === 'learning' && assistanceAllowsHotCold && !hotColdEnabled) {
|
|
||||||
setHotColdEnabled(true)
|
|
||||||
// Also persist to localStorage so it stays enabled if they navigate away
|
|
||||||
localStorage.setItem(STORAGE_KEYS.hotColdAudio, 'true')
|
|
||||||
}
|
|
||||||
}, [assistanceLevel, assistanceAllowsHotCold, hotColdEnabled])
|
|
||||||
|
|
||||||
// Persist auto-speak setting
|
|
||||||
const handleAutoSpeakChange = useCallback((enabled: boolean) => {
|
|
||||||
setAutoSpeak(enabled)
|
|
||||||
localStorage.setItem(STORAGE_KEYS.autoSpeak, String(enabled))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Persist with-accent setting
|
|
||||||
const handleWithAccentChange = useCallback((enabled: boolean) => {
|
|
||||||
setWithAccent(enabled)
|
|
||||||
localStorage.setItem(STORAGE_KEYS.withAccent, String(enabled))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Persist auto-hint setting
|
|
||||||
const handleAutoHintChange = useCallback((enabled: boolean) => {
|
|
||||||
setAutoHint(enabled)
|
|
||||||
localStorage.setItem(STORAGE_KEYS.autoHint, String(enabled))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Persist hot/cold audio setting
|
|
||||||
const handleHotColdChange = useCallback((enabled: boolean) => {
|
|
||||||
setHotColdEnabled(enabled)
|
|
||||||
localStorage.setItem(STORAGE_KEYS.hotColdAudio, String(enabled))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return {
|
|
||||||
autoSpeak,
|
|
||||||
withAccent,
|
|
||||||
autoHint,
|
|
||||||
hotColdEnabled,
|
|
||||||
handleAutoSpeakChange,
|
|
||||||
handleWithAccentChange,
|
|
||||||
handleAutoHintChange,
|
|
||||||
handleHotColdChange,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue