feat: Refactor settings management into a dedicated page with new configuration options

This commit is contained in:
zimplexing
2025-07-11 13:49:45 +08:00
parent 7b3fd4b9d5
commit fc8da352fb
13 changed files with 596 additions and 157 deletions

View File

@@ -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": []
}

View File

@@ -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" && <Stack.Screen name="play" options={{ headerShown: false }} />}
<Stack.Screen name="search" options={{ headerShown: false }} />
<Stack.Screen name="live" options={{ headerShown: false }} />
<Stack.Screen name="settings" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<Toast />

View File

@@ -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() {
>
<Search color={colorScheme === "dark" ? "white" : "black"} size={24} />
</StyledButton>
<StyledButton style={styles.searchButton} onPress={showSettingsModal} variant="ghost">
<StyledButton style={styles.searchButton} onPress={() => router.push("/settings")} variant="ghost">
<Settings color={colorScheme === "dark" ? "white" : "black"} size={24} />
</StyledButton>
</View>
@@ -207,7 +203,6 @@ export default function HomeScreen() {
}
/>
)}
<SettingsModal />
</ThemedView>
);
}

98
app/settings.tsx Normal file
View File

@@ -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 (
<ThemedView style={styles.container}>
<View style={styles.header}>
<ThemedText style={styles.title}></ThemedText>
</View>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
<RemoteInputSection onChanged={markAsChanged} />
<APIConfigSection onChanged={markAsChanged} />
<LiveStreamSection onChanged={markAsChanged} />
<PlaySourceSection onChanged={markAsChanged} />
</ScrollView>
<View style={styles.footer}>
<StyledButton
text={isLoading ? "保存中..." : "保存设置"}
onPress={handleSave}
variant="primary"
disabled={!hasChanges || isLoading}
style={[styles.saveButton, (!hasChanges || isLoading) && styles.disabledButton]}
/>
</View>
</ThemedView>
);
}
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,
},
});

View File

@@ -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<TextInput>(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 (
<Modal animationType="fade" transparent={true} visible={isModalVisible} onRequestClose={hideModal}>
<View style={styles.modalContainer}>
<ThemedView style={styles.modalContent}>
<ThemedText style={styles.title}></ThemedText>
<TextInput
ref={inputRef}
style={[styles.input, isInputFocused && styles.inputFocused]}
value={apiBaseUrl}
onChangeText={setApiBaseUrl}
placeholder="输入 API 地址"
placeholderTextColor={colorScheme === "dark" ? "#888" : "#555"}
autoCapitalize="none"
autoCorrect={false}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
/>
<View style={styles.buttonContainer}>
<StyledButton
text="取消"
onPress={hideModal}
style={styles.button}
textStyle={styles.buttonText}
variant="default"
/>
<StyledButton
text="保存"
onPress={handleSave}
style={styles.button}
textStyle={styles.buttonText}
variant="primary"
/>
</View>
</ThemedView>
</View>
</Modal>
);
};

View File

@@ -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<APIConfigSectionProps> = ({ onChanged }) => {
const { apiBaseUrl, setApiBaseUrl } = useSettingsStore();
const [isInputFocused, setIsInputFocused] = useState(false);
const inputRef = useRef<TextInput>(null);
const handleUrlChange = (url: string) => {
setApiBaseUrl(url);
onChanged();
};
return (
<ThemedView style={styles.section}>
<View style={styles.inputContainer}>
<ThemedText style={styles.sectionTitle}>API </ThemedText>
<TextInput
ref={inputRef}
style={[styles.input, isInputFocused && styles.inputFocused]}
value={apiBaseUrl}
onChangeText={handleUrlChange}
placeholder="输入 API 地址"
placeholderTextColor="#888"
autoCapitalize="none"
autoCorrect={false}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
/>
</View>
</ThemedView>
);
};
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,
},
});

View File

@@ -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<LiveStreamSectionProps> = ({ onChanged }) => {
return (
<ThemedView style={styles.section}>
<ThemedText style={styles.sectionTitle}></ThemedText>
<ThemedText style={styles.placeholder}>线</ThemedText>
</ThemedView>
);
};
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',
},
});

View File

@@ -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<PlaybackSourceSectionProps> = ({ onChanged }) => {
return (
<ThemedView style={styles.section}>
<ThemedText style={styles.sectionTitle}></ThemedText>
<ThemedText style={styles.placeholder}>线</ThemedText>
</ThemedView>
);
};
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",
},
});

View File

@@ -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<RemoteInputSectionProps> = ({ onChanged }) => {
const { remoteInputEnabled, setRemoteInputEnabled } = useSettingsStore();
const { isServerRunning, serverUrl, error } = useRemoteControlStore();
const handleToggle = async (enabled: boolean) => {
setRemoteInputEnabled(enabled);
onChanged();
};
return (
<ThemedView style={styles.section}>
<View style={styles.settingItem}>
<View style={styles.settingInfo}>
<ThemedText style={styles.settingName}></ThemedText>
</View>
<Switch
value={remoteInputEnabled}
onValueChange={handleToggle}
trackColor={{ false: "#767577", true: "#007AFF" }}
thumbColor={remoteInputEnabled ? "#ffffff" : "#f4f3f4"}
/>
</View>
{remoteInputEnabled && (
<View style={styles.statusContainer}>
<View style={styles.statusItem}>
<ThemedText style={styles.statusLabel}></ThemedText>
<ThemedText style={[styles.statusValue, { color: isServerRunning ? "#00FF00" : "#FF6B6B" }]}>
{isServerRunning ? "运行中" : "已停止"}
</ThemedText>
</View>
{serverUrl && (
<View style={styles.statusItem}>
<ThemedText style={styles.statusLabel}>访</ThemedText>
<ThemedText style={styles.statusValue}>{serverUrl}</ThemedText>
</View>
)}
{error && (
<View style={styles.statusItem}>
<ThemedText style={styles.statusLabel}></ThemedText>
<ThemedText style={[styles.statusValue, { color: "#FF6B6B" }]}>{error}</ThemedText>
</View>
)}
</View>
)}
</ThemedView>
);
};
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,
},
});

View File

@@ -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平台适配**:更好的遥控器交互体验

View File

@@ -119,11 +119,6 @@ class RemoteControlService {
public async startServer(): Promise<string> {
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}`);

View File

@@ -96,10 +96,6 @@ class TCPHttpServer {
}
public async start(): Promise<string> {
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) => {

View File

@@ -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<void>;
setApiBaseUrl: (url: string) => void;
setLiveStreamSources: (sources: LiveStreamSource[]) => void;
addLiveStreamSource: (source: Omit<LiveStreamSource, 'id'>) => void;
removeLiveStreamSource: (id: string) => void;
updateLiveStreamSource: (id: string, updates: Partial<LiveStreamSource>) => void;
setRemoteInputEnabled: (enabled: boolean) => void;
setPlaybackSourceConfig: (config: PlaybackSourceConfig) => void;
saveSettings: () => Promise<void>;
showModal: () => void;
hideModal: () => void;
@@ -15,16 +37,60 @@ interface SettingsState {
export const useSettingsStore = create<SettingsState>((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();