import { Alert, Box, FormControl, FormControlLabel, InputLabel, MenuItem, Select, Switch, TextField, Typography } from '@mui/material'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import axios from 'axios'; import React, { useEffect, useState } from 'react'; import { useLanguage } from '../../contexts/LanguageContext'; import PasswordModal from '../PasswordModal'; const API_URL = import.meta.env.VITE_API_URL; interface GeneralSettingsProps { language: string; websiteName?: string; itemsPerPage?: number; showYoutubeSearch?: boolean; visitorMode?: boolean; savedVisitorMode?: boolean; infiniteScroll?: boolean; videoColumns?: number; cloudflaredTunnelEnabled?: boolean; cloudflaredToken?: string; onChange: (field: string, value: string | number | boolean) => void; } const GeneralSettings: React.FC = (props) => { const { language, websiteName, showYoutubeSearch, visitorMode, savedVisitorMode, infiniteScroll, videoColumns, cloudflaredTunnelEnabled, cloudflaredToken, onChange } = props; const { t } = useLanguage(); const queryClient = useQueryClient(); const [showPasswordModal, setShowPasswordModal] = useState(false); const [passwordError, setPasswordError] = useState(''); const [isVerifyingPassword, setIsVerifyingPassword] = useState(false); const [pendingVisitorMode, setPendingVisitorMode] = useState(null); const [remainingWaitTime, setRemainingWaitTime] = useState(0); const [baseError, setBaseError] = useState(''); // Poll for Cloudflare Tunnel status const { data: cloudflaredStatus } = useQuery({ queryKey: ['cloudflaredStatus'], queryFn: async () => { if (!cloudflaredTunnelEnabled) return null; const res = await axios.get(`${API_URL}/settings/cloudflared/status`); return res.data; }, enabled: !!cloudflaredTunnelEnabled, refetchInterval: 5000 // Poll every 5 seconds }); // Use saved value for visibility, current value for toggle state const isVisitorMode = savedVisitorMode ?? visitorMode ?? false; const handleVisitorModeChange = (checked: boolean) => { setPendingVisitorMode(checked); setPasswordError(''); setBaseError(''); setRemainingWaitTime(0); setShowPasswordModal(true); }; const handlePasswordConfirm = async (password: string) => { setIsVerifyingPassword(true); setPasswordError(''); setBaseError(''); try { await axios.post(`${API_URL}/settings/verify-password`, { password }); // If successful, save the setting immediately if (pendingVisitorMode !== null) { // Save to backend await axios.post(`${API_URL}/settings`, { visitorMode: pendingVisitorMode }); // Invalidate settings query to ensure global state (VisitorModeContext) updates immediately await queryClient.invalidateQueries({ queryKey: ['settings'] }); // Update parent state onChange('visitorMode', pendingVisitorMode); } setShowPasswordModal(false); setPendingVisitorMode(null); } catch (error: any) { console.error('Password verification failed:', error); if (error.response) { const { status, data } = error.response; if (status === 429) { const waitTimeMs = data.waitTime || 0; const seconds = Math.ceil(waitTimeMs / 1000); setRemainingWaitTime(seconds); setBaseError(t('tooManyAttempts') || 'Too many attempts.'); } else if (status === 401) { const waitTimeMs = data.waitTime || 0; if (waitTimeMs > 0) { const seconds = Math.ceil(waitTimeMs / 1000); setRemainingWaitTime(seconds); setBaseError(t('incorrectPassword') || 'Incorrect password.'); } else { setPasswordError(t('incorrectPassword') || 'Incorrect password'); } } else { setPasswordError(t('loginFailed') || 'Verification failed'); } } else { setPasswordError(t('networkError' as any) || 'Network error'); } } finally { setIsVerifyingPassword(false); } }; const handleClosePasswordModal = () => { setShowPasswordModal(false); setPendingVisitorMode(null); setPasswordError(''); setBaseError(''); setRemainingWaitTime(0); }; // Effect to handle countdown useEffect(() => { let interval: NodeJS.Timeout; if (remainingWaitTime > 0) { // Update error message immediately const waitMsg = t('waitTimeMessage')?.replace('{time}', `${remainingWaitTime}s`) || `Please wait ${remainingWaitTime}s.`; setPasswordError(`${baseError} ${waitMsg}`); interval = setInterval(() => { setRemainingWaitTime((prev) => { if (prev <= 1) { // Countdown finished setPasswordError(baseError); return 0; } return prev - 1; }); }, 1000); } else if (baseError && !passwordError) { // Restore base error if countdown finished but no explicit error set (though logic above handles it) // simplified: if remainingTime hits 0, the effect re-runs. // We handled the 0 case in the setRemainingWaitTime callback or we can handle it here if it transitions to 0. } return () => { if (interval) clearInterval(interval); }; }, [remainingWaitTime, baseError, t]); return ( {t('general')} {!isVisitorMode && ( <> {t('language')} onChange('websiteName', e.target.value)} placeholder="MyTube" helperText={t('websiteNameHelper', { current: (websiteName || '').length, max: 15, default: 'MyTube' })} slotProps={{ htmlInput: { maxLength: 15 } }} /> { const val = parseInt(e.target.value); if (!isNaN(val) && val > 0) { onChange('itemsPerPage', val); } }} disabled={infiniteScroll ?? false} helperText={ infiniteScroll ? t('infiniteScrollDisabled') || "Disabled when Infinite Scroll is enabled" : (t('itemsPerPageHelper') || "Number of videos to show per page (Default: 12)") } slotProps={{ htmlInput: { min: 1 } }} /> {t('maxVideoColumns') || 'Maximum Video Columns (Homepage)'} onChange('infiniteScroll', e.target.checked)} /> } label={t('infiniteScroll') || "Infinite Scroll"} /> onChange('showYoutubeSearch', e.target.checked)} /> } label={t('showYoutubeSearch') || "Show YouTube Search Results"} /> )} {t('cloudflaredTunnel')} onChange('cloudflaredTunnelEnabled', e.target.checked)} /> } label={t('enableCloudflaredTunnel')} /> {(cloudflaredTunnelEnabled) && ( onChange('cloudflaredToken', e.target.value)} margin="normal" helperText={t('cloudflaredTokenHelper') || "Paste your tunnel token here, or leave empty to use a random Quick Tunnel."} /> )} {cloudflaredTunnelEnabled && cloudflaredStatus && ( Status: {cloudflaredStatus.isRunning ? 'Running' : 'Stopped'} {cloudflaredStatus.tunnelId && ( Tunnel ID: {cloudflaredStatus.tunnelId} )} {cloudflaredStatus.accountTag && ( Account Tag: {cloudflaredStatus.accountTag} )} {cloudflaredStatus.publicUrl && ( Public URL: {cloudflaredStatus.publicUrl} Quick Tunnel URLs change every time the tunnel restarts. )} {!cloudflaredStatus.publicUrl && ( Public hostname is managed in your Cloudflare Zero Trust Dashboard. )} )} handleVisitorModeChange(e.target.checked)} /> } label={t('visitorMode') || "Visitor Mode (Read-only)"} /> {t('visitorModeDescription') || "Read-only mode. Hidden videos will not be visible to visitors."} ); }; export default GeneralSettings;