'use client'; import React from 'react'; import { Button } from '@/components/ui/button'; import { TextInput } from '@/components/ui/text-input'; import { useUserData } from '@/context/userData'; import { UserConfigAPI } from '@/services/api'; import { PageWrapper } from '@/components/shared/page-wrapper'; import { Alert } from '@/components/ui/alert'; import { SettingsCard } from '../shared/settings-card'; import { toast } from 'sonner'; import { CopyIcon, DownloadIcon, PlusIcon, UploadIcon } from 'lucide-react'; import { useStatus } from '@/context/status'; import { BiCopy } from 'react-icons/bi'; import { PageControls } from '../shared/page-controls'; import { useDisclosure } from '@/hooks/disclosure'; import { Modal } from '../ui/modal'; import { Switch } from '../ui/switch'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '../ui/accordion'; import { PasswordInput } from '../ui/password-input'; export function SaveInstallMenu() { return ( <> > ); } function Content() { const { userData, setUserData, uuid, setUuid, password, setPassword, encryptedPassword, setEncryptedPassword, } = useUserData(); const [newPassword, setNewPassword] = React.useState(''); const [loading, setLoading] = React.useState(false); const [passwordRequirements, setPasswordRequirements] = React.useState< string[] >([]); const { status } = useStatus(); const baseUrl = status?.settings?.baseUrl || window.location.origin; const importFileRef = React.useRef(null); const installModal = useDisclosure(false); const passwordModal = useDisclosure(false); const [filterCredentialsInExport, setFilterCredentialsInExport] = React.useState(false); React.useEffect(() => { const requirements: string[] = []; // already created a config if (uuid && password) { setPasswordRequirements([]); return; } if (newPassword.length < 6) { requirements.push('Password must be at least 6 characters long'); } setPasswordRequirements(requirements); }, [newPassword, uuid, password]); const handleSave = async ( e?: React.FormEvent, authenticated: boolean = false ) => { e?.preventDefault(); if ( status?.settings.protected && !authenticated && !userData.addonPassword ) { passwordModal.open(); return; } if (passwordRequirements.length > 0) { toast.error('Password requirements not met'); return; } setLoading(true); try { const result = uuid ? await UserConfigAPI.updateConfig(uuid, userData, password!) : await UserConfigAPI.createConfig(userData, newPassword); if (!result.success) { if (result.error?.code === 'USER_INVALID_PASSWORD') { toast.error('Your addon password is incorrect'); setUserData((prev) => ({ ...prev, addonPassword: '', })); passwordModal.open(); return; } throw new Error( result.error?.message || 'Failed to save configuration' ); } if (!uuid && result.data) { toast.success( 'Configuration created successfully, your UUID and password are below' ); setUuid(result.data.uuid); setEncryptedPassword(result.data.encryptedPassword); setPassword(newPassword); } else if (uuid && result.success) { toast.success('Configuration updated successfully'); } if (authenticated) { passwordModal.close(); } } catch (err) { toast.error( err instanceof Error ? err.message : 'Failed to save configuration' ); if (authenticated) { passwordModal.close(); } } finally { setLoading(false); } }; const handleImport = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const json = JSON.parse(event.target?.result as string); // const validate = UserDataSchema.safeParse(json); // if (!validate.success) { // toast.error('Failed to import configuration: Invalid JSON file'); // return; // } setUserData((prev) => ({ ...prev, ...json, })); toast.success('Configuration imported successfully'); } catch (err) { toast.error('Failed to import configuration: Invalid JSON file'); } }; reader.readAsText(file); }; const handleExport = () => { try { const dataStr = JSON.stringify( { ...userData, uuid: filterCredentialsInExport ? undefined : userData.uuid, tmdbAccessToken: filterCredentialsInExport ? undefined : userData.tmdbAccessToken, rpdbApiKey: filterCredentialsInExport ? undefined : userData.rpdbApiKey, addonPassword: filterCredentialsInExport ? undefined : userData.addonPassword, services: userData?.services?.map((service) => ({ ...service, credentials: filterCredentialsInExport ? {} : service.credentials, })), proxy: { ...userData?.proxy, credentials: filterCredentialsInExport ? undefined : userData?.proxy?.credentials, url: filterCredentialsInExport ? undefined : userData?.proxy?.url, }, presets: userData?.presets?.map((preset) => { const presetMeta = status?.settings.presets.find( (p) => p.ID === preset.type ); return { ...preset, options: filterCredentialsInExport ? Object.fromEntries( Object.entries(preset.options || {}).filter(([key]) => { const optionMeta = presetMeta?.OPTIONS?.find( (opt) => opt.id === key ); return optionMeta?.type !== 'password'; }) ) : preset.options, }; }), }, null, 2 ); const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'aiostreams-config.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err) { toast.error('Failed to export configuration'); } }; const manifestUrl = `${baseUrl}/stremio/${uuid}/${encryptedPassword}/manifest.json`; const encodedManifest = encodeURIComponent(manifestUrl); const copyManifestUrl = async () => { try { if (!navigator.clipboard) { toast.error( 'The Clipboard API is not supported on this browser or context, please manually copy the URL' ); return; } await navigator.clipboard.writeText(manifestUrl); toast.success('Manifest URL copied to clipboard'); } catch (err) { toast.error('Failed to copy manifest URL'); } }; return ( <> Install Addon Configure and install your personalized Stremio addon {!uuid ? ( {passwordRequirements.length > 0 && newPassword?.length > 0 && ( {passwordRequirements.map((requirement) => ( {requirement} ))} } /> )} setNewPassword(value)} placeholder="Enter a password to protect your configuration" required autoComplete="new-password" /> This is the password you will use to access and update your configuration later. You cannot change this or reset the password once set, so please choose wisely, and remember it. Create ) : ( <> Your UUID: {uuid} { navigator.clipboard.writeText(uuid); toast.success('UUID copied to clipboard'); }} /> Save your UUID and password - you'll need them to update your configuration later } className="flex-1" /> Save {/* window.open( `stremio://${baseUrl.replace(/^https?:\/\//, '')}/stremio/${uuid}/${encryptedPassword}/manifest.json` ) } > Stremio Desktop window.open( `https://web.stremio.com/#/addons?addon=${encodedManifest}` ) } > Stremio Web Copy URL */} Install window.open( `stremio://${baseUrl.replace(/^https?:\/\//, '')}/stremio/${uuid}/${encryptedPassword}/manifest.json` ) } intent="primary" className="w-full" > Stremio window.open( `https://web.stremio.com/#/addons?addon=${encodedManifest}` ) } intent="primary" className="w-full" > Stremio Web Copy URL > )} { e.preventDefault(); handleSave(e, true); }} > setUserData((prev) => ({ ...prev, addonPassword: value, })) } /> Save } intent="gray" > Export } type="button" className="cursor-pointer" onClick={() => importFileRef.current?.click()} > Import Export Settings setFilterCredentialsInExport(value) } side="right" label="Exclude Credentials" /> > ); }
Configure and install your personalized Stremio addon
This is the password you will use to access and update your configuration later. You cannot change this or reset the password once set, so please choose wisely, and remember it.
Save your UUID and password - you'll need them to update your configuration later