diff --git a/src/pages/AdminApps.tsx b/src/pages/AdminApps.tsx index a84b54a..2ff0b12 100644 --- a/src/pages/AdminApps.tsx +++ b/src/pages/AdminApps.tsx @@ -1,7 +1,17 @@ -import { useState } from 'react' +import { useState, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' -import { adminAppsApi, AppDefinition, LocalizedText, AppStep, AppButton } from '../api/adminApps' +import { + adminAppsApi, + AppDefinition, + LocalizedText, + AppStep, + AppButton, + exportToRemnawaveFormat, + importFromRemnawaveFormat, + RemnawaveConfig, + importFromRemnawaveFormat as convertRemnawave, +} from '../api/adminApps' // Icons const AppsIcon = () => ( @@ -52,6 +62,37 @@ const StarIcon = ({ filled }: { filled: boolean }) => ( ) +const DownloadIcon = () => ( + + + +) + +const UploadIcon = () => ( + + + +) + +const CloudSyncIcon = () => ( + + + +) + +const RefreshIcon = () => ( + + + +) + +const SettingsIcon = () => ( + + + + +) + const PLATFORM_LABELS: Record = { ios: 'iOS', android: 'Android', @@ -389,12 +430,62 @@ export default function AdminApps() { const [selectedPlatform, setSelectedPlatform] = useState('ios') const [editingApp, setEditingApp] = useState<{ app: AppDefinition; isNew: boolean } | null>(null) const [copyTarget, setCopyTarget] = useState<{ appId: string; platform: string } | null>(null) + const [showImportModal, setShowImportModal] = useState(false) + const [importError, setImportError] = useState(null) + const [importPreview, setImportPreview] = useState<{ platformApps: Record } | null>(null) + const fileInputRef = useRef(null) + const [useRemnaWave, setUseRemnaWave] = useState(false) + const [showRemnaWaveSettings, setShowRemnaWaveSettings] = useState(false) + const [remnaWaveUuidInput, setRemnaWaveUuidInput] = useState('') - const { data: apps, isLoading } = useQuery({ + // Check if RemnaWave config is available + const { data: remnaWaveStatus, refetch: refetchStatus } = useQuery({ + queryKey: ['remnawave-status'], + queryFn: adminAppsApi.getRemnaWaveStatus, + staleTime: 60000, + }) + + // List available RemnaWave configs + 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 when toggle is enabled + const { data: remnaWaveConfig, isLoading: isRemnaWaveLoading, refetch: refetchRemnaWave } = useQuery({ + queryKey: ['remnawave-config'], + queryFn: adminAppsApi.getRemnaWaveConfig, + enabled: useRemnaWave && remnaWaveStatus?.enabled, + staleTime: 0, + }) + + // Convert RemnaWave config to local format + const remnaWaveApps = remnaWaveConfig + ? convertRemnawave(remnaWaveConfig).platformApps[selectedPlatform] || [] + : [] + + const { data: localApps, isLoading: isLocalLoading } = useQuery({ queryKey: ['admin-apps', selectedPlatform], queryFn: () => adminAppsApi.getPlatformApps(selectedPlatform), + enabled: !useRemnaWave, }) + // Select which apps to show based on toggle + const apps = useRemnaWave ? remnaWaveApps : localApps + const isLoading = useRemnaWave ? isRemnaWaveLoading : isLocalLoading + const createMutation = useMutation({ mutationFn: ({ platform, app }: { platform: string; app: AppDefinition }) => adminAppsApi.createApp(platform, app), @@ -438,6 +529,85 @@ export default function AdminApps() { }, }) + // Fetch all platforms for export + const { data: allPlatformApps } = useQuery({ + queryKey: ['admin-apps-all'], + queryFn: async () => { + const result: Record = {} + for (const platform of PLATFORMS) { + try { + result[platform] = await adminAppsApi.getPlatformApps(platform) + } catch { + result[platform] = [] + } + } + return result + }, + }) + + const { data: branding } = useQuery({ + queryKey: ['admin-branding'], + queryFn: adminAppsApi.getBranding, + }) + + // Export handler + const handleExport = () => { + if (!allPlatformApps) return + const remnawaveConfig = exportToRemnawaveFormat(allPlatformApps, branding) + const json = JSON.stringify(remnawaveConfig, null, 2) + const blob = new Blob([json], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `apps-config-${new Date().toISOString().split('T')[0]}.json` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + // Import file handler + const handleFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + setImportError(null) + setImportPreview(null) + + const reader = new FileReader() + reader.onload = (event) => { + try { + const json = JSON.parse(event.target?.result as string) as RemnawaveConfig + const imported = importFromRemnawaveFormat(json) + setImportPreview(imported) + setShowImportModal(true) + } catch (err) { + setImportError(t('admin.apps.importError', 'Invalid JSON file')) + setShowImportModal(true) + } + } + reader.readAsText(file) + // Reset file input + if (fileInputRef.current) fileInputRef.current.value = '' + } + + // Confirm import handler + const handleConfirmImport = async () => { + if (!importPreview) return + try { + for (const [platform, apps] of Object.entries(importPreview.platformApps)) { + for (const app of apps) { + await adminAppsApi.createApp(platform, app) + } + } + queryClient.invalidateQueries({ queryKey: ['admin-apps'] }) + queryClient.invalidateQueries({ queryKey: ['admin-apps-all'] }) + setShowImportModal(false) + setImportPreview(null) + } catch (err) { + setImportError(t('admin.apps.importCreateError', 'Failed to create some apps')) + } + } + const handleMoveUp = (index: number) => { if (!apps || index === 0) return const newOrder = [...apps] @@ -468,11 +638,60 @@ export default function AdminApps() { return (
-
- -

{t('admin.apps.title')}

+
+
+ +

{t('admin.apps.title')}

+
+ + {/* RemnaWave Toggle & Settings */} +
+ {remnaWaveStatus?.enabled && ( + <> + + {useRemnaWave && ( + + )} + + )} + +
+ {/* RemnaWave Info Banner */} + {useRemnaWave && ( +
+ + {t('admin.apps.remnaWaveMode', 'Showing apps from RemnaWave panel. Changes here are read-only.')} +
+ )} + {/* Platform Tabs */}
{PLATFORMS.map((platform) => ( @@ -490,16 +709,44 @@ export default function AdminApps() { ))}
- {/* Add App Button */} -
- -
+ {/* Action Buttons - Only show when not in RemnaWave mode */} + {!useRemnaWave && ( +
+
+ + + +
+ +
+ )} {/* Apps List */} {isLoading ? ( @@ -513,23 +760,25 @@ export default function AdminApps() { key={app.id} className="card flex items-center gap-4 p-4" > - {/* Reorder buttons */} -
- - -
+ {/* Reorder buttons - Only show when not in RemnaWave mode */} + {!useRemnaWave && ( +
+ + +
+ )} {/* App Info */}
@@ -544,30 +793,32 @@ export default function AdminApps() {
{app.urlScheme}
- {/* Actions */} -
- - - -
+ {/* Actions - Only show when not in RemnaWave mode */} + {!useRemnaWave && ( +
+ + + +
+ )}
))}
@@ -624,6 +875,149 @@ export default function AdminApps() { )} + + {/* Import Modal */} + {showImportModal && ( +
+
+

+ {t('admin.apps.importPreview', 'Import Preview')} +

+ + {importError && ( +
+ {importError} +
+ )} + + {importPreview && ( +
+ {Object.entries(importPreview.platformApps).map(([platform, platformApps]) => ( + platformApps.length > 0 && ( +
+

+ {PLATFORM_LABELS[platform] || platform} ({platformApps.length}) +

+
    + {platformApps.map((app, idx) => ( +
  • + {app.isFeatured && } + {app.name} + ({app.urlScheme || '-'}) +
  • + ))} +
+
+ ) + ))} + {Object.values(importPreview.platformApps).every(arr => arr.length === 0) && ( +
+ {t('admin.apps.noAppsToImport', 'No apps to import')} +
+ )} +
+ )} + +
+ + {importPreview && !importError && ( + + )} +
+
+
+ )} + + {/* RemnaWave Settings Modal */} + {showRemnaWaveSettings && ( +
+
+

+ + {t('admin.apps.remnaWaveSettings', 'RemnaWave Settings')} +

+ +
+
+ + 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 && ( +
+ +
+ {availableConfigs.map((config) => ( + + ))} +
+
+ )} +
+ +
+ + +
+
+
+ )}
) }