mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
feat(frontend): redesign marketplace
This commit is contained in:
@@ -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<string[]>([]);
|
||||
const [streamTypeFilters, setStreamTypeFilters] = useState<
|
||||
constants.StreamType[]
|
||||
>([]);
|
||||
const [resourceFilters, setResourceFilters] = useState<string[]>([]);
|
||||
const [categoryFilter, setCategoryFilter] = useState<
|
||||
constants.PresetCategory | 'all'
|
||||
>('all');
|
||||
const [serviceFilter, setServiceFilter] = useState<string>('all');
|
||||
const [streamTypeFilter, setStreamTypeFilter] = useState<string>('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<string, string> = {
|
||||
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 (
|
||||
<>
|
||||
{/* <div className="flex items-center w-full">
|
||||
@@ -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]"
|
||||
>
|
||||
<div>
|
||||
<h2>Marketplace</h2>
|
||||
@@ -453,56 +476,156 @@ function Content() {
|
||||
Browse and install addons from the marketplace.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-[--card] border border-[--border] rounded-xl p-4 mb-6 shadow-sm">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="w-full sm:w-[500px] flex gap-2">
|
||||
<TextInput
|
||||
value={search}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSearch(e.target.value)
|
||||
}
|
||||
placeholder="Search addons..."
|
||||
className="flex-1"
|
||||
leftIcon={<SearchIcon className="w-4 h-4" />}
|
||||
/>
|
||||
<AddonFilterPopover
|
||||
serviceOptions={serviceOptions}
|
||||
streamTypeOptions={streamTypeOptions}
|
||||
resourceOptions={resourceOptions}
|
||||
serviceFilters={serviceFilters}
|
||||
setServiceFilters={setServiceFilters}
|
||||
streamTypeFilters={streamTypeFilters}
|
||||
setStreamTypeFilters={setStreamTypeFilters}
|
||||
resourceFilters={resourceFilters}
|
||||
setResourceFilters={setResourceFilters}
|
||||
>
|
||||
<IconButton
|
||||
icon={<FilterIcon className="w-5 h-5" />}
|
||||
intent={
|
||||
activeFilterCount > 0 ? 'primary' : 'primary-outline'
|
||||
}
|
||||
aria-label="Filters"
|
||||
/>
|
||||
</AddonFilterPopover>
|
||||
</div>
|
||||
</div>
|
||||
{/* Scrollable Addon Cards Grid */}
|
||||
<div className="h-[calc(100vh-300px)] overflow-y-auto pr-1">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredPresets.map((preset: any) => {
|
||||
// Always allow adding, never show edit
|
||||
return (
|
||||
<AddonCard
|
||||
key={preset.ID}
|
||||
preset={preset}
|
||||
isAdded={false}
|
||||
onAdd={() => handleAddPreset(preset)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Category tabs */}
|
||||
<StaticTabs
|
||||
className="h-10 w-fit border rounded-full"
|
||||
triggerClass="px-4 py-1 text-sm"
|
||||
items={[
|
||||
{
|
||||
name: 'All',
|
||||
isCurrent: categoryFilter === 'all',
|
||||
onClick: () => 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 */}
|
||||
<div className="flex flex-col lg:flex-row gap-2">
|
||||
<div className="flex gap-2 flex-1 lg:flex-none">
|
||||
<Select
|
||||
value={serviceFilter}
|
||||
onValueChange={setServiceFilter}
|
||||
options={[
|
||||
{ label: 'All Services', value: 'all' },
|
||||
...serviceOptions,
|
||||
]}
|
||||
fieldClass="lg:w-[200px]"
|
||||
/>
|
||||
<Select
|
||||
value={streamTypeFilter}
|
||||
onValueChange={setStreamTypeFilter}
|
||||
options={[
|
||||
{ label: 'All Types', value: 'all' },
|
||||
...streamTypeOptions,
|
||||
]}
|
||||
fieldClass="lg:w-[200px]"
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
value={search}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSearch(e.target.value)
|
||||
}
|
||||
placeholder="Search addons..."
|
||||
className="flex-1"
|
||||
leftIcon={<SearchIcon className="w-4 h-4" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Addon cards by category */}
|
||||
{filteredPresets.length === 0 && (
|
||||
<Card className="p-8 text-center">
|
||||
<p className="text-[--muted]">
|
||||
No addons found matching your criteria.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!!streamPresets?.length && (
|
||||
<Card className="p-4 space-y-6">
|
||||
<h3 className="flex gap-3 items-center">
|
||||
<RiFolderDownloadFill /> Streams
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{streamPresets.map((preset: any) => (
|
||||
<AddonCard
|
||||
key={preset.ID}
|
||||
preset={preset}
|
||||
onAdd={() => handleAddPreset(preset)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!!subtitlePresets?.length && (
|
||||
<Card className="p-4 space-y-6">
|
||||
<h3 className="flex gap-3 items-center">
|
||||
<MdSubtitles /> Subtitles
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{subtitlePresets.map((preset: any) => (
|
||||
<AddonCard
|
||||
key={preset.ID}
|
||||
preset={preset}
|
||||
onAdd={() => handleAddPreset(preset)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!!metaCatalogPresets?.length && (
|
||||
<Card className="p-4 space-y-6">
|
||||
<h3 className="flex gap-3 items-center">
|
||||
<MdOutlineDataset /> Meta & Catalogues
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{metaCatalogPresets.map((preset: any) => (
|
||||
<AddonCard
|
||||
key={preset.ID}
|
||||
preset={preset}
|
||||
onAdd={() => handleAddPreset(preset)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!!miscPresets?.length && (
|
||||
<Card className="p-4 space-y-6">
|
||||
<h3 className="flex gap-3 items-center">
|
||||
<LuSettings /> Miscellaneous
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{miscPresets.map((preset: any) => (
|
||||
<AddonCard
|
||||
key={preset.ID}
|
||||
preset={preset}
|
||||
onAdd={() => handleAddPreset(preset)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</PageWrapper>
|
||||
)}
|
||||
{/* Add/Edit Addon Modal (ensure both tabs can use it)*/}
|
||||
@@ -701,15 +824,23 @@ function SortableAddonItem({
|
||||
/>
|
||||
<div className="flex items-center gap-2 sm:gap-3 flex-1 min-w-0">
|
||||
<div className="relative flex-shrink-0 h-8 w-8 hidden sm:block">
|
||||
{!logo ? (
|
||||
<IoExtensionPuzzle className="w-full h-full object-contain" />
|
||||
) : (
|
||||
{logo ? (
|
||||
<Image
|
||||
src={logo}
|
||||
alt={presetMetadata.NAME}
|
||||
fill
|
||||
className="w-full h-full object-contain rounded-md"
|
||||
/>
|
||||
) : presetMetadata.ID === 'custom' ? (
|
||||
<div className="w-full h-full flex items-center justify-center rounded-md bg-gray-950">
|
||||
<PlusIcon className="w-4 h-4 text-[--brand]" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center rounded-md bg-gray-950">
|
||||
<p className="text-lg font-bold">
|
||||
{preset.options.name?.[0]?.toUpperCase() || '?'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col min-h-72 h-auto bg-[--background] border border-[--border] rounded-lg shadow-sm p-4 relative overflow-hidden">
|
||||
{/* Built-in ribbon */}
|
||||
{preset.BUILTIN && (
|
||||
<div className="absolute -left-[30px] top-[20px] bg-[rgb(var(--color-brand-500))] text-white text-xs font-semibold py-1 w-[120px] text-center transform -rotate-45 shadow-md">
|
||||
Built-in
|
||||
</div>
|
||||
)}
|
||||
{/* Top: Logo + Name/Description */}
|
||||
<div className="flex gap-4 items-start">
|
||||
{preset.ID === 'custom' ? (
|
||||
<div className="w-28 h-28 min-w-[7rem] min-h-[7rem] flex items-center justify-center rounded-lg bg-gray-900 text-[--brand] text-4xl">
|
||||
<PlusIcon className="w-12 h-12" />
|
||||
</div>
|
||||
) : preset.LOGO ? (
|
||||
<img
|
||||
src={preset.LOGO}
|
||||
alt={preset.NAME}
|
||||
className="w-28 h-28 min-w-[7rem] min-h-[7rem] object-contain rounded-lg bg-gray-800"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-28 h-28 min-w-[7rem] min-h-[7rem] flex items-center justify-center rounded-lg bg-gray-900 text-[--brand] text-4xl">
|
||||
<IoExtensionPuzzle className="w-15 h-15" />
|
||||
<>
|
||||
<div className="border border-[rgb(255_255_255_/_5%)] relative overflow-hidden bg-gray-900 rounded-xl p-3 flex flex-col h-full">
|
||||
{/* Built-in ribbon - top-right */}
|
||||
{preset.BUILTIN && (
|
||||
<div
|
||||
className="absolute -right-[30px] top-[20px] bg-[rgb(var(--color-brand-500))] text-white text-xs font-semibold py-1 w-[120px] text-center transform rotate-45 shadow-md z-[2] cursor-pointer hover:bg-[rgb(var(--color-brand-600))] transition-colors"
|
||||
onClick={() => setShowBuiltinModal(true)}
|
||||
title="Click to learn more about built-in addons"
|
||||
>
|
||||
Built-in
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<div className="font-bold text-lg mb-1 truncate">{preset.NAME}</div>
|
||||
<div className="text-sm text-muted-foreground mb-2 line-clamp-3 whitespace-pre-line">
|
||||
<MarkdownLite>{preset.DESCRIPTION}</MarkdownLite>
|
||||
|
||||
<div className="z-[1] relative flex flex-col flex-1 gap-3">
|
||||
{/* Logo and Name */}
|
||||
<div className="flex gap-3 pr-16">
|
||||
{preset.ID === 'custom' ? (
|
||||
<div className="relative rounded-md size-12 bg-gray-950 overflow-hidden flex items-center justify-center">
|
||||
<PlusIcon className="w-6 h-6 text-[--brand]" />
|
||||
</div>
|
||||
) : preset.LOGO ? (
|
||||
<div className="relative rounded-md size-12 bg-gray-900 overflow-hidden">
|
||||
<img
|
||||
src={preset.LOGO}
|
||||
alt={preset.NAME}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative rounded-md size-12 bg-gray-950 overflow-hidden flex items-center justify-center">
|
||||
<p className="text-2xl font-bold">
|
||||
{preset.NAME[0].toUpperCase()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="font-semibold line-clamp-1">{preset.NAME}</p>
|
||||
<p className="text-xs line-clamp-1 tracking-wide opacity-30">
|
||||
{preset.ID}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Tags Section */}
|
||||
<div className="flex flex-col gap-1 mt-2">
|
||||
<div className="flex flex-wrap gap-1 items-center min-h-[1.5rem]">
|
||||
{preset.SUPPORTED_SERVICES?.length > 0 && (
|
||||
<span className="font-semibold text-xs text-[--muted] mr-1">
|
||||
Services:
|
||||
</span>
|
||||
|
||||
{/* Description */}
|
||||
{preset.DESCRIPTION && (
|
||||
<Popover
|
||||
trigger={
|
||||
<p className="text-sm text-[--muted] line-clamp-2 cursor-pointer">
|
||||
<MarkdownLite>{preset.DESCRIPTION}</MarkdownLite>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<p className="text-sm">
|
||||
<MarkdownLite>{preset.DESCRIPTION}</MarkdownLite>
|
||||
</p>
|
||||
</Popover>
|
||||
)}
|
||||
{preset.SUPPORTED_SERVICES?.map((sid: string) => {
|
||||
const service =
|
||||
constants.SERVICE_DETAILS[
|
||||
sid as keyof typeof constants.SERVICE_DETAILS
|
||||
];
|
||||
return (
|
||||
<Tooltip
|
||||
key={sid}
|
||||
side="top"
|
||||
trigger={
|
||||
<span className="bg-gray-800 text-xs px-2 py-0.5 rounded text-[--brand] font-mono">
|
||||
{service?.shortName || sid}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span className="bg-gray-800 text-xs px-2 py-0.5 rounded text-[--brand] font-mono">
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{preset.SUPPORTED_SERVICES?.map((sid: string) => {
|
||||
const service =
|
||||
constants.SERVICE_DETAILS[
|
||||
sid as keyof typeof constants.SERVICE_DETAILS
|
||||
];
|
||||
return (
|
||||
<Tooltip
|
||||
key={sid}
|
||||
side="top"
|
||||
trigger={
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-[--brand]/10 text-[--brand] border border-[--brand]/20">
|
||||
{service?.shortName || sid}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{service?.name || sid}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 items-center min-h-[1.5rem]">
|
||||
{preset.SUPPORTED_RESOURCES?.length > 0 && (
|
||||
<span className="font-semibold text-xs text-[--muted] mr-1">
|
||||
Resources:
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{preset.SUPPORTED_RESOURCES?.map((res: string) => (
|
||||
<span
|
||||
key={res}
|
||||
className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-500/10 text-blue-400 border border-blue-500/20"
|
||||
>
|
||||
{res}
|
||||
</span>
|
||||
))}
|
||||
{preset.SUPPORTED_STREAM_TYPES?.map((type: string) => (
|
||||
<span
|
||||
key={type}
|
||||
className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/20"
|
||||
>
|
||||
{type}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Spacer to push button to bottom */}
|
||||
<div className="flex-1"></div>
|
||||
|
||||
{preset.DISABLED ? (
|
||||
<div className="mt-auto">
|
||||
<Alert
|
||||
intent="alert"
|
||||
className="w-full overflow-x-auto whitespace-nowrap"
|
||||
description={
|
||||
<MarkdownLite>{preset.DISABLED.reason}</MarkdownLite>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-auto">
|
||||
<Button
|
||||
size="md"
|
||||
className="w-full"
|
||||
intent="primary-subtle"
|
||||
onClick={onAdd}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{preset.SUPPORTED_RESOURCES?.map((res: string) => (
|
||||
<span
|
||||
key={res}
|
||||
className="bg-gray-800 text-xs px-2 py-0.5 rounded text-blue-400 font-mono"
|
||||
>
|
||||
{res}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 items-center min-h-[1.5rem]">
|
||||
{preset.SUPPORTED_STREAM_TYPES?.length > 0 && (
|
||||
<span className="font-semibold text-xs text-[--muted] mr-1">
|
||||
Stream Types:
|
||||
</span>
|
||||
)}
|
||||
{preset.SUPPORTED_STREAM_TYPES?.map((type: string) => (
|
||||
<span
|
||||
key={type}
|
||||
className="bg-gray-800 text-xs px-2 py-0.5 rounded text-green-400 font-mono"
|
||||
>
|
||||
{type}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{preset.DISABLED ? (
|
||||
<div className="mt-auto pt-3 flex items-end">
|
||||
<Alert
|
||||
intent="alert"
|
||||
className="w-full overflow-x-auto whitespace-nowrap"
|
||||
description={<MarkdownLite>{preset.DISABLED.reason}</MarkdownLite>}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={showBuiltinModal}
|
||||
onOpenChange={setShowBuiltinModal}
|
||||
title="What are Built-in Addons?"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
<div className="bg-[--subtle] rounded-lg p-3 space-y-2">
|
||||
<p className="text-sm font-medium">Why does this matter?</p>
|
||||
<ul className="text-sm text-[--muted] space-y-1.5 list-disc list-inside">
|
||||
<li>
|
||||
No external server calls - everything runs on your machine
|
||||
</li>
|
||||
<li>Not affected by rate limits from external addon providers</li>
|
||||
<li>Faster response times since there's no network delay</li>
|
||||
<li>Works even if external addon servers are down</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="text-xs text-[--muted] italic">
|
||||
Think of it like having the addon server built into AIOStreams
|
||||
itself!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-auto pt-3 flex items-end">
|
||||
<Button size="md" className="w-full" onClick={onAdd}>
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
trigger={children}
|
||||
modal={false}
|
||||
className="p-4 max-w-full w-full"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||
<div className="flex flex-col max-h-60 overflow-y-auto">
|
||||
<div className="mb-2 font-semibold text-sm text-muted-foreground">
|
||||
Services
|
||||
</div>
|
||||
{serviceOptions.map((opt: any) => (
|
||||
<div
|
||||
key={opt.value}
|
||||
className="flex items-center gap-2 mb-2 last:mb-0"
|
||||
>
|
||||
<Switch
|
||||
value={serviceFilters.includes(opt.value)}
|
||||
onValueChange={(checked: boolean) => {
|
||||
setServiceFilters((prev: string[]) =>
|
||||
checked
|
||||
? [...prev, opt.value]
|
||||
: prev.filter((v) => v !== opt.value)
|
||||
);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="text-xs">{opt.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col max-h-60 overflow-y-auto">
|
||||
<div className="mb-2 font-semibold text-sm text-muted-foreground">
|
||||
Stream Types
|
||||
</div>
|
||||
{streamTypeOptions.map((opt: any) => (
|
||||
<div
|
||||
key={opt.value}
|
||||
className="flex items-center gap-2 mb-2 last:mb-0"
|
||||
>
|
||||
<Switch
|
||||
value={streamTypeFilters.includes(opt.value)}
|
||||
onValueChange={(checked: boolean) => {
|
||||
setStreamTypeFilters((prev: string[]) =>
|
||||
checked
|
||||
? [...prev, opt.value]
|
||||
: prev.filter((v) => v !== opt.value)
|
||||
);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="text-xs">{opt.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col max-h-60 overflow-y-auto">
|
||||
<div className="mb-2 font-semibold text-sm text-muted-foreground">
|
||||
Resources
|
||||
</div>
|
||||
{resourceOptions.map((opt: any) => (
|
||||
<div
|
||||
key={opt.value}
|
||||
className="flex items-center gap-2 mb-2 last:mb-0"
|
||||
>
|
||||
<Switch
|
||||
value={resourceFilters.includes(opt.value)}
|
||||
onValueChange={(checked: boolean) => {
|
||||
setResourceFilters((prev: string[]) =>
|
||||
checked
|
||||
? [...prev, opt.value]
|
||||
: prev.filter((v) => v !== opt.value)
|
||||
);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="text-xs">{opt.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full mt-2" onClick={() => setOpen(false)}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function AddonFetchingBehaviorCard() {
|
||||
const { userData, setUserData } = useUserData();
|
||||
const [mode, setMode] = useState(() => {
|
||||
|
||||
Reference in New Issue
Block a user