some fixes

This commit is contained in:
Viren070
2025-06-04 20:26:27 +01:00
parent c0b6fd3e7d
commit ffd9dbb042
7 changed files with 325 additions and 138 deletions
+49 -37
View File
@@ -808,7 +808,7 @@ export class AIOStreams {
);
const shouldFetch = await parser.parse(group.condition);
if (shouldFetch) {
logger.info(`Condition met for group ${i}, fetching streams`);
logger.info(`Condition met for group ${i + 1}, fetching streams`);
const groupAddons = supportedAddons.filter(
(addon) => addon.id && group.addons.includes(addon.id)
@@ -868,12 +868,15 @@ export class AIOStreams {
requiredUncachedFromServices: { total: 0, details: {} },
excludedUncachedMode: { total: 0, details: {} },
requiredUncachedMode: { total: 0, details: {} },
excludedEncode: { total: 0, details: {} },
requiredEncode: { total: 0, details: {} },
excludedCachedFromAddons: { total: 0, details: {} },
requiredCachedFromAddons: { total: 0, details: {} },
excludedCachedFromServices: { total: 0, details: {} },
requiredCachedFromServices: { total: 0, details: {} },
excludedCachedMode: { total: 0, details: {} },
requiredCachedMode: { total: 0, details: {} },
requiredLanguage: { total: 0, details: {} },
};
const start = Date.now();
@@ -945,40 +948,25 @@ export class AIOStreams {
serviceIds: string[] | undefined,
cached: boolean
) => {
if (this.userData.excludeCached && cached && stream.service?.cached) {
return false;
}
if (
this.userData.excludeUncached &&
cached === false &&
stream.service?.cached === false
) {
return false;
}
const addonCriteriaMet =
!addonIds ||
addonIds.length === 0 ||
(addonIds.some((addonId) => stream.addon.id === addonId) &&
stream.service?.cached === cached);
const serviceCriteriaMet =
!serviceIds ||
serviceIds.length === 0 ||
(serviceIds.some((serviceId) => stream.service?.id === serviceId) &&
stream.service?.cached === cached);
if (mode === 'and') {
return (
(!addonIds ||
addonIds.length === 0 ||
addonIds.some((addonId) => stream.addon.id === addonId)) &&
(!serviceIds ||
serviceIds.length === 0 ||
serviceIds.some((serviceId) => stream.service?.id === serviceId)) &&
stream.service?.cached === cached
);
return addonCriteriaMet && serviceCriteriaMet;
} else {
return (
(!addonIds ||
addonIds.length === 0 ||
addonIds.some((addonId) => stream.addon.id === addonId)) &&
(!serviceIds ||
serviceIds.length === 0 ||
serviceIds.some((serviceId) => stream.service?.id === serviceId)) &&
stream.service?.cached === cached
);
return addonCriteriaMet || serviceCriteriaMet;
}
};
const filteredStreams = streams.filter(async (stream) => {
const shouldKeepStream = async (stream: ParsedStream): Promise<boolean> => {
const file = stream.parsedFile;
// carry out include checks first
@@ -1278,7 +1266,7 @@ export class AIOStreams {
this.userData.excludeCachedFromAddons,
this.userData.excludeCachedFromServices,
true
)
) === false
) {
skipReasons.excludedCached.total++;
return false;
@@ -1291,7 +1279,7 @@ export class AIOStreams {
this.userData.excludeUncachedFromAddons,
this.userData.excludeUncachedFromServices,
false
)
) === false
) {
skipReasons.excludedUncached.total++;
return false;
@@ -1327,22 +1315,45 @@ export class AIOStreams {
// TODO: size filters
return true;
});
};
const filterResults = await Promise.all(streams.map(shouldKeepStream));
const filteredStreams = streams.filter((_, index) => filterResults[index]);
// Log filter summary
const totalFiltered = streams.length - filteredStreams.length;
if (totalFiltered > 0) {
const summary = [`Filtered out ${totalFiltered} streams:`];
const summary = [
'\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
` 🔍 Filter Summary`,
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
` 📊 Total Streams : ${streams.length}`,
` ✔️ Kept : ${filteredStreams.length}`,
` ❌ Filtered : ${totalFiltered}`,
];
// Add filter details if any streams were filtered
const filterDetails: string[] = [];
for (const [reason, stats] of Object.entries(skipReasons)) {
if (stats.total > 0) {
summary.push(` - ${stats.total} due to ${reason}:`);
// Convert camelCase to Title Case with spaces
const formattedReason = reason
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
filterDetails.push(`\n 📌 ${formattedReason} (${stats.total})`);
for (const [detail, count] of Object.entries(stats.details)) {
summary.push(`${count} ${detail}`);
filterDetails.push(`${count}× ${detail}`);
}
}
}
if (filterDetails.length > 0) {
summary.push('\n 🔎 Filter Details:');
summary.push(...filterDetails);
}
summary.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
logger.info(summary.join('\n'));
}
@@ -1684,7 +1695,7 @@ export class AIOStreams {
(stream) => stream.service?.cached || stream.service === undefined // streams without a service can be considered as 'cached'
);
const uncachedStreams = streams.filter(
(stream) => !stream.service?.cached
(stream) => stream.service?.cached === false
);
// sort the 2 lists separately, and put them after the other, depending on the direction of cached
@@ -1714,6 +1725,7 @@ export class AIOStreams {
sortedStreams = [...uncachedSorted, ...cachedSorted];
}
} else {
logger.debug(`using sort criteria: ${JSON.stringify(sortCriteria)}`);
sortedStreams = streams.slice().sort((a, b) => {
const aKey = this.dynamicSortKey(a, sortCriteria, type);
const bKey = this.dynamicSortKey(b, sortCriteria, type);
+2 -2
View File
@@ -1,7 +1,7 @@
import { Stream, ParsedStream, Addon } from '../db';
import { constants } from '../utils';
import { constants, createLogger } from '../utils';
import FileParser from './file';
const logger = createLogger('parser');
class StreamParser {
get errorRegexes(): { pattern: RegExp; message: string }[] | undefined {
return [
+3 -1
View File
@@ -1,7 +1,8 @@
import { Addon, Option, UserData, Resource } from '../db';
import { Addon, Option, UserData, Resource, Stream } from '../db';
import { baseOptions, Preset } from './preset';
import { Env } from '../utils';
import { constants, ServiceId } from '../utils';
import { StreamParser } from '../parser';
export class StremthruStorePreset extends Preset {
static override get METADATA() {
@@ -103,6 +104,7 @@ export class StremthruStorePreset extends Preset {
: options.name || this.METADATA.NAME,
manifestUrl: this.generateManifestUrl(userData, options, serviceId),
enabled: true,
library: true,
resources: options.resources || this.METADATA.SUPPORTED_RESOURCES,
timeout: options.timeout || this.METADATA.TIMEOUT,
fromPresetId: this.METADATA.ID,
+1 -5
View File
@@ -21,11 +21,7 @@ class TorboxStreamParser extends StreamParser {
return (stream as any).hash;
}
override getInLibrary(stream: Stream): boolean {
return (
((stream as any).is_your_media ||
stream.description?.includes('Your Media')) ??
false
);
return (stream as any).is_your_media || stream.name?.includes('Your Media');
}
protected override getService(
stream: Stream
+13 -8
View File
@@ -258,7 +258,7 @@ export async function validateConfig(
if (config.groups) {
for (const group of config.groups) {
validateGroup(group);
await validateGroup(group);
}
}
@@ -383,7 +383,7 @@ function validatePreset(preset: PresetObject) {
}
}
function validateGroup(group: Group) {
async function validateGroup(group: Group) {
if (!group) {
return;
}
@@ -394,13 +394,18 @@ function validateGroup(group: Group) {
}
// we must be able to parse the condition
let result;
try {
const result = ConditionParser.testParse(group.condition);
if (typeof result !== 'boolean') {
throw new Error('Group condition must evaluate to a boolean');
}
} catch (error) {
throw new Error(`Group condition is invalid: ${error}`);
result = await ConditionParser.testParse(group.condition);
} catch (error: any) {
throw new Error(
`Your group condition - '${group.condition}' - is invalid: ${error.message}`
);
}
if (typeof result !== 'boolean') {
throw new Error(
`Your group condition - '${group.condition}' - is invalid. Expected evaluation to a boolean, instead got '${typeof result}'`
);
}
}
@@ -33,7 +33,12 @@ import { Option, Resource } from '@aiostreams/core';
import { toast } from 'sonner';
import { Tooltip } from '../ui/tooltip';
import { StaticTabs } from '../ui/tabs';
import { LuDownload, LuGlobe } from 'react-icons/lu';
import {
LuDownload,
LuGlobe,
LuChevronsUp,
LuChevronsDown,
} from 'react-icons/lu';
import { AnimatePresence } from 'framer-motion';
import { PageControls } from '../shared/page-controls';
import Image from 'next/image';
@@ -44,6 +49,7 @@ import {
ConfirmationDialog,
useConfirmationDialog,
} from '../shared/confirmation-dialog';
import { MdRefresh } from 'react-icons/md';
interface CatalogModification {
id: string;
@@ -514,14 +520,14 @@ function SortableAddonItem({
};
return (
<li ref={setNodeRef} style={style}>
<div className="px-2.5 py-2 bg-[var(--background)] rounded-[--radius-md] border flex gap-3 relative">
<div className="px-2.5 py-2 bg-[var(--background)] rounded-[--radius-md] border flex gap-2 sm:gap-3 relative">
<div
className="rounded-full w-6 h-auto bg-[--muted] md:bg-[--subtle] md:hover:bg-[--subtle-highlight] cursor-move"
className="rounded-full w-6 h-auto bg-[--muted] md:bg-[--subtle] md:hover:bg-[--subtle-highlight] cursor-move flex-shrink-0"
{...attributes}
{...listeners}
/>
<div className="flex items-center gap-3 flex-1">
<div className="relative flex-shrink-0 h-8 w-8">
<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">
{preset.ID === 'custom' ? (
<PlusIcon className="w-full h-full object-contain" />
) : (
@@ -534,19 +540,26 @@ function SortableAddonItem({
)}
</div>
<p className="text-base line-clamp-1">{addon.options.name}</p>
<p className="text-base line-clamp-1 truncate block">
{addon.options.name}
</p>
</div>
<div className="flex items-center gap-2">
<Switch value={addon.enabled} onValueChange={onToggleEnabled} />
<div className="flex items-center gap-1 sm:gap-2">
<Switch
value={addon.enabled}
onValueChange={onToggleEnabled}
size="sm"
className="sm:scale-100 scale-90"
/>
<IconButton
className="rounded-full"
className="rounded-full sm:scale-100 scale-85"
icon={<BiEdit />}
intent="primary-subtle"
onClick={onEdit}
/>
<IconButton
className="rounded-full"
className="rounded-full sm:scale-100 scale-85"
icon={<BiTrash />}
intent="alert-subtle"
onClick={onRemove}
@@ -1020,7 +1033,7 @@ function CatalogSettingsCard() {
type: catalog.type,
enabled: true,
shuffle: false,
rpdb: false,
rpdb: userData.rpdbApiKey ? true : false,
}));
setUserData((prev) => ({
...prev,
@@ -1152,9 +1165,11 @@ function CatalogSettingsCard() {
<h3 className="text-lg font-semibold">Catalogs</h3>
<p className="text-[--muted] text-sm">Manage your catalogs</p>
</div>
<Button
<IconButton
size="sm"
intent="primary-outline"
intent="white-subtle"
icon={<MdRefresh />}
rounded
onClick={() => {
if (userData.catalogModifications?.length) {
confirmRefreshCatalogs.open();
@@ -1163,11 +1178,7 @@ function CatalogSettingsCard() {
}
}}
loading={loading}
>
{userData.catalogModifications?.length
? 'Refresh Catalogs'
: 'Fetch Catalogs'}
</Button>
/>
</div>
{!userData.catalogModifications?.length && (
@@ -1230,9 +1241,12 @@ function CatalogSettingsCard() {
<Modal
open={modalOpen}
onOpenChange={setModalOpen}
title={`Edit Catalog: ${editingCatalog?.name || editingCatalog?.id} - ${capitalise(
editingCatalog?.type
)}`}
title={
<div className="max-w-[calc(100vw-4rem)] sm:max-w-[400px] truncate">
Edit Catalog: {editingCatalog?.name || editingCatalog?.id} -{' '}
{capitalise(editingCatalog?.type)}
</div>
}
>
<form
className="space-y-4"
@@ -1321,37 +1335,81 @@ function SortableCatalogItem({
id: `${catalog.id}-${catalog.type}`,
});
const { setUserData } = useUserData();
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
const moveToTop = () => {
setUserData((prev) => {
if (!prev.catalogModifications) return prev;
const index = prev.catalogModifications.findIndex(
(c) => c.id === catalog.id && c.type === catalog.type
);
if (index <= 0) return prev;
const newMods = [...prev.catalogModifications];
const [item] = newMods.splice(index, 1);
newMods.unshift(item);
return { ...prev, catalogModifications: newMods };
});
};
const moveToBottom = () => {
setUserData((prev) => {
if (!prev.catalogModifications) return prev;
const index = prev.catalogModifications.findIndex(
(c) => c.id === catalog.id && c.type === catalog.type
);
if (index === prev.catalogModifications.length - 1) return prev;
const newMods = [...prev.catalogModifications];
const [item] = newMods.splice(index, 1);
newMods.push(item);
return { ...prev, catalogModifications: newMods };
});
};
return (
<div ref={setNodeRef} style={style}>
<div className="px-2.5 py-2 bg-[var(--background)] rounded-[--radius-md] border flex gap-3 relative">
<div className="px-2.5 py-2 bg-[var(--background)] rounded-[--radius-md] border flex gap-2 sm:gap-3 relative">
<div
className="rounded-full w-6 h-auto bg-[--muted] md:bg-[--subtle] md:hover:bg-[--subtle-highlight] cursor-move"
className="rounded-full w-6 h-auto bg-[--muted] md:bg-[--subtle] md:hover:bg-[--subtle-highlight] cursor-move flex-shrink-0"
{...attributes}
{...listeners}
/>
<div className="flex items-center gap-3 flex-1">
<p className="text-base line-clamp-1">
{catalog.name || catalog.id} - {capitalise(catalog.type)}
<div className="flex items-center gap-3 flex-1 min-w-0">
<p className="text-base line-clamp-1 truncate block">
{catalog.name ?? catalog.id} - {capitalise(catalog.type)}
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 sm:gap-2">
<Switch
value={catalog.enabled ?? true}
onValueChange={onToggleEnabled}
size="sm"
className="sm:scale-100 scale-90"
/>
<IconButton
className="rounded-full"
className="rounded-full sm:scale-100 scale-85"
icon={<BiEdit />}
intent="primary-subtle"
onClick={onEdit}
/>
<IconButton
className="rounded-full sm:scale-100 scale-85"
icon={<LuChevronsUp />}
intent="primary-subtle"
onClick={moveToTop}
/>
<IconButton
className="rounded-full sm:scale-100 scale-85"
icon={<LuChevronsDown />}
intent="primary-subtle"
onClick={moveToBottom}
/>
</div>
</div>
</div>
+171 -57
View File
@@ -740,6 +740,16 @@ function Content() {
requiredKeywords: values,
}));
}}
onValueChange={(value, index) => {
setUserData((prev) => ({
...prev,
requiredKeywords: [
...(prev.requiredKeywords || []).slice(0, index),
value,
...(prev.requiredKeywords || []).slice(index + 1),
],
}));
}}
/>
<TextInputs
label="Excluded Keywords"
@@ -752,6 +762,16 @@ function Content() {
excludedKeywords: values,
}));
}}
onValueChange={(value, index) => {
setUserData((prev) => ({
...prev,
excludedKeywords: [
...(prev.excludedKeywords || []).slice(0, index),
value,
...(prev.excludedKeywords || []).slice(index + 1),
],
}));
}}
/>
<TextInputs
label="Included Keywords"
@@ -764,6 +784,16 @@ function Content() {
includedKeywords: values,
}));
}}
onValueChange={(value, index) => {
setUserData((prev) => ({
...prev,
includedKeywords: [
...(prev.includedKeywords || []).slice(0, index),
value,
...(prev.includedKeywords || []).slice(index + 1),
],
}));
}}
/>
<TextInputs
label="Preferred Keywords"
@@ -776,6 +806,16 @@ function Content() {
preferredKeywords: values,
}));
}}
onValueChange={(value, index) => {
setUserData((prev) => ({
...prev,
preferredKeywords: [
...(prev.preferredKeywords || []).slice(0, index),
value,
...(prev.preferredKeywords || []).slice(index + 1),
],
}));
}}
/>
</div>
</PageWrapper>
@@ -818,6 +858,16 @@ function Content() {
requiredRegexPatterns: values,
}));
}}
onValueChange={(value, index) => {
setUserData((prev) => ({
...prev,
requiredRegexPatterns: [
...(prev.requiredRegexPatterns || []).slice(0, index),
value,
...(prev.requiredRegexPatterns || []).slice(index + 1),
],
}));
}}
/>
<TextInputs
label="Excluded Regex"
@@ -830,6 +880,16 @@ function Content() {
excludedRegexPatterns: values,
}));
}}
onValueChange={(value, index) => {
setUserData((prev) => ({
...prev,
excludedRegexPatterns: [
...(prev.excludedRegexPatterns || []).slice(0, index),
value,
...(prev.excludedRegexPatterns || []).slice(index + 1),
],
}));
}}
/>
<TextInputs
label="Included Regex"
@@ -842,16 +902,26 @@ function Content() {
includedRegexPatterns: values,
}));
}}
onValueChange={(value, index) => {
setUserData((prev) => ({
...prev,
includedRegexPatterns: [
...(prev.includedRegexPatterns || []).slice(0, index),
value,
...(prev.includedRegexPatterns || []).slice(index + 1),
],
}));
}}
/>
<TwoTextInputs
title="Preferred Regex"
description="Streams that contain any of these regular expressions will be preferred. Give each regex a name which will appear in your stream if the format you use supports it."
title="Preferred Regex Patterns"
description="Define regex patterns with names for easy reference"
keyName="Name"
keyId="name"
keyPlaceholder="Enter the name of this regex here"
valueName="Regex"
keyPlaceholder="Enter pattern name"
valueId="pattern"
valuePlaceholder="Enter the actual regex pattern here"
valueName="Pattern"
valuePlaceholder="Enter regex pattern"
values={(userData.preferredRegexPatterns || []).map(
(pattern) => ({
name: pattern.name,
@@ -861,12 +931,38 @@ function Content() {
onValuesChange={(values) => {
setUserData((prev) => ({
...prev,
preferredRegexPatterns: values.map((value) => ({
name: value.name,
pattern: value.value,
preferredRegexPatterns: values.map((v) => ({
name: v.name,
pattern: v.value,
})),
}));
}}
onValueChange={(value, index) => {
setUserData((prev) => ({
...prev,
preferredRegexPatterns: [
...(prev.preferredRegexPatterns || []).slice(0, index),
{
...(prev.preferredRegexPatterns || [])[index],
pattern: value,
},
...(prev.preferredRegexPatterns || []).slice(index + 1),
],
}));
}}
onKeyChange={(key, index) => {
setUserData((prev) => ({
...prev,
preferredRegexPatterns: [
...(prev.preferredRegexPatterns || []).slice(0, index),
{
...(prev.preferredRegexPatterns || [])[index],
name: key,
},
...(prev.preferredRegexPatterns || []).slice(index + 1),
],
}));
}}
/>
</div>
</PageWrapper>
@@ -885,12 +981,15 @@ function Content() {
help="Global limit for all results"
label="Global Limit"
value={userData.resultLimits?.global || undefined}
min={1}
min={0}
defaultValue={undefined}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
resultLimits: { ...prev.resultLimits, global: value },
resultLimits: {
...prev.resultLimits,
global: value || undefined,
},
}));
}}
/>
@@ -898,12 +997,15 @@ function Content() {
help="Limit for results by service"
label="Service Limit"
value={userData.resultLimits?.service || undefined}
min={1}
min={0}
defaultValue={undefined}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
resultLimits: { ...prev.resultLimits, service: value },
resultLimits: {
...prev.resultLimits,
service: value || undefined,
},
}));
}}
/>
@@ -911,12 +1013,15 @@ function Content() {
help="Limit for results by addon"
label="Addon Limit"
value={userData.resultLimits?.addon || undefined}
min={1}
min={0}
defaultValue={undefined}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
resultLimits: { ...prev.resultLimits, addon: value },
resultLimits: {
...prev.resultLimits,
addon: value || undefined,
},
}));
}}
/>
@@ -924,14 +1029,14 @@ function Content() {
help="Limit for results by resolution"
label="Resolution Limit"
value={userData.resultLimits?.resolution || undefined}
min={1}
min={0}
defaultValue={undefined}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
resultLimits: {
...prev.resultLimits,
resolution: value,
resolution: value || undefined,
},
}));
}}
@@ -940,12 +1045,15 @@ function Content() {
help="Limit for results by quality"
label="Quality Limit"
value={userData.resultLimits?.quality || undefined}
min={1}
min={0}
defaultValue={undefined}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
resultLimits: { ...prev.resultLimits, quality: value },
resultLimits: {
...prev.resultLimits,
quality: value || undefined,
},
}));
}}
/>
@@ -953,12 +1061,15 @@ function Content() {
help="Limit for results by indexer"
label="Indexer Limit"
value={userData.resultLimits?.indexer || undefined}
min={1}
min={0}
defaultValue={undefined}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
resultLimits: { ...prev.resultLimits, indexer: value },
resultLimits: {
...prev.resultLimits,
indexer: value || undefined,
},
}));
}}
/>
@@ -966,14 +1077,14 @@ function Content() {
help="Limit for results by release group"
label="Release Group Limit"
value={userData.resultLimits?.releaseGroup || undefined}
min={1}
min={0}
defaultValue={undefined}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
resultLimits: {
...prev.resultLimits,
releaseGroup: value,
releaseGroup: value || undefined,
},
}));
}}
@@ -1410,6 +1521,7 @@ type TextInputProps = {
help: string; // help text that shows below the label
values: string[];
onValuesChange: (values: string[]) => void;
onValueChange: (value: string, index: number) => void;
placeholder?: string;
};
@@ -1421,6 +1533,7 @@ function TextInputs({
help,
values,
onValuesChange,
onValueChange,
placeholder,
}: TextInputProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -1471,13 +1584,14 @@ function TextInputs({
value={value}
label={itemName}
placeholder={placeholder}
onValueChange={(value) =>
onValuesChange([
...values.slice(0, index),
value,
...values.slice(index + 1),
])
}
onValueChange={(value) => onValueChange(value, index)}
// onValueChange={(value) =>
// onValuesChange([
// ...values.slice(0, index),
// value,
// ...values.slice(index + 1),
// ])
// }
/>
</div>
<IconButton
@@ -1556,6 +1670,8 @@ type KeyValueInputProps = {
valuePlaceholder: string;
values: { name: string; value: string }[];
onValuesChange: (values: { name: string; value: string }[]) => void;
onValueChange: (value: string, index: number) => void;
onKeyChange: (key: string, index: number) => void;
};
function TwoTextInputs({
@@ -1569,6 +1685,8 @@ function TwoTextInputs({
valuePlaceholder,
values,
onValuesChange,
onValueChange,
onKeyChange,
}: KeyValueInputProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -1602,11 +1720,17 @@ function TwoTextInputs({
if (
Array.isArray(data) &&
data.every(
(value: { name: string; value: string }) =>
typeof value.name === 'string' && typeof value.value === 'string'
(value: { [key: string]: string }) =>
typeof value[keyId] === 'string' &&
typeof value[valueId] === 'string'
)
) {
onValuesChange(data);
onValuesChange(
data.map((v: { [key: string]: string }) => ({
name: v[keyId],
value: v[valueId],
}))
);
}
} catch (error) {
console.error('Error importing file:', error);
@@ -1622,31 +1746,21 @@ function TwoTextInputs({
<SettingsCard title={title} description={description}>
{values.map((value, index) => (
<div key={index} className="flex gap-2">
<div className="flex-1 flex gap-2">
<div className="flex-1">
<TextInput
value={value.name}
label={keyName}
placeholder={keyPlaceholder}
onValueChange={(value) => {
const newValues = [...values];
newValues[index].name = value;
onValuesChange(newValues);
}}
/>
</div>
<div className="flex-1">
<TextInput
value={value.value}
label={valueName}
placeholder={valuePlaceholder}
onValueChange={(value) => {
const newValues = [...values];
newValues[index].value = value;
onValuesChange(newValues);
}}
/>
</div>
<div className="flex-1">
<TextInput
value={value.name}
label={keyName}
placeholder={keyPlaceholder}
onValueChange={(newValue) => onKeyChange(newValue, index)}
/>
</div>
<div className="flex-1">
<TextInput
value={value.value}
label={valueName}
placeholder={valuePlaceholder}
onValueChange={(newValue) => onValueChange(newValue, index)}
/>
</div>
<IconButton
size="sm"