-
Notifications
You must be signed in to change notification settings - Fork 66
Adds basic http auth support to Electron frontend. #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Adds basic http auth support to Electron frontend. #15
Conversation
New settings options for authentication in settings>general Added error feedback and localization for auth errors, and enhances background and Live2D URL normalization to support new URL schemes and auth requirements. Refactors settings persistence and validation logic accordingly. Tested to ensure that this is working both locally and remotely and over both HTTP and HTTPS://
Summary of ChangesHello @NBD-1138, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request integrates basic HTTP authentication into the Electron frontend, enabling users to secure their connections to backend services. It provides a user-friendly interface in the settings for managing authentication credentials, alongside robust validation and error handling. The changes also refine how the application processes and normalizes URLs for various assets, ensuring consistent behavior across different network configurations and authentication states. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces basic HTTP authentication support, which is a significant feature. The implementation touches many parts of the application, from the Electron main process to the renderer's state management and UI components. The core auth logic, including the webRequest interceptor in the main process and the fetch interceptor in the renderer, is well-designed. The refactoring of the WebSocketService to be more robust with reconnection logic is a major improvement.
However, there are several areas that need attention. The new logic for resolving background URLs in use-general-settings.ts is overly complex and should be simplified to improve maintainability. The settings page forces a page reload on save, which is a poor user experience and indicates potential issues with state management. There's also significant code duplication, particularly with a URL normalization function and what appears to be a duplicated WebSocket provider component. Additionally, the Chinese translation file is corrupted. Addressing these issues will greatly improve the quality and stability of the codebase.
| { | ||
| "common": { | ||
| "save": "保存", | ||
| "cancel": "取消", | ||
| "settings": "设置", | ||
| "close": "关闭", | ||
| "accept": "接受" | ||
| "save": "保存", | ||
| "cancel": "取消", | ||
| "settings": "设置", | ||
| "close": "关闭", | ||
| "accept": "接受" | ||
| }, | ||
| "settings": { | ||
| "tabs": { | ||
| "general": "常规", | ||
| "general": "常规", | ||
| "live2d": "Live2D", | ||
| "asr": "识别", | ||
| "tts": "合成", | ||
| "agent": "代理", | ||
| "about": "关于" | ||
| "asr": "识别", | ||
| "tts": "合成", | ||
| "agent": "代理", | ||
| "about": "关于" | ||
| }, | ||
| "general": { | ||
| "language": "语言", | ||
| "useCameraBackground": "使用摄像头背景", | ||
| "showSubtitle": "显示字幕", | ||
| "backgroundImage": "背景图片", | ||
| "customBgUrlPlaceholder": "输入图片URL", | ||
| "customBgUrl": "或输入自定义背景URL", | ||
| "characterPreset": "角色预设", | ||
| "wsUrl": "WebSocket地址", | ||
| "baseUrl": "基础URL", | ||
| "imageCompressionQuality": "图片压缩质量", | ||
| "imageCompressionQualityHelp": "JPEG压缩质量(0.1-1.0)。默认为0.8以减小文件体积,因为我们在向AI模型传输图片时没有进行压缩,可能导致文件过大。", | ||
| "imageMaxWidth": "图片最大宽度", | ||
| "imageMaxWidthHelp": "图片缩放的最大宽度。超过此宽度的图片会按比例缩小。设为0表示不限制大小。此功能存在的原因是某些AI模型在处理超大尺寸图片时可能存在限制。但是大部分AI模型都能自动处理图片,因此默认值为0(不限制),以保持您原始图片的完整性。" | ||
| "language": "Φ»¡Φ¿Ç", | ||
| "useCameraBackground": "使用摄像头背景", | ||
| "showSubtitle": "显示字幕", | ||
| "backgroundImage": "背景图片", | ||
| "customBgUrlPlaceholder": "输入图片URL", | ||
| "customBgUrl": "或输入自定义背景URL", | ||
| "characterPreset": "角色预设", | ||
| "wsUrl": "WebSocket地址", | ||
| "baseUrl": "基础URL", | ||
| "imageCompressionQuality": "图片压缩质量", | ||
| "imageCompressionQualityHelp": "JPEG压缩质量(0.1-1.0)。默认为0.8以减小文件体积,因为我们在向AI模型传输图片时没有进行压缩,可能导致文件过大。", | ||
| "imageMaxWidth": "图片最大宽度", | ||
| "imageMaxWidthHelp": "图片缩放的最大宽度。超过此宽度的图片会按比例缩小。设为0表示不限制大小。此功能存在的原因是某些AI模型在处理超大尺寸图片时可能存在限制。但是大部分AI模型都能自动处理图片,因此默认值为0(不限制),以保持您原始图片的完整性。" | ||
| }, | ||
| "live2d": { | ||
| "pointerInteractive": "鼠标交互", | ||
| "scrollToResize": "启用滚轮缩放" | ||
| "pointerInteractive": "鼠标交互", | ||
| "scrollToResize": "启用滚轮缩放" | ||
| }, | ||
| "asr": { | ||
| "autoStopMic": "AI开始说话时自动关闭麦克风", | ||
| "autoStopMicDesc": "当AI开始说话时自动关闭麦克风,防止音频反馈", | ||
| "autoStartMicOnConvEnd": "对话结束时自动开启麦克风", | ||
| "autoStartMicOnConvEndDesc": "AI说话结束时自动重新开启麦克风,实现无缝对话", | ||
| "autoStartMicOn": "AI被打断时自动开启麦克风", | ||
| "autoStartMicOnDesc": "当您打断AI时自动重新开启麦克风,保持连续交互", | ||
| "positiveSpeechThreshold": "语音识别阈值", | ||
| "positiveSpeechThresholdDesc": "检测语音所需的最低置信度(1-100),数值越高越能减少误检测", | ||
| "negativeSpeechThreshold": "负面语音阈值", | ||
| "negativeSpeechThresholdDesc": "停止语音检测的置信度下限(0-100),数值越低检测越不敏感", | ||
| "redemptionFrames": "验证帧数", | ||
| "redemptionFramesDesc": "确认语音检测所需的连续帧数(1-100),数值越高越能减少噪音触发" | ||
| "autoStopMic": "AI开始说话时自动关闭麦克风", | ||
| "autoStopMicDesc": "当AI开始说话时自动关闭麦克风,防止音频反馈", | ||
| "autoStartMicOnConvEnd": "对话结束时自动开启麦克风", | ||
| "autoStartMicOnConvEndDesc": "AI说话结束时自动重新开启麦克风,实现无缝对话", | ||
| "autoStartMicOn": "AI被打断时自动开启麦克风", | ||
| "autoStartMicOnDesc": "当您打断AI时自动重新开启麦克风,保持连续交互", | ||
| "positiveSpeechThreshold": "语音识别阈值", | ||
| "positiveSpeechThresholdDesc": "检测语音所需的最低置信度(1-100),数值越高越能减少误检测", | ||
| "negativeSpeechThreshold": "负面语音阈值", | ||
| "negativeSpeechThresholdDesc": "停止语音检测的置信度下限(0-100),数值越低检测越不敏感", | ||
| "redemptionFrames": "验证帧数", | ||
| "redemptionFramesDesc": "确认语音检测所需的连续帧数(1-100),数值越高越能减少噪音触发" | ||
| }, | ||
| "agent": { | ||
| "allowProactiveSpeak": "允许AI主动发言", | ||
| "idleSecondsToSpeak": "空闲多少秒后AI可发言", | ||
| "allowButtonTrigger": "通过举手按钮提示AI发言" | ||
| "allowProactiveSpeak": "允许AI主动发言", | ||
| "idleSecondsToSpeak": "空闲多少秒后AI可发言", | ||
| "allowButtonTrigger": "通过举手按钮提示AI发言" | ||
| }, | ||
| "about": { | ||
| "title": "Open LLM VTuber 前端", | ||
| "version": "版本", | ||
| "projectLinks": "项目链接", | ||
| "title": "Open LLM VTuber σëìτ½»", | ||
| "version": "τëêµ£¼", | ||
| "projectLinks": "项目链接", | ||
| "github": "GitHub", | ||
| "documentation": "文档", | ||
| "copyright": "版权", | ||
| "viewLicense": "查看许可证" | ||
| "documentation": "文档", | ||
| "copyright": "版权", | ||
| "viewLicense": "查看许可证" | ||
| } | ||
| }, | ||
| "footer": { | ||
| "typeYourMessage": "输入您的消息...", | ||
| "cameraControl": "点击启动摄像头", | ||
| "cameraStopping": "点击停止摄像头", | ||
| "screenControl": "点击开始屏幕共享", | ||
| "screenStopping": "点击停止屏幕共享" | ||
| "typeYourMessage": "输入您的消息...", | ||
| "cameraControl": "点击启动摄像头", | ||
| "cameraStopping": "点击停止摄像头", | ||
| "screenControl": "点击开始屏幕共享", | ||
| "screenStopping": "点击停止屏幕共享" | ||
| }, | ||
| "sidebar": { | ||
| "camera": "摄像头", | ||
| "screen": "屏幕", | ||
| "browser": "浏览器", | ||
| "live": "直播", | ||
| "noMessages": "暂无消息。开始对话吧!", | ||
| "noBrowserSession": "无活跃浏览器会话", | ||
| "browserSession": "浏览器会话" | ||
| "camera": "摄像头", | ||
| "screen": "屏幕", | ||
| "browser": "浏览器", | ||
| "live": "直播", | ||
| "noMessages": "暂无消息。开始对话吧!", | ||
| "noBrowserSession": "无活跃浏览器会话", | ||
| "browserSession": "浏览器会话" | ||
| }, | ||
| "group": { | ||
| "management": "群组管理", | ||
| "yourUuid": "您的UUID", | ||
| "inviteMember": "邀请成员", | ||
| "enterMemberUuid": "输入成员UUID", | ||
| "invite": "邀请", | ||
| "members": "成员", | ||
| "you": "您", | ||
| "leaveGroup": "离开群组", | ||
| "removeMember": "移除成员", | ||
| "leave": "离开" | ||
| "management": "群组管理", | ||
| "yourUuid": "您的UUID", | ||
| "inviteMember": "邀请成员", | ||
| "enterMemberUuid": "输入成员UUID", | ||
| "invite": "邀请", | ||
| "members": "成员", | ||
| "you": "您", | ||
| "leaveGroup": "离开群组", | ||
| "removeMember": "移除成员", | ||
| "leave": "离开" | ||
| }, | ||
| "history": { | ||
| "chatHistoryList": "聊天历史列表", | ||
| "noMessages": "暂无消息" | ||
| "chatHistoryList": "聊天历史列表", | ||
| "noMessages": "暂无消息" | ||
| }, | ||
| "notification": { | ||
| "characterLoaded": "新角色已加载", | ||
| "characterSwitched": "角色已切换", | ||
| "historyLoaded": "历史记录已加载", | ||
| "newConversation": "新对话已开始", | ||
| "newChatHistory": "新聊天历史已创建", | ||
| "historyDeleteSuccess": "历史记录删除成功", | ||
| "historyDeleteFail": "删除历史记录失败" | ||
| "characterLoaded": "新角色已加载", | ||
| "characterSwitched": "角色已切换", | ||
| "historyLoaded": "历史记录已加载", | ||
| "newConversation": "新对话已开始", | ||
| "newChatHistory": "新聊天历史已创建", | ||
| "historyDeleteSuccess": "历史记录删除成功", | ||
| "historyDeleteFail": "删除历史记录失败" | ||
| }, | ||
| "error": { | ||
| "cameraApiNotSupported": "此设备不支持摄像头API", | ||
| "noCameraFound": "未找到摄像头设备", | ||
| "failedStartCamera": "启动摄像头失败", | ||
| "failedStartBackgroundCamera": "启动背景摄像头失败", | ||
| "failedStartScreenCapture": "启动屏幕捕获失败", | ||
| "failedStartVAD": "启动语音活动检测失败", | ||
| "llmCantHear": "AI无法听到您的声音", | ||
| "audioPlayback": "音频播放错误", | ||
| "enterValidUuid": "请输入有效的UUID", | ||
| "cannotDeleteCurrentHistory": "无法删除当前聊天记录", | ||
| "failedCapture": "捕获{{source}}帧失败", | ||
| "failedParseWebSocket": "解析WebSocket消息失败", | ||
| "websocketNotOpen": "WebSocket未连接", | ||
| "vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)" | ||
| "cameraApiNotSupported": "此设备不支持摄像头API", | ||
| "noCameraFound": "未找到摄像头设备", | ||
| "failedStartCamera": "启动摄像头失败", | ||
| "failedStartBackgroundCamera": "启动背景摄像头失败", | ||
| "failedStartScreenCapture": "启动屏幕捕获失败", | ||
| "failedStartVAD": "启动语音活动检测失败", | ||
| "llmCantHear": "AI无法听到您的声音", | ||
| "audioPlayback": "音频播放错误", | ||
| "enterValidUuid": "请输入有效的UUID", | ||
| "cannotDeleteCurrentHistory": "无法删除当前聊天记录", | ||
| "failedCapture": "捕获{{source}}帧失败", | ||
| "failedParseWebSocket": "解析WebSocket消息失败", | ||
| "websocketNotOpen": "WebSocket未连接", | ||
| "vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)" | ||
| }, | ||
| "aiState": { | ||
| "idle": "空闲", | ||
| "thinking-speaking": "思考/说话中", | ||
| "interrupted": "已打断", | ||
| "loading": "加载中", | ||
| "listening": "聆听中", | ||
| "waiting": "等待中" | ||
| "idle": "空闲", | ||
| "thinking-speaking": "思考/说话中", | ||
| "interrupted": "已打断", | ||
| "loading": "加载中", | ||
| "listening": "聆听中", | ||
| "waiting": "等待中" | ||
| }, | ||
| "wsStatus": { | ||
| "connected": "已连接", | ||
| "connecting": "连接中", | ||
| "clickToReconnect": "点击重新连接" | ||
| "connected": "已连接", | ||
| "connecting": "连接中", | ||
| "clickToReconnect": "点击重新连接", | ||
| "authFailed": "认证失败,点击重新连接" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The content of this file appears to be corrupted, likely due to a character encoding issue. The Chinese characters are not rendered correctly (mojibake). This will break the UI for users who have selected the Chinese language. Please fix the file encoding to ensure the translations are displayed correctly.
| const arraysShallowEqual = (a: string[], b: string[]) => ( | ||
| a.length === b.length && a.every((value, index) => value === b[index]) | ||
| ); | ||
|
|
||
| const settingsEqual = (prev: GeneralSettings, next: GeneralSettings) => ( | ||
| arraysShallowEqual(prev.language, next.language) | ||
| && prev.customBgUrl === next.customBgUrl | ||
| && arraysShallowEqual(prev.selectedBgUrl, next.selectedBgUrl) | ||
| && prev.backgroundUrl === next.backgroundUrl | ||
| && arraysShallowEqual(prev.selectedCharacterPreset, next.selectedCharacterPreset) | ||
| && prev.useCameraBackground === next.useCameraBackground | ||
| && prev.wsUrl === next.wsUrl | ||
| && prev.baseUrl === next.baseUrl | ||
| && prev.showSubtitle === next.showSubtitle | ||
| && prev.imageCompressionQuality === next.imageCompressionQuality | ||
| && prev.imageMaxWidth === next.imageMaxWidth | ||
| && prev.basicAuthEnabled === next.basicAuthEnabled | ||
| && prev.basicAuthUsername === next.basicAuthUsername | ||
| && prev.basicAuthPassword === next.basicAuthPassword | ||
| ); | ||
|
|
||
| type BackgroundFile = BgUrlContextState['backgroundFiles'][number]; | ||
|
|
||
| type BackgroundEntry = BackgroundFile | string; | ||
|
|
||
| const toBackgroundEntry = (value: BackgroundEntry | null | undefined): BackgroundEntry | undefined => { | ||
| if (value == null) return undefined; | ||
| if (typeof value === 'string') { | ||
| const trimmed = value.trim(); | ||
| return trimmed || undefined; | ||
| } | ||
| if (typeof value === 'object') { | ||
| return value; | ||
| } | ||
| return undefined; | ||
| }; | ||
|
|
||
| const getEntryName = (entry: BackgroundEntry | undefined): string => { | ||
| if (!entry) return ''; | ||
| if (typeof entry === 'string') return entry.trim(); | ||
| return entry.name?.trim() || ''; | ||
| }; | ||
|
|
||
| const getEntryUrl = (entry: BackgroundEntry | undefined): string => { | ||
| if (!entry) return ''; | ||
| if (typeof entry === 'string') return entry.trim(); | ||
| return entry.url?.trim() || ''; | ||
| }; | ||
|
|
||
| const normalizeScheme = (value: string): string => { | ||
| if (!value) return value; | ||
| if (/^ws:\/\//i.test(value)) { | ||
| return value.replace(/^ws:\/\//i, 'http://'); | ||
| } | ||
| if (/^wss:\/\//i.test(value)) { | ||
| return value.replace(/^wss:\/\//i, 'https://'); | ||
| } | ||
| return value; | ||
| }; | ||
|
|
||
| const ensureTrailingSlash = (value: string): string => ( | ||
| value.endsWith('/') ? value : `${value}/` | ||
| ); | ||
|
|
||
| const stripProtocolAndHost = (value: string): string => value.replace(/^https?:\/\/[^/]+/i, ''); | ||
|
|
||
| const stripLeadingSlashes = (value: string): string => value.replace(/^[/\\]+/, ''); | ||
|
|
||
| const stripBgPrefix = (value: string): string => value.replace(/^bg[/\\]/i, ''); | ||
|
|
||
| const computeBackgroundOptionValue = (entry: BackgroundEntry): string => { | ||
| const name = getEntryName(entry); | ||
| if (name) { | ||
| return name; | ||
| } | ||
| const url = getEntryUrl(entry); | ||
| if (!url) { | ||
| return ''; | ||
| } | ||
| const filename = url.split('/').filter(Boolean).pop(); | ||
| return filename || url; | ||
| }; | ||
|
|
||
| const matchBackgroundFile = ( | ||
| candidateRaw: string, | ||
| files?: BackgroundEntry[], | ||
| ): BackgroundEntry | undefined => { | ||
| const entries = files | ||
| ?.map((value) => toBackgroundEntry(value)) | ||
| .filter((value): value is BackgroundEntry => Boolean(value)); | ||
|
|
||
| if (!entries?.length) { | ||
| return undefined; | ||
| } | ||
| const candidate = candidateRaw.trim(); | ||
| if (!candidate) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const candidateWithoutHost = stripProtocolAndHost(candidate); | ||
| const candidateWithoutLeading = stripLeadingSlashes(candidateWithoutHost); | ||
| const candidateCore = stripBgPrefix(candidateWithoutLeading); | ||
|
|
||
| return entries.find((entry) => { | ||
| const sources = [getEntryUrl(entry), getEntryName(entry)].filter(Boolean); | ||
| if (!sources.length) { | ||
| return false; | ||
| } | ||
| return sources.some((source) => { | ||
| const trimmedSource = source.trim(); | ||
| const sourceWithoutHost = stripProtocolAndHost(trimmedSource); | ||
| const sourceWithoutLeading = stripLeadingSlashes(sourceWithoutHost); | ||
| const sourceCore = stripBgPrefix(sourceWithoutLeading); | ||
|
|
||
| return candidate === trimmedSource | ||
| || candidateWithoutLeading === sourceWithoutLeading | ||
| || candidateCore === sourceCore; | ||
| }); | ||
| }); | ||
| }; | ||
|
|
||
| const normalizeBackgroundResourcePath = ( | ||
| value: string, | ||
| fallbackToBgDirectory: boolean, | ||
| ): string => { | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) { | ||
| return ''; | ||
| } | ||
| if (trimmed.startsWith('/')) { | ||
| return trimmed; | ||
| } | ||
| if (trimmed.startsWith('bg/')) { | ||
| return `/${trimmed}`; | ||
| } | ||
| if (trimmed.startsWith('./') || trimmed.startsWith('../')) { | ||
| return trimmed; | ||
| } | ||
| if (fallbackToBgDirectory) { | ||
| if (trimmed.includes('/')) { | ||
| return `/${trimmed}`; | ||
| } | ||
| return `/bg/${trimmed}`; | ||
| } | ||
| return trimmed; | ||
| }; | ||
|
|
||
| const resolveBackgroundUrl = ( | ||
| candidate: string | undefined, | ||
| baseUrl: string, | ||
| files?: BackgroundEntry[], | ||
| matchedFile?: BackgroundEntry, | ||
| ): string | undefined => { | ||
| const trimmedCandidate = candidate?.trim(); | ||
| if (!trimmedCandidate) { | ||
| return undefined; | ||
| } | ||
| const normalizedBase = ensureTrailingSlash(normalizeScheme(baseUrl)); | ||
| const entries = files | ||
| ?.map((value) => toBackgroundEntry(value)) | ||
| .filter((value): value is BackgroundEntry => Boolean(value)); | ||
| const file = matchedFile ?? matchBackgroundFile(trimmedCandidate, entries); | ||
| const rawSource = [ | ||
| getEntryUrl(file), | ||
| getEntryName(file), | ||
| trimmedCandidate, | ||
| ].find((value) => Boolean(value && value.trim())) || trimmedCandidate; | ||
|
|
||
| const rawValue = normalizeScheme(rawSource); | ||
|
|
||
| if (/^https?:\/\//i.test(rawValue)) { | ||
| return rawValue; | ||
| } | ||
|
|
||
| const resourcePath = normalizeBackgroundResourcePath(rawValue, Boolean(file)); | ||
| if (!resourcePath) { | ||
| return undefined; | ||
| } | ||
|
|
||
| try { | ||
| return new URL(resourcePath, normalizedBase).toString(); | ||
| } catch (error) { | ||
| console.error('Failed to resolve background URL:', { | ||
| candidate: trimmedCandidate, | ||
| resourcePath, | ||
| baseUrl: normalizedBase, | ||
| error, | ||
| }); | ||
| return resourcePath; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for resolving background URLs is very complex and spans almost 200 lines. It involves multiple helper functions for string manipulation and matching (stripProtocolAndHost, stripLeadingSlashes, stripBgPrefix, matchBackgroundFile, etc.). This complexity makes the code difficult to understand and maintain.
A simpler approach would be to ensure that the value for each background option in the select dropdown is a stable, unique identifier (like the full URL or a unique key). This would eliminate the need for this complex and potentially brittle matching logic.
| if (changedSinceLastSave && typeof window !== 'undefined') { | ||
| window.setTimeout(() => { | ||
| window.location.reload(); | ||
| }, 200); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forcing a page reload via window.location.reload() after saving settings provides a poor user experience. It suggests that the application's state is not fully reactive to settings changes. The goal should be to update the application state dynamically without requiring a full reload. Please consider refactoring the state management so that components re-render correctly when settings are updated.
| ], | ||
| }); | ||
|
|
||
| console.debug('[Settings-General] backgroundFiles from context', backgroundFiles); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| }, [setBasicAuthErrors, i18n.language]); | ||
|
|
||
| useEffect(() => { | ||
| console.debug('[useGeneralSettings] settings effect triggered', settings); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Removed console debug
New settings options for authentication in settings>general Added error feedback and localization for auth errors, and enhances background and Live2D URL normalization to support new URL schemes and auth requirements. Refactors settings persistence and validation logic accordingly. Tested to ensure that this is working both locally and remotely and over both HTTP and HTTPS://