diff --git a/packages/frontend/src/components/menu/addons.tsx b/packages/frontend/src/components/menu/addons.tsx index f5d3f644..b5ecd802 100644 --- a/packages/frontend/src/components/menu/addons.tsx +++ b/packages/frontend/src/components/menu/addons.tsx @@ -7,6 +7,7 @@ import { SettingsCard } from '../shared/settings-card'; import { Button, IconButton } from '../ui/button'; import { Modal } from '../ui/modal'; import { Switch } from '../ui/switch'; +import { Card } from '../ui/card'; import { DndContext, useSensor, @@ -26,6 +27,8 @@ import { PlusIcon, SearchIcon, FilterIcon } from 'lucide-react'; import TemplateOption from '../shared/template-option'; import * as constants from '../../../../core/src/utils/constants'; import { TextInput } from '../ui/text-input'; +import { MdSubtitles, MdOutlineDataset } from 'react-icons/md'; +import { RiFolderDownloadFill } from 'react-icons/ri'; import { Popover } from '../ui/popover'; import { BiEdit, BiTrash } from 'react-icons/bi'; @@ -110,11 +113,11 @@ function Content() { const [page, setPage] = useState<'installed' | 'marketplace'>('installed'); const [search, setSearch] = useState(''); // Filter states - const [serviceFilters, setServiceFilters] = useState([]); - const [streamTypeFilters, setStreamTypeFilters] = useState< - constants.StreamType[] - >([]); - const [resourceFilters, setResourceFilters] = useState([]); + const [categoryFilter, setCategoryFilter] = useState< + constants.PresetCategory | 'all' + >('all'); + const [serviceFilter, setServiceFilter] = useState('all'); + const [streamTypeFilter, setStreamTypeFilter] = useState('all'); // Modal states const [modalOpen, setModalOpen] = useState(false); const [modalMode, setModalMode] = useState<'add' | 'edit'>('add'); @@ -127,33 +130,35 @@ function Content() { // Filtering and search for marketplace const filteredPresets = useMemo(() => { if (!status?.settings?.presets) return []; - return status.settings.presets.filter((preset) => { - if (preset.ID === 'custom') return true; - const matchesService = - serviceFilters.length === 0 || - (preset.SUPPORTED_SERVICES && - serviceFilters.every((s) => preset.SUPPORTED_SERVICES.includes(s))); - const matchesStreamType = - streamTypeFilters.length === 0 || - (preset.SUPPORTED_STREAM_TYPES && - streamTypeFilters.every((t) => - preset.SUPPORTED_STREAM_TYPES.includes(t) - )); - const matchesResource = - resourceFilters.length === 0 || - (preset.SUPPORTED_RESOURCES && - resourceFilters.every((r) => - preset.SUPPORTED_RESOURCES.includes(r as Resource) - )); - const matchesSearch = - !search || - preset.NAME.toLowerCase().includes(search.toLowerCase()) || - preset.DESCRIPTION.toLowerCase().includes(search.toLowerCase()); - return ( - matchesService && matchesStreamType && matchesResource && matchesSearch + let filtered = [...status.settings.presets]; + if (categoryFilter !== 'all') { + filtered = filtered.filter( + (n) => + (n.CATEGORY || constants.PresetCategory.STREAMS) === categoryFilter ); - }); - }, [status, search, serviceFilters, streamTypeFilters, resourceFilters]); + } + if (serviceFilter !== 'all') { + filtered = filtered.filter( + (n) => + n.SUPPORTED_SERVICES && n.SUPPORTED_SERVICES.includes(serviceFilter) + ); + } + if (streamTypeFilter !== 'all') { + filtered = filtered.filter( + (n) => + n.SUPPORTED_STREAM_TYPES && + n.SUPPORTED_STREAM_TYPES.includes(streamTypeFilter as any) + ); + } + if (search) { + filtered = filtered.filter( + (n) => + n.NAME.toLowerCase().includes(search.toLowerCase()) || + n.DESCRIPTION.toLowerCase().includes(search.toLowerCase()) + ); + } + return filtered; + }, [status, search, categoryFilter, serviceFilter, streamTypeFilter]); // My Addons (user's enabled/added presets) @@ -247,19 +252,22 @@ function Content() { setIsDragging(true); } - // Service, stream type, and resource options + // Service, stream type options const serviceOptions = Object.values(constants.SERVICE_DETAILS).map( (service) => ({ label: service.name, value: service.id }) ); + const typeLabelMap: Record = { + p2p: 'P2P', + http: 'HTTP', + usenet: 'Usenet', + debrid: 'Debrid', + live: 'Live', + }; const streamTypeOptions = (constants.STREAM_TYPES || []) - .filter((type) => type !== 'error') - .map((type: string) => ({ label: type, value: type })); - const resourceOptions = (constants.RESOURCES || []).map((res: string) => ({ - label: res, - value: res, - })); - const activeFilterCount = - serviceFilters.length + streamTypeFilters.length + resourceFilters.length; + .filter( + (type) => !['error', 'statistic', 'external', 'youtube'].includes(type) + ) + .map((type: string) => ({ label: typeLabelMap[type], value: type })); // DND-kit setup const sensors = useSensors( @@ -298,6 +306,21 @@ function Content() { document.removeEventListener('touchend', handleDragEnd); }; }, [isDragging]); + + // Group presets by category + const streamPresets = filteredPresets.filter( + (n) => n.CATEGORY === constants.PresetCategory.STREAMS || !n.CATEGORY + ); + const subtitlePresets = filteredPresets.filter( + (n) => n.CATEGORY === constants.PresetCategory.SUBTITLES + ); + const metaCatalogPresets = filteredPresets.filter( + (n) => n.CATEGORY === constants.PresetCategory.META_CATALOGS + ); + const miscPresets = filteredPresets.filter( + (n) => n.CATEGORY === constants.PresetCategory.MISC + ); + return ( <> {/*
@@ -445,7 +468,7 @@ function Content() { }, }} key="marketplace" - className="pt-0 space-y-8 relative z-[4]" + className="pt-0 space-y-6 relative z-[4]" >

Marketplace

@@ -453,56 +476,156 @@ function Content() { Browse and install addons from the marketplace.

-
-
-
- ) => - setSearch(e.target.value) - } - placeholder="Search addons..." - className="flex-1" - leftIcon={} - /> - - } - intent={ - activeFilterCount > 0 ? 'primary' : 'primary-outline' - } - aria-label="Filters" - /> - -
-
- {/* Scrollable Addon Cards Grid */} -
-
- {filteredPresets.map((preset: any) => { - // Always allow adding, never show edit - return ( - handleAddPreset(preset)} - /> - ); - })} -
+ + {/* Category tabs */} + setCategoryFilter('all'), + }, + { + name: 'Streams', + isCurrent: + categoryFilter === constants.PresetCategory.STREAMS, + onClick: () => + setCategoryFilter(constants.PresetCategory.STREAMS), + }, + { + name: 'Subtitles', + isCurrent: + categoryFilter === constants.PresetCategory.SUBTITLES, + onClick: () => + setCategoryFilter(constants.PresetCategory.SUBTITLES), + }, + { + name: 'Meta & Catalogues', + isCurrent: + categoryFilter === constants.PresetCategory.META_CATALOGS, + onClick: () => + setCategoryFilter(constants.PresetCategory.META_CATALOGS), + }, + { + name: 'Miscellaneous', + isCurrent: categoryFilter === constants.PresetCategory.MISC, + onClick: () => + setCategoryFilter(constants.PresetCategory.MISC), + }, + ]} + /> + + {/* Filters and search row */} +
+
+
+ ) => + setSearch(e.target.value) + } + placeholder="Search addons..." + className="flex-1" + leftIcon={} + />
+ + {/* Addon cards by category */} + {filteredPresets.length === 0 && ( + +

+ No addons found matching your criteria. +

+
+ )} + + {!!streamPresets?.length && ( + +

+ Streams +

+
+ {streamPresets.map((preset: any) => ( + handleAddPreset(preset)} + /> + ))} +
+
+ )} + + {!!subtitlePresets?.length && ( + +

+ Subtitles +

+
+ {subtitlePresets.map((preset: any) => ( + handleAddPreset(preset)} + /> + ))} +
+
+ )} + + {!!metaCatalogPresets?.length && ( + +

+ Meta & Catalogues +

+
+ {metaCatalogPresets.map((preset: any) => ( + handleAddPreset(preset)} + /> + ))} +
+
+ )} + + {!!miscPresets?.length && ( + +

+ Miscellaneous +

+
+ {miscPresets.map((preset: any) => ( + handleAddPreset(preset)} + /> + ))} +
+
+ )} )} {/* Add/Edit Addon Modal (ensure both tabs can use it)*/} @@ -701,15 +824,23 @@ function SortableAddonItem({ />
- {!logo ? ( - - ) : ( + {logo ? ( {presetMetadata.NAME} + ) : presetMetadata.ID === 'custom' ? ( +
+ +
+ ) : ( +
+

+ {preset.options.name?.[0]?.toUpperCase() || '?'} +

+
)}
@@ -826,124 +957,166 @@ function SortableAddonItem({ } // AddonCard component -function AddonCard({ - preset, - isAdded, - onAdd, -}: { - preset: any; - isAdded: boolean; - onAdd: () => void; -}) { +function AddonCard({ preset, onAdd }: { preset: any; onAdd: () => void }) { + const [showDescriptionPopover, setShowDescriptionPopover] = useState(false); + const [showBuiltinModal, setShowBuiltinModal] = useState(false); + return ( -
- {/* Built-in ribbon */} - {preset.BUILTIN && ( -
- Built-in -
- )} - {/* Top: Logo + Name/Description */} -
- {preset.ID === 'custom' ? ( -
- -
- ) : preset.LOGO ? ( - {preset.NAME} - ) : ( -
- + <> +
+ {/* Built-in ribbon - top-right */} + {preset.BUILTIN && ( +
setShowBuiltinModal(true)} + title="Click to learn more about built-in addons" + > + Built-in
)} -
-
{preset.NAME}
-
- {preset.DESCRIPTION} + +
+ {/* Logo and Name */} +
+ {preset.ID === 'custom' ? ( +
+ +
+ ) : preset.LOGO ? ( +
+ {preset.NAME} +
+ ) : ( +
+

+ {preset.NAME[0].toUpperCase()} +

+
+ )} + +
+

{preset.NAME}

+

+ {preset.ID} +

+
-
-
- {/* Tags Section */} -
-
- {preset.SUPPORTED_SERVICES?.length > 0 && ( - - Services: - + + {/* Description */} + {preset.DESCRIPTION && ( + + {preset.DESCRIPTION} +

+ } + > +

+ {preset.DESCRIPTION} +

+
)} - {preset.SUPPORTED_SERVICES?.map((sid: string) => { - const service = - constants.SERVICE_DETAILS[ - sid as keyof typeof constants.SERVICE_DETAILS - ]; - return ( - - {service?.shortName || sid} - - } - > - + +
+ {preset.SUPPORTED_SERVICES?.map((sid: string) => { + const service = + constants.SERVICE_DETAILS[ + sid as keyof typeof constants.SERVICE_DETAILS + ]; + return ( + + {service?.shortName || sid} + + } + > {service?.name || sid} - - - ); - })} -
-
- {preset.SUPPORTED_RESOURCES?.length > 0 && ( - - Resources: - + + ); + })} + {preset.SUPPORTED_RESOURCES?.map((res: string) => ( + + {res} + + ))} + {preset.SUPPORTED_STREAM_TYPES?.map((type: string) => ( + + {type} + + ))} +
+ + {/* Spacer to push button to bottom */} +
+ + {preset.DISABLED ? ( +
+ {preset.DISABLED.reason} + } + /> +
+ ) : ( +
+ +
)} - {preset.SUPPORTED_RESOURCES?.map((res: string) => ( - - {res} - - ))} -
-
- {preset.SUPPORTED_STREAM_TYPES?.length > 0 && ( - - Stream Types: - - )} - {preset.SUPPORTED_STREAM_TYPES?.map((type: string) => ( - - {type} - - ))}
- {preset.DISABLED ? ( -
- {preset.DISABLED.reason}} - /> + + +
+

+ Built-in addons are addons whose code lives directly inside + AIOStreams. You still install and configure them from the + marketplace just like any other addon (such as Comet or Torrentio), + but they run locally on your AIOStreams instance. +

+
+

Why does this matter?

+
    +
  • + No external server calls - everything runs on your machine +
  • +
  • Not affected by rate limits from external addon providers
  • +
  • Faster response times since there's no network delay
  • +
  • Works even if external addon servers are down
  • +
+
+

+ Think of it like having the addon server built into AIOStreams + itself! +

- ) : ( -
- -
- )} -
+ + ); } @@ -1082,110 +1255,6 @@ function AddonModal({ ); } -function AddonFilterPopover({ - serviceOptions, - streamTypeOptions, - resourceOptions, - serviceFilters, - setServiceFilters, - streamTypeFilters, - setStreamTypeFilters, - resourceFilters, - setResourceFilters, - children, -}: any) { - const [open, setOpen] = useState(false); - return ( - -
-
-
-
- Services -
- {serviceOptions.map((opt: any) => ( -
- { - setServiceFilters((prev: string[]) => - checked - ? [...prev, opt.value] - : prev.filter((v) => v !== opt.value) - ); - }} - size="sm" - /> - {opt.label} -
- ))} -
-
-
- Stream Types -
- {streamTypeOptions.map((opt: any) => ( -
- { - setStreamTypeFilters((prev: string[]) => - checked - ? [...prev, opt.value] - : prev.filter((v) => v !== opt.value) - ); - }} - size="sm" - /> - {opt.label} -
- ))} -
-
-
- Resources -
- {resourceOptions.map((opt: any) => ( -
- { - setResourceFilters((prev: string[]) => - checked - ? [...prev, opt.value] - : prev.filter((v) => v !== opt.value) - ); - }} - size="sm" - /> - {opt.label} -
- ))} -
-
- -
-
- ); -} - function AddonFetchingBehaviorCard() { const { userData, setUserData } = useUserData(); const [mode, setMode] = useState(() => {