diff --git a/app/index.tsx b/app/index.tsx index 60d1220..e386cd0 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -128,7 +128,7 @@ export default function HomeScreen() { // TV端和平板端的顶部导航 const renderHeader = () => { - if (deviceType === 'mobile') { + if (deviceType === "mobile") { // 移动端不显示顶部导航,使用底部Tab导航 return null; } @@ -171,7 +171,7 @@ export default function HomeScreen() { const dynamicStyles = StyleSheet.create({ container: { flex: 1, - paddingTop: deviceType === 'mobile' ? insets.top : 40, + paddingTop: deviceType === "mobile" ? 0 : 40, }, headerContainer: { flexDirection: "row", @@ -181,7 +181,7 @@ export default function HomeScreen() { marginBottom: spacing, }, headerTitle: { - fontSize: deviceType === 'mobile' ? 24 : deviceType === 'tablet' ? 28 : 32, + fontSize: deviceType === "mobile" ? 24 : deviceType === "tablet" ? 28 : 32, fontWeight: "bold", paddingTop: 16, }, @@ -200,13 +200,13 @@ export default function HomeScreen() { paddingHorizontal: spacing, }, categoryButton: { - paddingHorizontal: deviceType === 'tv' ? spacing / 4 : spacing / 2, - paddingVertical: deviceType === 'tv' ? spacing / 4 : spacing / 2, - borderRadius: deviceType === 'mobile' ? 6 : 8, - marginHorizontal: deviceType === 'tv' ? spacing / 4 : spacing / 2, + paddingHorizontal: deviceType === "tv" ? spacing / 4 : spacing / 2, + paddingVertical: spacing / 2, + borderRadius: deviceType === "mobile" ? 6 : 8, + marginHorizontal: deviceType === "tv" ? spacing / 4 : spacing / 2, // TV端使用更小的间距 }, categoryText: { - fontSize: deviceType === 'mobile' ? 14 : 16, + fontSize: deviceType === "mobile" ? 14 : 16, fontWeight: "500", }, contentContainer: { @@ -217,8 +217,8 @@ export default function HomeScreen() { const content = ( {/* 状态栏 */} - {deviceType === 'mobile' && } - + {deviceType === "mobile" && } + {/* 顶部导航 */} {renderHeader()} @@ -291,13 +291,9 @@ export default function HomeScreen() { ); // 根据设备类型决定是否包装在响应式导航中 - if (deviceType === 'tv') { + if (deviceType === "tv") { return content; } - return ( - - {content} - - ); -} \ No newline at end of file + return {content}; +} diff --git a/app/search.tsx b/app/search.tsx index 91d1534..9ae719e 100644 --- a/app/search.tsx +++ b/app/search.tsx @@ -26,7 +26,7 @@ export default function SearchScreen() { const [error, setError] = useState(null); const textInputRef = useRef(null); const [isInputFocused, setIsInputFocused] = useState(false); - const { showModal: showRemoteModal, lastMessage } = useRemoteControlStore(); + const { showModal: showRemoteModal, lastMessage, targetPage, clearMessage } = useRemoteControlStore(); const { remoteInputEnabled } = useSettingsStore(); const router = useRouter(); @@ -36,14 +36,15 @@ export default function SearchScreen() { const { deviceType, spacing } = responsiveConfig; useEffect(() => { - if (lastMessage) { + if (lastMessage && targetPage === 'search') { console.log("Received remote input:", lastMessage); const realMessage = lastMessage.split("_")[0]; setKeyword(realMessage); handleSearch(realMessage); + clearMessage(); // Clear the message after processing } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastMessage]); + }, [lastMessage, targetPage]); // useEffect(() => { // // Focus the text input when the screen loads @@ -87,10 +88,10 @@ export default function SearchScreen() { ]); return; } - showRemoteModal(); + showRemoteModal('search'); }; - const renderItem = ({ item, index }: { item: SearchResult; index: number }) => ( + const renderItem = ({ item }: { item: SearchResult; index: number }) => ( { - if (lastMessage) { + if (lastMessage && !targetPage) { const realMessage = lastMessage.split("_")[0]; handleRemoteInput(realMessage); + clearMessage(); // Clear the message after processing } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastMessage]); + }, [lastMessage, targetPage]); const handleRemoteInput = (message: string) => { // Handle remote input based on currently focused section @@ -133,10 +134,8 @@ export default function SettingsScreen() { // ), // key: "videoSource", // }, - Platform.OS === 'android' && { - component: ( - - ), + Platform.OS === "android" && { + component: , key: "update", }, ].filter(Boolean); @@ -144,8 +143,8 @@ export default function SettingsScreen() { // TV遥控器事件处理 - 仅在TV设备上启用 const handleTVEvent = React.useCallback( (event: any) => { - if (deviceType !== 'tv') return; - + if (deviceType !== "tv") return; + if (event.eventType === "down") { const nextIndex = Math.min(currentFocusIndex + 1, sections.length); setCurrentFocusIndex(nextIndex); @@ -160,18 +159,15 @@ export default function SettingsScreen() { [currentFocusIndex, sections.length, deviceType] ); - useTVEventHandler(deviceType === 'tv' ? handleTVEvent : () => {}); + useTVEventHandler(deviceType === "tv" ? handleTVEvent : () => {}); // 动态样式 const dynamicStyles = createResponsiveStyles(deviceType, spacing); const renderSettingsContent = () => ( - + - {deviceType === 'tv' && ( + {deviceType === "tv" && ( 设置 @@ -186,7 +182,7 @@ export default function SettingsScreen() { } return null; }} - keyExtractor={(item) => item ? item.key : 'default'} + keyExtractor={(item) => (item ? item.key : "default")} showsVerticalScrollIndicator={false} contentContainerStyle={dynamicStyles.listContent} /> @@ -198,10 +194,7 @@ export default function SettingsScreen() { onPress={handleSave} variant="primary" disabled={!hasChanges || isLoading} - style={[ - dynamicStyles.saveButton, - (!hasChanges || isLoading) && dynamicStyles.disabledButton - ]} + style={[dynamicStyles.saveButton, (!hasChanges || isLoading) && dynamicStyles.disabledButton]} /> @@ -209,7 +202,7 @@ export default function SettingsScreen() { ); // 根据设备类型决定是否包装在响应式导航中 - if (deviceType === 'tv') { + if (deviceType === "tv") { return renderSettingsContent(); } @@ -222,9 +215,9 @@ export default function SettingsScreen() { } const createResponsiveStyles = (deviceType: string, spacing: number) => { - const isMobile = deviceType === 'mobile'; - const isTablet = deviceType === 'tablet'; - const isTV = deviceType === 'tv'; + const isMobile = deviceType === "mobile"; + const isTablet = deviceType === "tablet"; + const isTV = deviceType === "tv"; const minTouchTarget = DeviceUtils.getMinTouchTargetSize(); return StyleSheet.create({ @@ -243,7 +236,7 @@ const createResponsiveStyles = (deviceType: string, spacing: number) => { fontSize: isMobile ? 24 : isTablet ? 28 : 32, fontWeight: "bold", paddingTop: spacing, - color: 'white', + color: "white", }, scrollView: { flex: 1, @@ -257,7 +250,7 @@ const createResponsiveStyles = (deviceType: string, spacing: number) => { }, saveButton: { minHeight: isMobile ? minTouchTarget : isTablet ? 50 : 50, - width: isMobile ? '100%' : isTablet ? 140 : 120, + width: isMobile ? "100%" : isTablet ? 140 : 120, maxWidth: isMobile ? 280 : undefined, }, disabledButton: { diff --git a/components/UpdateModal.tsx b/components/UpdateModal.tsx index 22c3f2a..78d7416 100644 --- a/components/UpdateModal.tsx +++ b/components/UpdateModal.tsx @@ -1,15 +1,9 @@ -import React from 'react'; -import { - Modal, - View, - Text, - TouchableOpacity, - StyleSheet, - ActivityIndicator, - Platform, -} from 'react-native'; -import { useUpdateStore } from '../stores/updateStore'; -import { Colors } from '../constants/Colors'; +import React from "react"; +import { Modal, View, StyleSheet, ActivityIndicator, Platform } from "react-native"; +import { useUpdateStore } from "../stores/updateStore"; +import { Colors } from "../constants/Colors"; +import { StyledButton } from "./StyledButton"; +import { ThemedText } from "./ThemedText"; export function UpdateModal() { const { @@ -26,9 +20,9 @@ export function UpdateModal() { downloadedPath, } = useUpdateStore(); - const updateButtonRef = React.useRef(null); - const laterButtonRef = React.useRef(null); - const skipButtonRef = React.useRef(null); + const updateButtonRef = React.useRef(null); + const laterButtonRef = React.useRef(null); + const skipButtonRef = React.useRef(null); async function handleUpdate() { if (!downloading && !downloadedPath) { @@ -61,86 +55,59 @@ export function UpdateModal() { if (downloading) { return `下载中 ${downloadProgress}%`; } else if (downloadedPath) { - return '立即安装'; + return "立即安装"; } else { - return '立即更新'; + return "立即更新"; } }; return ( - + - 发现新版本 - + 发现新版本 + - - 当前版本: v{currentVersion} - - - - 新版本: v{remoteVersion} - + 当前版本: v{currentVersion} + + 新版本: v{remoteVersion} {downloading && ( - + - {downloadProgress}% + {downloadProgress}% )} - {error && ( - {error} - )} + {error && {error}} - {downloading && !downloadedPath ? ( ) : ( - {getButtonText()} + {getButtonText()} )} - + {!downloading && !downloadedPath && ( <> - - - 稍后再说 - - + + 稍后再说 + - - - 跳过此版本 - - + + 跳过此版本 + )} @@ -153,27 +120,28 @@ export function UpdateModal() { const styles = StyleSheet.create({ overlay: { flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.7)', - justifyContent: 'center', - alignItems: 'center', + backgroundColor: "rgba(0, 0, 0, 0.7)", + justifyContent: "center", + alignItems: "center", }, container: { backgroundColor: Colors.dark.background, borderRadius: 12, padding: 24, - width: Platform.isTV ? 500 : '90%', + width: Platform.isTV ? 500 : "90%", maxWidth: 500, - alignItems: 'center', + alignItems: "center", }, title: { fontSize: Platform.isTV ? 28 : 24, - fontWeight: 'bold', + fontWeight: "bold", color: Colors.dark.text, marginBottom: 20, + paddingTop: 12, }, versionInfo: { - flexDirection: 'row', - alignItems: 'center', + flexDirection: "row", + alignItems: "center", marginBottom: 24, }, versionText: { @@ -181,8 +149,8 @@ const styles = StyleSheet.create({ color: Colors.dark.text, }, newVersion: { - color: Colors.dark.primary || '#00bb5e', - fontWeight: 'bold', + color: Colors.dark.primary || "#00bb5e", + fontWeight: "bold", }, arrow: { fontSize: Platform.isTV ? 20 : 18, @@ -190,64 +158,44 @@ const styles = StyleSheet.create({ marginHorizontal: 12, }, progressContainer: { - width: '100%', + width: "100%", marginBottom: 20, }, progressBar: { height: 6, backgroundColor: Colors.dark.border, borderRadius: 3, - overflow: 'hidden', + overflow: "hidden", marginBottom: 8, }, progressFill: { - height: '100%', - backgroundColor: Colors.dark.primary || '#00bb5e', + height: "100%", + backgroundColor: Colors.dark.primary || "#00bb5e", }, progressText: { fontSize: Platform.isTV ? 16 : 14, color: Colors.dark.text, - textAlign: 'center', + textAlign: "center", }, errorText: { fontSize: Platform.isTV ? 16 : 14, - color: '#ff4444', + color: "#ff4444", marginBottom: 16, - textAlign: 'center', + textAlign: "center", }, buttonContainer: { - width: '100%', + width: "100%", gap: 12, + justifyContent: "center", // 居中对齐 + alignItems: "center", }, button: { - paddingVertical: Platform.isTV ? 14 : 12, - paddingHorizontal: 24, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - minHeight: Platform.isTV ? 56 : 48, - }, - primaryButton: { - backgroundColor: Colors.dark.primary || '#00bb5e', - }, - secondaryButton: { - backgroundColor: 'transparent', - borderWidth: 1, - borderColor: Colors.dark.border, - }, - textButton: { - backgroundColor: 'transparent', + width: "80%", }, + buttonText: { fontSize: Platform.isTV ? 18 : 16, - fontWeight: '600', - color: '#fff', + fontWeight: "600", + color: "#fff", }, - secondaryButtonText: { - color: Colors.dark.text, - }, - textButtonText: { - color: Colors.dark.text, - fontWeight: 'normal', - }, -}); \ No newline at end of file +}); diff --git a/components/settings/UpdateSection.tsx b/components/settings/UpdateSection.tsx index bb44ffb..3506bee 100644 --- a/components/settings/UpdateSection.tsx +++ b/components/settings/UpdateSection.tsx @@ -6,15 +6,8 @@ import { useUpdateStore } from "@/stores/updateStore"; import { UPDATE_CONFIG } from "@/constants/UpdateConfig"; export function UpdateSection() { - const { - currentVersion, - remoteVersion, - updateAvailable, - downloading, - downloadProgress, - checkForUpdate, - setShowUpdateModal, - } = useUpdateStore(); + const { currentVersion, remoteVersion, updateAvailable, downloading, downloadProgress, checkForUpdate } = + useUpdateStore(); const [checking, setChecking] = React.useState(false); @@ -30,7 +23,7 @@ export function UpdateSection() { return ( 应用更新 - + 当前版本 v{currentVersion} @@ -39,9 +32,7 @@ export function UpdateSection() { {updateAvailable && ( 最新版本 - - v{remoteVersion} - + v{remoteVersion} )} @@ -53,26 +44,13 @@ export function UpdateSection() { )} - + {checking ? ( ) : ( 检查更新 )} - - {updateAvailable && !downloading && ( - setShowUpdateModal(true)} - style={[styles.button, styles.updateButton]} - > - 立即更新 - - )} {UPDATE_CONFIG.AUTO_CHECK && ( @@ -99,6 +77,7 @@ const styles = StyleSheet.create({ fontSize: Platform.isTV ? 24 : 20, fontWeight: "bold", marginBottom: 16, + paddingTop: 8, }, row: { flexDirection: "row", @@ -121,12 +100,16 @@ const styles = StyleSheet.create({ flexDirection: "row", gap: 12, marginTop: 16, + justifyContent: "center", // 居中对齐 + alignItems: "center", }, button: { - flex: 1, - }, - updateButton: { - backgroundColor: "#00bb5e", + width: "90%", + ...(Platform.isTV && { + // TV平台焦点样式 + borderWidth: 2, + borderColor: "transparent", + }), }, buttonText: { color: "#ffffff", @@ -139,4 +122,4 @@ const styles = StyleSheet.create({ marginTop: 12, textAlign: "center", }, -}); \ No newline at end of file +}); diff --git a/constants/UpdateConfig.ts b/constants/UpdateConfig.ts index 735d76c..083b8e6 100644 --- a/constants/UpdateConfig.ts +++ b/constants/UpdateConfig.ts @@ -7,7 +7,7 @@ export const UPDATE_CONFIG = { // GitHub相关URL GITHUB_RAW_URL: 'https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json', - GITHUB_RELEASE_URL_TEMPLATE: 'https://github.com/zimplexing/OrionTV/releases/download/v{version}/app-release.apk', + GITHUB_RELEASE_URL_TEMPLATE: 'https://github.com/zimplexing/OrionTV/releases/download/v{version}/orionTV.{version}.apk', // 是否显示更新日志 SHOW_RELEASE_NOTES: true, diff --git a/package.json b/package.json index e0afc5b..639ad43 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "OrionTV", "private": true, "main": "expo-router/entry", - "version": "1.2.11", + "version": "1.2.9", "scripts": { "start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start", "start-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start", diff --git a/services/updateService.ts b/services/updateService.ts index 46eb1eb..0505753 100644 --- a/services/updateService.ts +++ b/services/updateService.ts @@ -1,6 +1,7 @@ import ReactNativeBlobUtil from "react-native-blob-util"; import FileViewer from "react-native-file-viewer"; import { version as currentVersion } from "../package.json"; +import { UPDATE_CONFIG } from "../constants/UpdateConfig"; interface VersionInfo { version: string; @@ -18,75 +19,168 @@ class UpdateService { } async checkVersion(): Promise { - try { - const response = await fetch( - "https://raw.githubusercontent.com/zimplexing/OrionTV/refs/heads/master/package.json" - ); + let retries = 0; + const maxRetries = 3; + + while (retries < maxRetries) { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 + + const response = await fetch(UPDATE_CONFIG.GITHUB_RAW_URL, { + signal: controller.signal, + }); + + clearTimeout(timeoutId); - if (!response.ok) { - throw new Error("Failed to fetch version info"); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: Failed to fetch version info`); + } + + const remotePackage = await response.json(); + const remoteVersion = remotePackage.version; + + return { + version: remoteVersion, + downloadUrl: UPDATE_CONFIG.GITHUB_RELEASE_URL_TEMPLATE.replace(/{version}/g, remoteVersion), + }; + } catch (error) { + retries++; + console.error(`Error checking version (attempt ${retries}/${maxRetries}):`, error); + + if (retries === maxRetries) { + throw error; + } + + // 等待一段时间后重试 + await new Promise(resolve => setTimeout(resolve, 2000 * retries)); } + } + + throw new Error("Maximum retry attempts exceeded"); + } - const remotePackage = await response.json(); - const remoteVersion = remotePackage.version; - - return { - version: remoteVersion, - downloadUrl: `https://github.com/zimplexing/OrionTV/releases/download/v${remoteVersion}/orionTV. -${remoteVersion}.apk`, - }; + // 清理旧的APK文件 + private async cleanOldApkFiles(): Promise { + try { + const { dirs } = ReactNativeBlobUtil.fs; + // 使用DocumentDir而不是DownloadDir + const files = await ReactNativeBlobUtil.fs.ls(dirs.DocumentDir); + + // 查找所有OrionTV APK文件 + const apkFiles = files.filter(file => file.startsWith('OrionTV_v') && file.endsWith('.apk')); + + // 保留最新的2个文件,删除其他的 + if (apkFiles.length > 2) { + const sortedFiles = apkFiles.sort((a, b) => { + // 从文件名中提取时间戳进行排序 + const timeA = a.match(/OrionTV_v(\d+)\.apk/)?.[1] || '0'; + const timeB = b.match(/OrionTV_v(\d+)\.apk/)?.[1] || '0'; + return parseInt(timeB) - parseInt(timeA); + }); + + // 删除旧文件 + const filesToDelete = sortedFiles.slice(2); + for (const file of filesToDelete) { + try { + await ReactNativeBlobUtil.fs.unlink(`${dirs.DocumentDir}/${file}`); + console.log(`Cleaned old APK file: ${file}`); + } catch (deleteError) { + console.warn(`Failed to delete old APK file ${file}:`, deleteError); + } + } + } } catch (error) { - console.error("Error checking version:", error); - throw error; + console.warn('Failed to clean old APK files:', error); } } async downloadApk(url: string, onProgress?: (progress: number) => void): Promise { - try { - const { dirs } = ReactNativeBlobUtil.fs; - const fileName = `OrionTV_v${new Date().getTime()}.apk`; - const filePath = `${dirs.DownloadDir}/${fileName}`; + let retries = 0; + const maxRetries = 3; + + // 清理旧文件 + await this.cleanOldApkFiles(); + + while (retries < maxRetries) { + try { + const { dirs } = ReactNativeBlobUtil.fs; + const timestamp = new Date().getTime(); + const fileName = `OrionTV_v${timestamp}.apk`; + // 使用应用的外部文件目录,而不是系统下载目录 + const filePath = `${dirs.DocumentDir}/${fileName}`; - const task = ReactNativeBlobUtil.config({ - fileCache: true, - path: filePath, - addAndroidDownloads: { - useDownloadManager: true, - notification: true, - title: "OrionTV 更新下载中", - description: "正在下载新版本...", - mime: "application/vnd.android.package-archive", - mediaScannable: true, - }, - }).fetch("GET", url); + const task = ReactNativeBlobUtil.config({ + fileCache: true, + path: filePath, + timeout: UPDATE_CONFIG.DOWNLOAD_TIMEOUT, + // 移除 addAndroidDownloads 配置,避免使用系统下载管理器 + // addAndroidDownloads: { + // useDownloadManager: true, + // notification: true, + // title: UPDATE_CONFIG.NOTIFICATION.TITLE, + // description: UPDATE_CONFIG.NOTIFICATION.DOWNLOADING_TEXT, + // mime: "application/vnd.android.package-archive", + // mediaScannable: true, + // }, + }).fetch("GET", url); - // 监听下载进度 - if (onProgress) { - task.progress((received: string, total: string) => { - const receivedNum = parseInt(received, 10); - const totalNum = parseInt(total, 10); - const progress = Math.floor((receivedNum / totalNum) * 100); - onProgress(progress); - }); + // 监听下载进度 + if (onProgress) { + task.progress((received: string, total: string) => { + const receivedNum = parseInt(received, 10); + const totalNum = parseInt(total, 10); + const progress = Math.floor((receivedNum / totalNum) * 100); + onProgress(progress); + }); + } + + const res = await task; + console.log(`APK downloaded successfully: ${filePath}`); + return res.path(); + } catch (error) { + retries++; + console.error(`Error downloading APK (attempt ${retries}/${maxRetries}):`, error); + + if (retries === maxRetries) { + throw new Error(`Download failed after ${maxRetries} attempts: ${error}`); + } + + // 等待一段时间后重试 + await new Promise(resolve => setTimeout(resolve, 3000 * retries)); } - - const res = await task; - return res.path(); - } catch (error) { - console.error("Error downloading APK:", error); - throw error; } + + throw new Error("Maximum retry attempts exceeded for download"); } async installApk(filePath: string): Promise { try { + // 首先检查文件是否存在 + const exists = await ReactNativeBlobUtil.fs.exists(filePath); + if (!exists) { + throw new Error(`APK file not found: ${filePath}`); + } + + // 使用FileViewer打开APK文件进行安装 + // 这会触发Android的包安装器 await FileViewer.open(filePath, { - showOpenWithDialog: false, - showAppsSuggestions: false, + showOpenWithDialog: true, // 显示选择应用对话框 + showAppsSuggestions: true, // 显示应用建议 displayName: "OrionTV Update", }); } catch (error) { console.error("Error installing APK:", error); + + // 提供更详细的错误信息 + if (error instanceof Error) { + if (error.message.includes('No app found')) { + throw new Error('未找到可安装APK的应用,请确保允许安装未知来源的应用'); + } else if (error.message.includes('permission')) { + throw new Error('没有安装权限,请在设置中允许此应用安装未知来源的应用'); + } + } + throw error; } } diff --git a/stores/remoteControlStore.ts b/stores/remoteControlStore.ts index 01547c4..f196484 100644 --- a/stores/remoteControlStore.ts +++ b/stores/remoteControlStore.ts @@ -8,10 +8,12 @@ interface RemoteControlState { startServer: () => Promise; stopServer: () => void; isModalVisible: boolean; - showModal: () => void; + showModal: (targetPage?: string) => void; hideModal: () => void; lastMessage: string | null; - setMessage: (message: string) => void; + targetPage: string | null; + setMessage: (message: string, targetPage?: string) => void; + clearMessage: () => void; } export const useRemoteControlStore = create((set, get) => ({ @@ -20,6 +22,7 @@ export const useRemoteControlStore = create((set, get) => ({ error: null, isModalVisible: false, lastMessage: null, + targetPage: null, startServer: async () => { if (get().isServerRunning) { @@ -28,7 +31,9 @@ export const useRemoteControlStore = create((set, get) => ({ remoteControlService.init({ onMessage: (message: string) => { console.log('[RemoteControlStore] Received message:', message); - set({ lastMessage: message }); + const currentState = get(); + // Use the current targetPage from the store + set({ lastMessage: message, targetPage: currentState.targetPage }); }, onHandshake: () => { console.log('[RemoteControlStore] Handshake successful'); @@ -53,10 +58,14 @@ export const useRemoteControlStore = create((set, get) => ({ } }, - showModal: () => set({ isModalVisible: true }), - hideModal: () => set({ isModalVisible: false }), + showModal: (targetPage?: string) => set({ isModalVisible: true, targetPage }), + hideModal: () => set({ isModalVisible: false, targetPage: null }), - setMessage: (message: string) => { - set({ lastMessage: `${message}_${Date.now()}` }); + setMessage: (message: string, targetPage?: string) => { + set({ lastMessage: `${message}_${Date.now()}`, targetPage }); + }, + + clearMessage: () => { + set({ lastMessage: null, targetPage: null }); }, })); \ No newline at end of file