From fc8da352fba41ca7627623baba4233d002f19db8 Mon Sep 17 00:00:00 2001 From: zimplexing Date: Fri, 11 Jul 2025 13:49:45 +0800 Subject: [PATCH] feat: Refactor settings management into a dedicated page with new configuration options --- .claude/settings.local.json | 4 +- app/_layout.tsx | 29 ++-- app/index.tsx | 11 +- app/settings.tsx | 98 +++++++++++++ components/SettingsModal.tsx | 118 ---------------- components/settings/APIConfigSection.tsx | 81 +++++++++++ components/settings/LiveStreamSection.tsx | 37 +++++ components/settings/PlaybackSourceSection.tsx | 37 +++++ components/settings/RemoteInputSection.tsx | 121 ++++++++++++++++ docs/SETTINGS_REFACTOR_PLAN.md | 131 ++++++++++++++++++ services/remoteControlService.ts | 5 - services/tcpHttpServer.ts | 9 +- stores/settingsStore.ts | 72 +++++++++- 13 files changed, 596 insertions(+), 157 deletions(-) create mode 100644 app/settings.tsx delete mode 100644 components/SettingsModal.tsx create mode 100644 components/settings/APIConfigSection.tsx create mode 100644 components/settings/LiveStreamSection.tsx create mode 100644 components/settings/PlaybackSourceSection.tsx create mode 100644 components/settings/RemoteInputSection.tsx create mode 100644 docs/SETTINGS_REFACTOR_PLAN.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9f73004..8b7acb1 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,9 @@ "Bash(rm:*)", "Bash(yarn install)", "Bash(yarn lint)", - "Bash(yarn prebuild-tv:*)" + "Bash(yarn prebuild-tv:*)", + "Bash(mkdir:*)", + "Bash(yarn lint:*)" ], "deny": [] } diff --git a/app/_layout.tsx b/app/_layout.tsx index 6535068..072a576 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -18,11 +18,12 @@ export default function RootLayout() { const [loaded, error] = useFonts({ SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"), }); - const initializeSettings = useSettingsStore((state) => state.loadSettings); + const { loadSettings, remoteInputEnabled } = useSettingsStore(); + const { startServer, stopServer } = useRemoteControlStore(); useEffect(() => { - initializeSettings(); - }, [initializeSettings]); + loadSettings(); + }, [loadSettings]); useEffect(() => { if (loaded || error) { @@ -34,21 +35,12 @@ export default function RootLayout() { }, [loaded, error]); useEffect(() => { - // Initialize the service with store actions to break require cycle - const { setMessage, hideModal } = useRemoteControlStore.getState(); - remoteControlService.init({ - onMessage: setMessage, - onHandshake: hideModal, - }); - - // Start the remote control server on app launch - useRemoteControlStore.getState().startServer(); - - return () => { - // Stop the server on app close - useRemoteControlStore.getState().stopServer(); - }; - }, []); + if (remoteInputEnabled) { + startServer(); + } else { + stopServer(); + } + }, [remoteInputEnabled, startServer, stopServer]); if (!loaded && !error) { return null; @@ -62,6 +54,7 @@ export default function RootLayout() { {Platform.OS !== "web" && } + diff --git a/app/index.tsx b/app/index.tsx index 661e5dc..18f186d 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -6,10 +6,8 @@ import { api } from "@/services/api"; import VideoCard from "@/components/VideoCard.tv"; import { useFocusEffect, useRouter } from "expo-router"; import { Search, Settings } from "lucide-react-native"; -import { SettingsModal } from "@/components/SettingsModal"; import { StyledButton } from "@/components/StyledButton"; import useHomeStore, { RowItem, Category } from "@/stores/homeStore"; -import { useSettingsStore } from "@/stores/settingsStore"; const NUM_COLUMNS = 5; const { width } = Dimensions.get("window"); @@ -34,8 +32,6 @@ export default function HomeScreen() { refreshPlayRecords, } = useHomeStore(); - const showSettingsModal = useSettingsStore((state) => state.showModal); - useFocusEffect( useCallback(() => { refreshPlayRecords(); @@ -52,14 +48,14 @@ export default function HomeScreen() { setSelectedTag(defaultTag); selectCategory({ ...selectedCategory, tag: defaultTag }); } - }, [selectedCategory, fetchInitialData]); + }, [selectedCategory, fetchInitialData, selectCategory]); useEffect(() => { if (selectedCategory && selectedCategory.tag) { fetchInitialData(); flatListRef.current?.scrollToOffset({ animated: false, offset: 0 }); } - }, [selectedCategory?.tag]); + }, [fetchInitialData, selectedCategory, selectedCategory.tag]); const handleCategorySelect = (category: Category) => { setSelectedTag(null); @@ -133,7 +129,7 @@ export default function HomeScreen() { > - + router.push("/settings")} variant="ghost"> @@ -207,7 +203,6 @@ export default function HomeScreen() { } /> )} - ); } diff --git a/app/settings.tsx b/app/settings.tsx new file mode 100644 index 0000000..a411d0a --- /dev/null +++ b/app/settings.tsx @@ -0,0 +1,98 @@ +import React, { useState, useEffect } from "react"; +import { View, StyleSheet, ScrollView, Alert } from "react-native"; +import { ThemedText } from "@/components/ThemedText"; +import { ThemedView } from "@/components/ThemedView"; +import { StyledButton } from "@/components/StyledButton"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { APIConfigSection } from "@/components/settings/APIConfigSection"; +import { LiveStreamSection } from "@/components/settings/LiveStreamSection"; +import { RemoteInputSection } from "@/components/settings/RemoteInputSection"; +import { PlaySourceSection } from "@/components/settings/PlaybackSourceSection"; + +export default function SettingsScreen() { + const { loadSettings, saveSettings } = useSettingsStore(); + + const [hasChanges, setHasChanges] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + loadSettings(); + }, [loadSettings]); + + const handleSave = async () => { + setIsLoading(true); + try { + await saveSettings(); + setHasChanges(false); + Alert.alert("成功", "设置已保存"); + } catch { + Alert.alert("错误", "保存设置失败"); + } finally { + setIsLoading(false); + } + }; + + const markAsChanged = () => { + setHasChanges(true); + }; + + return ( + + + 设置 + + + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 24, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 10, + }, + title: { + fontSize: 32, + fontWeight: "bold", + paddingTop: 24, + }, + backButton: { + minWidth: 100, + }, + scrollView: { + flex: 1, + }, + footer: { + paddingTop: 24, + borderTopWidth: 1, + borderTopColor: "#333", + }, + saveButton: { + minHeight: 50, + }, + disabledButton: { + opacity: 0.5, + }, +}); diff --git a/components/SettingsModal.tsx b/components/SettingsModal.tsx deleted file mode 100644 index 995281b..0000000 --- a/components/SettingsModal.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useState, useEffect, useRef } from "react"; -import { Modal, View, Text, TextInput, StyleSheet } from "react-native"; -import { ThemedText } from "./ThemedText"; -import { ThemedView } from "./ThemedView"; -import { useSettingsStore } from "@/stores/settingsStore"; -import { StyledButton } from "./StyledButton"; - -export const SettingsModal: React.FC = () => { - const { isModalVisible, hideModal, apiBaseUrl, setApiBaseUrl, saveSettings, loadSettings } = useSettingsStore(); - - const [isInputFocused, setIsInputFocused] = useState(false); - const colorScheme = "dark"; // Replace with useColorScheme() if needed - const inputRef = useRef(null); - - useEffect(() => { - if (isModalVisible) { - loadSettings(); - const timer = setTimeout(() => { - inputRef.current?.focus(); - }, 200); - return () => clearTimeout(timer); - } - }, [isModalVisible, loadSettings]); - - const handleSave = () => { - saveSettings(); - }; - - const styles = StyleSheet.create({ - modalContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: "rgba(0, 0, 0, 0.6)", - }, - modalContent: { - width: "80%", - maxWidth: 500, - padding: 24, - borderRadius: 12, - elevation: 10, - }, - title: { - fontSize: 24, - fontWeight: "bold", - marginBottom: 20, - textAlign: "center", - }, - input: { - height: 50, - borderWidth: 2, - borderRadius: 8, - paddingHorizontal: 15, - fontSize: 16, - marginBottom: 24, - backgroundColor: colorScheme === "dark" ? "#3a3a3c" : "#f0f0f0", - color: colorScheme === "dark" ? "white" : "black", - borderColor: "transparent", - }, - inputFocused: { - borderColor: "#007AFF", - shadowColor: "#007AFF", - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 0.8, - shadowRadius: 10, - elevation: 5, - }, - buttonContainer: { - flexDirection: "row", - justifyContent: "space-around", - }, - button: { - flex: 1, - marginHorizontal: 8, - }, - buttonText: { - fontSize: 18, - }, - }); - - return ( - - - - 设置 - setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - /> - - - - - - - - ); -}; diff --git a/components/settings/APIConfigSection.tsx b/components/settings/APIConfigSection.tsx new file mode 100644 index 0000000..5d22a30 --- /dev/null +++ b/components/settings/APIConfigSection.tsx @@ -0,0 +1,81 @@ +import React, { useState, useRef } from "react"; +import { View, TextInput, StyleSheet } from "react-native"; +import { ThemedText } from "@/components/ThemedText"; +import { ThemedView } from "@/components/ThemedView"; +import { useSettingsStore } from "@/stores/settingsStore"; + +interface APIConfigSectionProps { + onChanged: () => void; +} + +export const APIConfigSection: React.FC = ({ onChanged }) => { + const { apiBaseUrl, setApiBaseUrl } = useSettingsStore(); + const [isInputFocused, setIsInputFocused] = useState(false); + const inputRef = useRef(null); + + const handleUrlChange = (url: string) => { + setApiBaseUrl(url); + onChanged(); + }; + + return ( + + + API 地址 + setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + /> + + + ); +}; + +const styles = StyleSheet.create({ + section: { + padding: 20, + marginBottom: 16, + borderRadius: 12, + borderWidth: 1, + borderColor: "#333", + }, + sectionTitle: { + fontSize: 16, + fontWeight: "bold", + marginBottom: 8, + }, + inputContainer: { + marginBottom: 12, + }, + label: { + fontSize: 16, + marginBottom: 8, + color: "#ccc", + }, + input: { + height: 50, + borderWidth: 2, + borderRadius: 8, + paddingHorizontal: 15, + fontSize: 16, + backgroundColor: "#3a3a3c", + color: "white", + borderColor: "transparent", + }, + inputFocused: { + borderColor: "#007AFF", + shadowColor: "#007AFF", + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.8, + shadowRadius: 10, + elevation: 5, + }, +}); diff --git a/components/settings/LiveStreamSection.tsx b/components/settings/LiveStreamSection.tsx new file mode 100644 index 0000000..98a396b --- /dev/null +++ b/components/settings/LiveStreamSection.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; + +interface LiveStreamSectionProps { + onChanged: () => void; +} + +export const LiveStreamSection: React.FC = ({ onChanged }) => { + return ( + + 直播源配置 + 直播源配置功能即将上线 + + ); +}; + +const styles = StyleSheet.create({ + section: { + padding: 20, + marginBottom: 16, + borderRadius: 12, + borderWidth: 1, + borderColor: '#333', + }, + sectionTitle: { + fontSize: 20, + fontWeight: 'bold', + marginBottom: 16, + }, + placeholder: { + fontSize: 14, + color: '#888', + fontStyle: 'italic', + }, +}); \ No newline at end of file diff --git a/components/settings/PlaybackSourceSection.tsx b/components/settings/PlaybackSourceSection.tsx new file mode 100644 index 0000000..8895461 --- /dev/null +++ b/components/settings/PlaybackSourceSection.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { StyleSheet } from "react-native"; +import { ThemedText } from "@/components/ThemedText"; +import { ThemedView } from "@/components/ThemedView"; + +interface PlaybackSourceSectionProps { + onChanged: () => void; +} + +export const PlaySourceSection: React.FC = ({ onChanged }) => { + return ( + + 播放源配置 + 播放源配置功能即将上线 + + ); +}; + +const styles = StyleSheet.create({ + section: { + padding: 20, + marginBottom: 16, + borderRadius: 12, + borderWidth: 1, + borderColor: "#333", + }, + sectionTitle: { + fontSize: 20, + fontWeight: "bold", + marginBottom: 16, + }, + placeholder: { + fontSize: 14, + color: "#888", + fontStyle: "italic", + }, +}); diff --git a/components/settings/RemoteInputSection.tsx b/components/settings/RemoteInputSection.tsx new file mode 100644 index 0000000..7e59d7b --- /dev/null +++ b/components/settings/RemoteInputSection.tsx @@ -0,0 +1,121 @@ +import React from "react"; +import { View, Switch, StyleSheet } from "react-native"; +import { ThemedText } from "@/components/ThemedText"; +import { ThemedView } from "@/components/ThemedView"; +import { useSettingsStore } from "@/stores/settingsStore"; +import { useRemoteControlStore } from "@/stores/remoteControlStore"; + +interface RemoteInputSectionProps { + onChanged: () => void; +} + +export const RemoteInputSection: React.FC = ({ onChanged }) => { + const { remoteInputEnabled, setRemoteInputEnabled } = useSettingsStore(); + const { isServerRunning, serverUrl, error } = useRemoteControlStore(); + + const handleToggle = async (enabled: boolean) => { + setRemoteInputEnabled(enabled); + onChanged(); + }; + + return ( + + + + 启用远程输入 + + + + + {remoteInputEnabled && ( + + + 服务状态: + + {isServerRunning ? "运行中" : "已停止"} + + + + {serverUrl && ( + + 访问地址: + {serverUrl} + + )} + + {error && ( + + 错误: + {error} + + )} + + )} + + ); +}; + +const styles = StyleSheet.create({ + section: { + padding: 20, + marginBottom: 16, + borderRadius: 12, + borderWidth: 1, + borderColor: "#333", + }, + sectionTitle: { + fontSize: 20, + fontWeight: "bold", + marginBottom: 16, + }, + settingItem: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: 12, + }, + settingInfo: { + flex: 1, + }, + settingName: { + fontSize: 16, + fontWeight: "bold", + marginBottom: 4, + }, + settingDescription: { + fontSize: 14, + color: "#888", + }, + statusContainer: { + marginTop: 16, + padding: 16, + backgroundColor: "#2a2a2c", + borderRadius: 8, + }, + statusItem: { + flexDirection: "row", + marginBottom: 8, + }, + statusLabel: { + fontSize: 14, + color: "#ccc", + minWidth: 80, + }, + statusValue: { + fontSize: 14, + flex: 1, + }, + actionButtons: { + flexDirection: "row", + gap: 12, + marginTop: 12, + }, + actionButton: { + flex: 1, + }, +}); diff --git a/docs/SETTINGS_REFACTOR_PLAN.md b/docs/SETTINGS_REFACTOR_PLAN.md new file mode 100644 index 0000000..f1a0a02 --- /dev/null +++ b/docs/SETTINGS_REFACTOR_PLAN.md @@ -0,0 +1,131 @@ +# 设置页面重构方案 + +## 目标 +1. 将设置从弹窗模式改为独立页面 +2. 新增直播源配置功能 +3. 新增远程输入开关配置 +4. 新增播放源启用配置 + +## 现有架构分析 + +### 当前设置相关文件: +- `stores/settingsStore.ts` - 设置状态管理,目前只有API地址配置 +- `components/SettingsModal.tsx` - 设置弹窗组件 +- `stores/remoteControlStore.ts` - 远程控制状态管理 + +### 现有功能: +- API基础地址配置 +- 远程控制服务器(但未集成到设置中) + +## 重构方案 + +### 1. 创建独立设置页面 +- 新建 `app/settings.tsx` 页面 +- 使用 Expo Router 的文件路由系统 +- 删除现有的 `SettingsModal.tsx` 组件 + +### 2. 扩展设置Store +在 `settingsStore.ts` 中新增以下配置项: +```typescript +interface SettingsState { + // 现有配置 + apiBaseUrl: string; + + // 新增配置项 + liveStreamSources: LiveStreamSource[]; // 直播源配置 + remoteInputEnabled: boolean; // 远程输入开关 + playbackSourceConfig: PlaybackSourceConfig; // 播放源配置 +} + +interface LiveStreamSource { + id: string; + name: string; + url: string; + enabled: boolean; +} + +interface PlaybackSourceConfig { + primarySource: string; + fallbackSources: string[]; + enabledSources: string[]; +} +``` + +### 3. 设置页面UI结构 +``` +设置页面 (app/settings.tsx) +├── API 配置区域 +│ └── API 基础地址输入框 +├── 直播源配置区域 +│ ├── 直播源列表 +│ ├── 添加直播源按钮 +│ └── 编辑/删除直播源功能 +├── 远程输入配置区域 +│ └── 远程输入开关 +└── 播放源配置区域 + ├── 主播放源选择 + ├── 备用播放源配置 + └── 启用的播放源选择 +``` + +### 4. 组件设计 +- 使用 TV 适配的组件和样式 +- 实现焦点管理和遥控器导航 +- 遵循现有的设计规范(ThemedView, ThemedText, StyledButton) + +### 5. 导航集成 +- 在主页面添加设置入口 +- 使用 Expo Router 的 router.push('/settings') 进行导航 + +## 实施步骤 + +1. **扩展 settingsStore.ts** + - 添加新的状态接口 + - 实现新配置项的增删改查方法 + - 集成本地存储 + +2. **创建设置页面** + - 新建 `app/settings.tsx` + - 实现基础页面结构和导航 + +3. **实现配置组件** + - API 配置组件(复用现有逻辑) + - 直播源配置组件 + - 远程输入开关组件 + - 播放源配置组件 + +4. **集成远程控制** + - 将远程控制功能集成到设置页面 + - 统一管理所有设置项 + +5. **更新导航** + - 在主页面添加设置入口 + - 移除现有的设置弹窗触发逻辑 + +6. **测试验证** + - 测试所有配置项的保存和加载 + - 验证TV平台的交互体验 + - 确保配置项生效 + +## 技术考虑 + +### TV平台适配 +- 使用 `useTVRemoteHandler` 处理遥控器事件 +- 实现合适的焦点管理 +- 确保所有交互元素可通过遥控器操作 + +### 数据持久化 +- 使用现有的 `SettingsManager` 进行本地存储 +- 确保新配置项能正确保存和恢复 + +### 向后兼容 +- 保持现有API配置功能不变 +- 为新配置项提供默认值 +- 处理旧版本设置数据的迁移 + +## 预期收益 + +1. **更好的用户体验**:独立页面提供更多空间展示配置选项 +2. **功能扩展性**:为未来添加更多配置项提供良好基础 +3. **代码组织**:将设置相关功能集中管理 +4. **TV平台适配**:更好的遥控器交互体验 \ No newline at end of file diff --git a/services/remoteControlService.ts b/services/remoteControlService.ts index cbde133..94f196a 100644 --- a/services/remoteControlService.ts +++ b/services/remoteControlService.ts @@ -119,11 +119,6 @@ class RemoteControlService { public async startServer(): Promise { console.log('[RemoteControl] Attempting to start server...'); - if (this.httpServer.getIsRunning()) { - console.log('[RemoteControl] Server is already running.'); - throw new Error('Server is already running.'); - } - try { const url = await this.httpServer.start(); console.log(`[RemoteControl] Server started successfully at: ${url}`); diff --git a/services/tcpHttpServer.ts b/services/tcpHttpServer.ts index 174ba82..515dcd9 100644 --- a/services/tcpHttpServer.ts +++ b/services/tcpHttpServer.ts @@ -96,10 +96,6 @@ class TCPHttpServer { } public async start(): Promise { - if (this.isRunning) { - throw new Error('Server is already running'); - } - const netState = await NetInfo.fetch(); let ipAddress: string | null = null; @@ -111,6 +107,11 @@ class TCPHttpServer { throw new Error('无法获取IP地址,请确认设备已连接到WiFi或以太网。'); } + if (this.isRunning) { + console.log('[TCPHttpServer] Server is already running.'); + return `http://${ipAddress}:${PORT}`; + } + return new Promise((resolve, reject) => { try { this.server = TcpSocket.createServer((socket: TcpSocket.Socket) => { diff --git a/stores/settingsStore.ts b/stores/settingsStore.ts index 5a2df24..4d6ba0e 100644 --- a/stores/settingsStore.ts +++ b/stores/settingsStore.ts @@ -3,11 +3,33 @@ import { SettingsManager } from '@/services/storage'; import { api } from '@/services/api'; import useHomeStore from './homeStore'; +export interface LiveStreamSource { + id: string; + name: string; + url: string; + enabled: boolean; +} + +export interface PlaybackSourceConfig { + primarySource: string; + fallbackSources: string[]; + enabledSources: string[]; +} + interface SettingsState { apiBaseUrl: string; + liveStreamSources: LiveStreamSource[]; + remoteInputEnabled: boolean; + playbackSourceConfig: PlaybackSourceConfig; isModalVisible: boolean; loadSettings: () => Promise; setApiBaseUrl: (url: string) => void; + setLiveStreamSources: (sources: LiveStreamSource[]) => void; + addLiveStreamSource: (source: Omit) => void; + removeLiveStreamSource: (id: string) => void; + updateLiveStreamSource: (id: string, updates: Partial) => void; + setRemoteInputEnabled: (enabled: boolean) => void; + setPlaybackSourceConfig: (config: PlaybackSourceConfig) => void; saveSettings: () => Promise; showModal: () => void; hideModal: () => void; @@ -15,16 +37,60 @@ interface SettingsState { export const useSettingsStore = create((set, get) => ({ apiBaseUrl: 'https://orion-tv.edu.deal', + liveStreamSources: [], + remoteInputEnabled: false, + playbackSourceConfig: { + primarySource: 'default', + fallbackSources: [], + enabledSources: ['default'], + }, isModalVisible: false, loadSettings: async () => { const settings = await SettingsManager.get(); - set({ apiBaseUrl: settings.apiBaseUrl }); + set({ + apiBaseUrl: settings.apiBaseUrl, + liveStreamSources: settings.liveStreamSources || [], + remoteInputEnabled: settings.remoteInputEnabled || false, + playbackSourceConfig: settings.playbackSourceConfig || { + primarySource: 'default', + fallbackSources: [], + enabledSources: ['default'], + }, + }); api.setBaseUrl(settings.apiBaseUrl); }, setApiBaseUrl: (url) => set({ apiBaseUrl: url }), + setLiveStreamSources: (sources) => set({ liveStreamSources: sources }), + addLiveStreamSource: (source) => { + const { liveStreamSources } = get(); + const newSource = { + ...source, + id: Date.now().toString(), + }; + set({ liveStreamSources: [...liveStreamSources, newSource] }); + }, + removeLiveStreamSource: (id) => { + const { liveStreamSources } = get(); + set({ liveStreamSources: liveStreamSources.filter(s => s.id !== id) }); + }, + updateLiveStreamSource: (id, updates) => { + const { liveStreamSources } = get(); + set({ + liveStreamSources: liveStreamSources.map(s => + s.id === id ? { ...s, ...updates } : s + ) + }); + }, + setRemoteInputEnabled: (enabled) => set({ remoteInputEnabled: enabled }), + setPlaybackSourceConfig: (config) => set({ playbackSourceConfig: config }), saveSettings: async () => { - const { apiBaseUrl } = get(); - await SettingsManager.save({ apiBaseUrl }); + const { apiBaseUrl, liveStreamSources, remoteInputEnabled, playbackSourceConfig } = get(); + await SettingsManager.save({ + apiBaseUrl, + liveStreamSources, + remoteInputEnabled, + playbackSourceConfig, + }); api.setBaseUrl(apiBaseUrl); set({ isModalVisible: false }); useHomeStore.getState().fetchInitialData();