diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx
index a24558e..1a34e55 100644
--- a/src/screens/SearchScreen.tsx
+++ b/src/screens/SearchScreen.tsx
@@ -6,6 +6,7 @@ import { posterPrefsFromState, type PosterPrefs } from '../core/posterPrefs';
import { addRecentSearch, clearRecentSearches, loadRecentSearches, removeRecentSearch, type RecentSearch } from '../core/searchHistory';
import type { AppState, HomeCategory, Meta } from '../core/types';
import { getLanguage, t } from '../i18n';
+import { coreInvoke } from '../core/engine';
interface Props {
state: AppState;
@@ -37,25 +38,53 @@ export const SearchScreen = React.memo(function SearchScreen({ state, onDispatch
const search = state.search;
const posterPrefs = posterPrefsFromState(state, 0.85);
const trimmedQuery = query.trim();
+ const lastRecentQueryRef = useRef('');
+ const [screenPlan, setScreenPlan] = useState<{
+ query: string;
+ queryEligible: boolean;
+ shouldDispatch: boolean;
+ shouldCache: boolean;
+ categories: HomeCategory[];
+ resultCount: number;
+ categoryCount: number;
+ isLoading: boolean;
+ }>({ query: '', queryEligible: false, shouldDispatch: false, shouldCache: false, categories: [], resultCount: 0, categoryCount: 0, isLoading: false });
useEffect(() => {
loadRecentSearches().then(setRecentSearches);
}, []);
- useEffect(() => {
- if (trimmedQuery.length < 2) return;
- void addRecentSearch(trimmedQuery, recentSearches).then(setRecentSearches);
- if (searchResultsCache.has(trimmedQuery)) return;
- onDispatch(JSON.stringify({ type: 'searchRequested', query: trimmedQuery, language: getLanguage() }));
- }, [trimmedQuery, onDispatch]);
-
- const resultsMatchCurrentQuery = search.query === trimmedQuery;
- if (resultsMatchCurrentQuery && (search.categories?.length ?? 0) > 0) {
- searchResultsCache.set(trimmedQuery, search.categories as HomeCategory[]);
- }
const cachedCategories = searchResultsCache.get(trimmedQuery) ?? null;
- const rawCategories = resultsMatchCurrentQuery ? (search.categories ?? cachedCategories ?? []) : (cachedCategories ?? []);
- const isLoading = search.isLoading && !cachedCategories;
+ useEffect(() => {
+ let active = true;
+ void coreInvoke
('searchScreenPlan', JSON.stringify({
+ query,
+ searchQuery: search.query,
+ searchCategories: search.categories ?? [],
+ cachedCategories: cachedCategories ?? [],
+ hasCache: cachedCategories != null,
+ searchLoading: search.isLoading,
+ typeFilter,
+ })).then((plan) => {
+ if (!active || !plan) return;
+ if (plan.shouldCache) searchResultsCache.set(plan.query, search.categories as HomeCategory[]);
+ setScreenPlan(plan);
+ });
+ return () => { active = false; };
+ }, [query, search.query, search.categories, search.isLoading, cachedCategories, typeFilter]);
+
+ useEffect(() => {
+ if (!screenPlan.queryEligible || screenPlan.query !== trimmedQuery) return;
+ if (lastRecentQueryRef.current !== screenPlan.query) {
+ lastRecentQueryRef.current = screenPlan.query;
+ void addRecentSearch(screenPlan.query, recentSearches).then(setRecentSearches);
+ }
+ if (screenPlan.shouldDispatch) onDispatch(JSON.stringify({ type: 'searchRequested', query: screenPlan.query, language: getLanguage() }));
+ }, [screenPlan.query, screenPlan.queryEligible, screenPlan.shouldDispatch, trimmedQuery, onDispatch]);
+
+ const categories = screenPlan.categories;
+ const resultCount = screenPlan.resultCount;
+ const isLoading = screenPlan.isLoading;
const handleGenreClick = (genreKey: string) => {
onQueryChange(t(genreKey));
@@ -78,23 +107,6 @@ export const SearchScreen = React.memo(function SearchScreen({ state, onDispatch
void clearRecentSearches().then(setRecentSearches);
};
- const categories = useMemo(
- () =>
- rawCategories
- .map((category) => ({
- ...category,
- items: typeFilter
- ? category.items.filter((meta) => meta.type === typeFilter)
- : category.items,
- }))
- .filter((category) => category.items.length > 0),
- [rawCategories, typeFilter],
- );
- const resultCount = useMemo(
- () => categories.reduce((sum, category) => sum + category.items.length, 0),
- [categories],
- );
-
return (
@@ -107,7 +119,7 @@ export const SearchScreen = React.memo(function SearchScreen({ state, onDispatch
{t('auto.search_results')}
{query.trim() ? query.trim() : t('auto.search')}
{query.trim().length >= 2 && !isLoading && (
-
{t('search.results_across_catalogs', resultCount, categories.length)}
+
{t('search.results_across_catalogs', resultCount, screenPlan.categoryCount)}
)}
diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx
index 650c2d9..9d196c7 100644
--- a/src/screens/SettingsScreen.tsx
+++ b/src/screens/SettingsScreen.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { coreApplyPreferenceUpdate, httpFetchText, storageRead, storageWrite } from '../core/engine';
+import { coreApplyPreferenceUpdate, coreInvoke, httpFetchText, storageRead, storageWrite } from '../core/engine';
import { Keyboard, Search } from 'lucide-react';
import {
coreAddonCollectionMutationPlan,
@@ -44,41 +44,6 @@ import { AddonsSection } from '../components/settings/AddonsSection';
import { DownloadsSection } from '../components/settings/DownloadsSection';
import { AddonAddedDialog } from '../components/AddonAddedDialog';
-function mergeAddons(existing: AddonDescriptor[], incoming: AddonDescriptor[]): AddonDescriptor[] {
- const merged = new Map
();
- for (const addon of existing) merged.set(addonKey(addon), addon);
- for (const addon of incoming) merged.set(addonKey(addon), addon);
- return [...merged.values()];
-}
-
-function addonUrlIdentity(url: string): string {
- return url
- .trim()
- .replace(/\/+$/, '')
- .replace(/^https?:\/\//i, '')
- .toLowerCase();
-}
-
-function profileLocalAddons(profile: UserProfile | null): string[] {
- return profile?.addonSettings?.localAddons ?? profile?.localAddons ?? [];
-}
-
-function withInstalledLocalAddon(profile: UserProfile, normalizedUrl: string): UserProfile {
- const existing = profileLocalAddons(profile);
- const next = existing.some((url) => addonUrlIdentity(url) === addonUrlIdentity(normalizedUrl))
- ? existing
- : [...existing, normalizedUrl];
- return {
- ...profile,
- localAddons: next,
- addonSettings: {
- ...(profile.addonSettings ?? {}),
- localAddons: next,
- disabledLocalAddons: profile.addonSettings?.disabledLocalAddons ?? profile.disabledLocalAddons ?? [],
- },
- };
-}
-
async function settingsFetchJson(url: string): Promise {
const response = await httpFetchText(url);
if (response.statusCode < 200 || response.statusCode > 299) {
@@ -213,7 +178,7 @@ export function SettingsScreen({ state, onDispatch, activeProfile, onProfileUpda
if (engineAddons.length > 0) {
loadAddons().then((stored) => {
coreAddonCollectionMutationPlan({ existing: stored, incoming: engineAddons })
- .then((plan) => ((plan?.addons as AddonDescriptor[] | undefined) ?? mergeAddons(stored, engineAddons)))
+ .then((plan) => ((plan?.addons as AddonDescriptor[] | undefined) ?? stored))
.then((merged) => {
setInstalledAddons(merged);
});
@@ -264,12 +229,12 @@ export function SettingsScreen({ state, onDispatch, activeProfile, onProfileUpda
const normalizedAddon = await normalizeAddonDescriptor({ ...addon, transportUrl: normalizedUrl });
const stored = await loadAddons();
const plan = await coreAddonCollectionMutationPlan({ existing: stored, incoming: [normalizedAddon] });
- const updated = await Promise.all(((plan?.addons as AddonDescriptor[] | undefined) ?? mergeAddons(stored, [normalizedAddon])).map(normalizeAddonDescriptor));
+ const updated = await Promise.all(((plan?.addons as AddonDescriptor[] | undefined) ?? stored).map(normalizeAddonDescriptor));
await saveAddons(updated);
let syncProfile = activeProfile;
if (activeProfile) {
- const updatedProfile = withInstalledLocalAddon(activeProfile, normalizedUrl);
+ const updatedProfile = (await coreInvoke('addonProfileMutationPlan', JSON.stringify({ profile: activeProfile, command: 'install', addonKey: normalizedUrl }))) ?? activeProfile;
await saveProfile(updatedProfile);
onProfileUpdated(updatedProfile);
syncProfile = updatedProfile;
@@ -295,16 +260,7 @@ export function SettingsScreen({ state, onDispatch, activeProfile, onProfileUpda
await saveAddons(updated);
setInstalledAddons(updated);
if (activeProfile) {
- const nextUrls = profileLocalAddons(activeProfile).filter((url) => addonUrlIdentity(url) !== addonUrlIdentity(removeKey));
- const updatedProfile: UserProfile = {
- ...activeProfile,
- localAddons: nextUrls,
- addonSettings: {
- ...(activeProfile.addonSettings ?? {}),
- localAddons: nextUrls,
- disabledLocalAddons: activeProfile.addonSettings?.disabledLocalAddons ?? activeProfile.disabledLocalAddons ?? [],
- },
- };
+ const updatedProfile = (await coreInvoke('addonProfileMutationPlan', JSON.stringify({ profile: activeProfile, command: 'remove', addonKey: removeKey }))) ?? activeProfile;
await saveProfile(updatedProfile);
onProfileUpdated(updatedProfile);
void syncNuvioAddons(updatedProfile, updated);
@@ -322,17 +278,7 @@ export function SettingsScreen({ state, onDispatch, activeProfile, onProfileUpda
const handleToggleAddon = async (addon: AddonDescriptor) => {
if (!activeProfile) return;
const key = addonKey(addon);
- const disabled = activeProfile.addonSettings?.disabledLocalAddons ?? activeProfile.disabledLocalAddons ?? [];
- const isDisabled = disabled.includes(key);
- const nextDisabled = isDisabled ? disabled.filter((k) => k !== key) : [...disabled, key];
- const updatedProfile: UserProfile = {
- ...activeProfile,
- addonSettings: {
- ...(activeProfile.addonSettings ?? {}),
- localAddons: profileLocalAddons(activeProfile),
- disabledLocalAddons: nextDisabled,
- },
- };
+ const updatedProfile = (await coreInvoke('addonProfileMutationPlan', JSON.stringify({ profile: activeProfile, command: 'toggle', addonKey: key }))) ?? activeProfile;
await saveProfile(updatedProfile);
onProfileUpdated(updatedProfile);
void syncNuvioAddons(updatedProfile, installedAddons);