import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import DOMPurify from 'dompurify'; import { adminAppsApi, LocalizedText, RemnawaveApp, RemnawaveSvgItem, RemnawaveBlock, RemnawaveButton, RemnawaveBaseSettings, RemnawaveBaseTranslations, } from '../api/adminApps'; import { useBackButton } from '../platform/hooks/useBackButton'; import { usePlatform } from '../platform/hooks/usePlatform'; // Icons const BackIcon = () => ( ); const AppsIcon = () => ( ); const StarIcon = ({ filled }: { filled: boolean }) => ( ); const CloudSyncIcon = () => ( ); const RefreshIcon = () => ( ); const SettingsIcon = () => ( ); const ChevronDownIcon = () => ( ); const PLATFORM_LABELS: Record = { ios: 'iOS', android: 'Android', macos: 'macOS', windows: 'Windows', linux: 'Linux', androidTV: 'Android TV', appleTV: 'Apple TV', }; const PLATFORMS = ['ios', 'android', 'macos', 'windows', 'linux', 'androidTV', 'appleTV']; function getLocalizedText(text: LocalizedText | undefined, lang: string): string { if (!text) return ''; return ( text[lang as keyof LocalizedText] || text['en'] || text['ru'] || Object.values(text).find((v) => v) || '' ); } function renderSvgIcon(svgLibrary: Record, key?: string, color?: string) { if (!key || !svgLibrary[key]?.svgString) return null; const sanitized = DOMPurify.sanitize(svgLibrary[key].svgString, { USE_PROFILES: { svg: true, svgFilters: true }, }); return ( ); } function BaseSettingsPanel({ settings }: { settings: RemnawaveBaseSettings }) { const { t } = useTranslation(); return ( {t('admin.apps.baseSettings', 'Base Settings')} {t('admin.apps.tutorialButton', 'Tutorial button')}: {settings.isShowTutorialButton ? 'ON' : 'OFF'} {settings.tutorialUrl && ( URL: {settings.tutorialUrl} )} ); } function BaseTranslationsPanel({ translations, lang, }: { translations: RemnawaveBaseTranslations; lang: string; }) { const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); const keys: (keyof RemnawaveBaseTranslations)[] = [ 'installApp', 'addSubscription', 'connectAndUse', 'copyLink', 'openApp', 'tutorial', 'close', ]; return ( setExpanded(!expanded)} className="flex w-full items-center justify-between p-4 text-left" > {t('admin.apps.baseTranslations', 'Base Translations')} {expanded && ( Key Value {keys.map((key) => ( {key} {getLocalizedText(translations[key], lang)} ))} )} ); } function AppCard({ app, svgLibrary, lang, }: { app: RemnawaveApp; svgLibrary: Record; lang: string; }) { const [expanded, setExpanded] = useState(false); const firstBlock = app.blocks?.[0]; return ( setExpanded(!expanded)} className="flex w-full items-center gap-4 p-4 text-left" > {/* Icon from first block */} {renderSvgIcon(svgLibrary, firstBlock?.svgIconKey, firstBlock?.svgIconColor) || ( )} {/* Name + badges */} {app.name} {app.featured && } {app.isNeedBase64Encoding && ( Base64 )} {app.urlScheme && ( {app.urlScheme} )} {/* Expand indicator */} {/* Expanded blocks */} {expanded && ( {app.blocks?.map((block: RemnawaveBlock, idx: number) => ( ))} {(!app.blocks || app.blocks.length === 0) && ( No blocks )} )} ); } function BlockCard({ block, index, svgLibrary, lang, }: { block: RemnawaveBlock; index: number; svgLibrary: Record; lang: string; }) { const title = getLocalizedText(block.title, lang); const description = getLocalizedText(block.description, lang); const iconColor = block.svgIconColor || '#8B5CF6'; return ( {renderSvgIcon(svgLibrary, block.svgIconKey, block.svgIconColor) || ( {index + 1} )} {title && {title}} {description && {description}} {block.buttons && block.buttons.length > 0 && ( {block.buttons.map((btn: RemnawaveButton, btnIdx: number) => ( {btn.svgIconKey && renderSvgIcon(svgLibrary, btn.svgIconKey)} {getLocalizedText(btn.text, lang)} {btn.type && ( {btn.type} )} ))} )} ); } export default function AdminApps() { const { t, i18n } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { capabilities } = usePlatform(); const lang = i18n.language || 'en'; useBackButton(() => navigate('/admin')); const [selectedPlatform, setSelectedPlatform] = useState('ios'); const [showRemnaWaveSettings, setShowRemnaWaveSettings] = useState(false); const [remnaWaveUuidInput, setRemnaWaveUuidInput] = useState(''); // RemnaWave status const { data: remnaWaveStatus, refetch: refetchStatus } = useQuery({ queryKey: ['remnawave-status'], queryFn: adminAppsApi.getRemnaWaveStatus, staleTime: 60000, }); // List available RemnaWave configs (for settings modal) const { data: availableConfigs, isLoading: isLoadingConfigs } = useQuery({ queryKey: ['remnawave-configs-list'], queryFn: adminAppsApi.listRemnaWaveConfigs, enabled: showRemnaWaveSettings, staleTime: 30000, }); // Mutation for setting UUID const setUuidMutation = useMutation({ mutationFn: adminAppsApi.setRemnaWaveUuid, onSuccess: () => { refetchStatus(); queryClient.invalidateQueries({ queryKey: ['remnawave-config'] }); setShowRemnaWaveSettings(false); }, }); // Fetch RemnaWave config const { data: remnaWaveConfig, isLoading, refetch: refetchRemnaWave, } = useQuery({ queryKey: ['remnawave-config'], queryFn: adminAppsApi.getRemnaWaveConfig, enabled: !!remnaWaveStatus?.enabled, staleTime: 0, }); // Extract data from raw RemnaWave config const currentPlatformApps = remnaWaveConfig?.platforms?.[selectedPlatform]?.apps || []; const svgLibrary = remnaWaveConfig?.svgLibrary || {}; const baseSettings = remnaWaveConfig?.baseSettings; const baseTranslations = remnaWaveConfig?.baseTranslations; return ( {/* Header */} {!capabilities.hasBackButton && ( navigate('/admin')} className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600" > )} {t('admin.apps.title')} {remnaWaveStatus?.enabled && ( refetchRemnaWave()} className="rounded-lg p-2 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200" title={t('admin.apps.refreshRemnaWave', 'Refresh from RemnaWave')} > )} { setRemnaWaveUuidInput(remnaWaveStatus?.config_uuid || ''); setShowRemnaWaveSettings(true); }} className="rounded-lg p-2 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200" title={t('admin.apps.remnaWaveSettings', 'RemnaWave Settings')} > {/* RemnaWave Info Banner */} {remnaWaveStatus?.enabled && ( {t('admin.apps.remnaWaveMode', 'Showing apps from RemnaWave panel. Read-only mode.')} )} {/* Base Settings */} {baseSettings && } {/* Platform Tabs */} {PLATFORMS.map((platform) => ( setSelectedPlatform(platform)} className={`rounded-lg px-4 py-2 text-sm font-medium transition-colors ${ selectedPlatform === platform ? 'bg-accent-500 text-white' : 'bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > {PLATFORM_LABELS[platform]} ))} {/* Apps List */} {isLoading ? ( ) : currentPlatformApps.length > 0 ? ( {currentPlatformApps.map((app: RemnawaveApp, index: number) => ( ))} ) : ( {t('admin.apps.noApps')} )} {/* Base Translations */} {baseTranslations && } {/* RemnaWave Settings Modal */} {showRemnaWaveSettings && ( {t('admin.apps.remnaWaveSettings', 'RemnaWave Settings')} {t('admin.apps.remnaWaveConfigUuid', 'Config UUID')} setRemnaWaveUuidInput(e.target.value)} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" className="input w-full font-mono text-sm" /> {t( 'admin.apps.remnaWaveConfigUuidHint', 'UUID of subscription page config from RemnaWave panel', )} {/* Available configs from RemnaWave */} {isLoadingConfigs ? ( ) : ( availableConfigs && availableConfigs.length > 0 && ( {t('admin.apps.availableConfigs', 'Available configs')} {availableConfigs.map((config) => ( setRemnaWaveUuidInput(config.uuid)} className={`w-full rounded-lg border p-3 text-left transition-colors ${ remnaWaveUuidInput === config.uuid ? 'border-accent-500 bg-accent-500/10' : 'border-dark-700 bg-dark-800/50 hover:border-dark-600' }`} > {config.name} {config.uuid} ))} ) )} setShowRemnaWaveSettings(false)} className="btn-secondary"> {t('common.cancel')} setUuidMutation.mutate(remnaWaveUuidInput || null)} disabled={setUuidMutation.isPending} className="btn-primary" > {setUuidMutation.isPending ? t('common.loading') : t('common.save')} )} ); }
No blocks
{t('admin.apps.noApps')}
{t( 'admin.apps.remnaWaveConfigUuidHint', 'UUID of subscription page config from RemnaWave panel', )}