From 8245c6f2df29c70f9b98af3ea473b0d472f574f0 Mon Sep 17 00:00:00 2001 From: Rishi Date: Wed, 3 Jun 2026 06:03:35 +0530 Subject: [PATCH] feat: make subtitles draggable and customizable --- package-lock.json | 4 +- src/renderer/src/App.tsx | 17 ++++++- .../src/components/canvas/subtitle.tsx | 44 ++++++++++++++--- .../components/sidebar/setting/general.tsx | 31 ++++++++++++ src/renderer/src/context/subtitle-context.tsx | 49 ++++++++++++++++++- .../sidebar/setting/use-general-settings.ts | 9 +++- 6 files changed, 141 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6bd2ca1f..1cbf70c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-llm-vtuber", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "open-llm-vtuber", - "version": "1.2.0", + "version": "1.2.1", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^3.2.3", diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 9fb874ec..75aa8a37 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -29,6 +29,7 @@ import Background from "./components/canvas/background"; import WebSocketStatus from "./components/canvas/ws-status"; import Subtitle from "./components/canvas/subtitle"; import { ModeProvider, useMode } from "./context/mode-context"; +import { useDraggable } from "@/hooks/electron/use-draggable"; function AppContent(): JSX.Element { const [showSidebar, setShowSidebar] = useState(true); @@ -37,6 +38,13 @@ function AppContent(): JSX.Element { const isElectron = window.api !== undefined; const live2dContainerRef = useRef(null); + const { + elementRef: subtitleRef, + handleMouseDown: handleSubtitleMouseDown, + handleMouseEnter: handleSubtitleMouseEnter, + handleMouseLeave: handleSubtitleMouseLeave, + } = useDraggable({ componentId: 'subtitle' }); + useEffect(() => { const handleResize = () => { const vh = window.innerHeight * 0.01; @@ -131,8 +139,15 @@ function AppContent(): JSX.Element { transform="translateX(-50%)" zIndex={10} width="60%" + display="flex" + justifyContent="center" > - + ( - +const SubtitleText = memo(({ text, fontSize, color, fontFamily }: SubtitleTextProps) => ( + {text} )); SubtitleText.displayName = 'SubtitleText'; +interface SubtitleProps { + onMouseDown?: (e: React.MouseEvent) => void + onMouseEnter?: () => void + onMouseLeave?: () => void +} + // Main component -const Subtitle = memo((): JSX.Element | null => { +const Subtitle = memo(forwardRef(({ onMouseDown, onMouseEnter, onMouseLeave }, ref): JSX.Element | null => { const { subtitleText, isLoaded } = useSubtitleDisplay(); - const { showSubtitle } = useSubtitle(); + const { showSubtitle, subtitleConfig } = useSubtitle(); if (!isLoaded || !subtitleText || !showSubtitle) return null; return ( - - + + ); -}); +})); Subtitle.displayName = 'Subtitle'; diff --git a/src/renderer/src/components/sidebar/setting/general.tsx b/src/renderer/src/components/sidebar/setting/general.tsx index 9fbbafa3..57dc5bdb 100644 --- a/src/renderer/src/components/sidebar/setting/general.tsx +++ b/src/renderer/src/components/sidebar/setting/general.tsx @@ -61,6 +61,8 @@ function General({ onSave, onCancel }: GeneralProps): JSX.Element { handleCharacterPresetChange, showSubtitle, setShowSubtitle, + subtitleConfig, + setSubtitleConfig, } = useGeneralSettings({ bgUrlContext, confName, @@ -99,6 +101,35 @@ function General({ onSave, onCancel }: GeneralProps): JSX.Element { onChange={setShowSubtitle} /> + {showSubtitle && ( + <> + setSubtitleConfig({ fontSize: value as string })} + placeholder="e.g. 1.5rem, 24px" + /> + setSubtitleConfig({ color: value as string })} + placeholder="e.g. white, #ffffff" + /> + setSubtitleConfig({ backgroundColor: value as string })} + placeholder="e.g. rgba(0, 0, 0, 0.7)" + /> + setSubtitleConfig({ fontFamily: value as string })} + placeholder="e.g. Arial, sans-serif" + /> + + )} + {!settings.useCameraBackground && ( <> void + + /** Subtitle style configuration */ + subtitleConfig: SubtitleConfig + + /** Update subtitle style configuration */ + setSubtitleConfig: (config: Partial) => void } /** @@ -26,8 +43,16 @@ interface SubtitleState { const DEFAULT_SUBTITLE = { text: "Hi, I'm some random AI VTuber. Who the hell are ya? " + 'Ahh, you must be amazed by my awesomeness, right? right?', + config: { + fontSize: '1.5rem', + color: 'white', + backgroundColor: 'rgba(0, 0, 0, 0.7)', + fontFamily: 'inherit', + }, }; +const SUBTITLE_CONFIG_KEY = 'appSubtitleConfig'; + /** * Create the subtitle context */ @@ -45,6 +70,26 @@ export const SubtitleProvider = memo(({ children }: { children: React.ReactNode const [subtitleText, setSubtitleText] = useState(DEFAULT_SUBTITLE.text); const [showSubtitle, setShowSubtitle] = useState(true); + const [subtitleConfig, setSubtitleConfigState] = useState(() => { + const savedConfig = localStorage.getItem(SUBTITLE_CONFIG_KEY); + if (savedConfig) { + try { + return { ...DEFAULT_SUBTITLE.config, ...JSON.parse(savedConfig) }; + } catch (e) { + console.error('Failed to parse subtitle config', e); + } + } + return DEFAULT_SUBTITLE.config; + }); + + const setSubtitleConfig = (config: Partial) => { + setSubtitleConfigState((prev) => { + const newConfig = { ...prev, ...config }; + localStorage.setItem(SUBTITLE_CONFIG_KEY, JSON.stringify(newConfig)); + return newConfig; + }); + }; + // Memoized context value const contextValue = useMemo( () => ({ @@ -52,8 +97,10 @@ export const SubtitleProvider = memo(({ children }: { children: React.ReactNode setSubtitleText, showSubtitle, setShowSubtitle, + subtitleConfig, + setSubtitleConfig, }), - [subtitleText, showSubtitle], + [subtitleText, showSubtitle, subtitleConfig], ); return ( diff --git a/src/renderer/src/hooks/sidebar/setting/use-general-settings.ts b/src/renderer/src/hooks/sidebar/setting/use-general-settings.ts index 0633b539..3055904c 100644 --- a/src/renderer/src/hooks/sidebar/setting/use-general-settings.ts +++ b/src/renderer/src/hooks/sidebar/setting/use-general-settings.ts @@ -73,7 +73,12 @@ export const useGeneralSettings = ({ onSave, onCancel, }: UseGeneralSettingsProps) => { - const { showSubtitle, setShowSubtitle } = useSubtitle(); + const { + showSubtitle, + setShowSubtitle, + subtitleConfig, + setSubtitleConfig, + } = useSubtitle(); const { setUseCameraBackground } = bgUrlContext || {}; const { startBackgroundCamera, stopBackgroundCamera } = useCamera(); const { configFiles, getFilenameByName } = useConfig(); @@ -256,5 +261,7 @@ export const useGeneralSettings = ({ handleCharacterPresetChange, showSubtitle, setShowSubtitle, + subtitleConfig, + setSubtitleConfig, }; };