From 111be874282edc71c71b2ec61f35bcb1d6a2dcff Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 4 Mar 2026 02:07:54 +1300 Subject: [PATCH] Test filter list by item instead of sorting the whole array of items --- .../lib/components/layout/ItemsList.svelte | 76 +++++++-------- materialious/src/lib/filtering.ts | 92 +++++++++---------- materialious/src/lib/store.ts | 6 +- 3 files changed, 85 insertions(+), 89 deletions(-) diff --git a/materialious/src/lib/components/layout/ItemsList.svelte b/materialious/src/lib/components/layout/ItemsList.svelte index 21b06902..b166242a 100644 --- a/materialious/src/lib/components/layout/ItemsList.svelte +++ b/materialious/src/lib/components/layout/ItemsList.svelte @@ -14,7 +14,7 @@ import { SpatialMenu } from 'melt/builders'; import { mergeAttrs } from 'melt'; import { Capacitor } from '@capacitor/core'; - import { filterByBlocklist } from '$lib/filtering'; + import { isItemFiltered } from '$lib/filtering'; interface Props { items?: FeedItems; @@ -30,8 +30,6 @@ classes = 'page right active' }: Props = $props(); - items = filterByBlocklist(items); - async function removePlaylistItem(indexId: string) { if (!playlistId) return; await removePlaylistVideo(playlistId, indexId); @@ -88,42 +86,44 @@ {/if}
{#each items as item, index (index)} - {@const uniqueItemId = extractUniqueId(item)} - {@const spatialItem = spatialMenu.getItem(item, { onSelect: () => goToItem(uniqueItemId) })} - -
goToItem(uniqueItemId), - id: uniqueItemId - })} - class="no-padding item-select border" - class:item-select-focused={spatialItem.highlighted} - style="height: 100%;" - > - {#if item.type === 'video' || item.type === 'shortVideo' || item.type === 'stream' || item.type === 'historyVideo'} - {#key item.videoId} - - {/key} - {#if $invidiousAuthStore && decodeURIComponent($invidiousAuthStore.username) === playlistAuthor && 'indexId' in item} -
- -
+ {#if !isItemFiltered(item)} + {@const uniqueItemId = extractUniqueId(item)} + {@const spatialItem = spatialMenu.getItem(item, { onSelect: () => goToItem(uniqueItemId) })} + +
goToItem(uniqueItemId), + id: uniqueItemId + })} + class="no-padding item-select border" + class:item-select-focused={spatialItem.highlighted} + style="height: 100%;" + > + {#if item.type === 'video' || item.type === 'shortVideo' || item.type === 'stream' || item.type === 'historyVideo'} + {#key item.videoId} + + {/key} + {#if $invidiousAuthStore && decodeURIComponent($invidiousAuthStore.username) === playlistAuthor && 'indexId' in item} +
+ +
+ {/if} + {:else if item.type === 'channel'} + + {:else if item.type === 'playlist'} + + {:else if item.type === 'hashtag'} + {/if} - {:else if item.type === 'channel'} - - {:else if item.type === 'playlist'} - - {:else if item.type === 'hashtag'} - - {/if} -
-
+
+
+ {/if} {/each}
diff --git a/materialious/src/lib/filtering.ts b/materialious/src/lib/filtering.ts index c41a4e1e..0133aa88 100644 --- a/materialious/src/lib/filtering.ts +++ b/materialious/src/lib/filtering.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import type { FeedItem } from './feed'; import isSafeRegex from 'safe-regex2'; -import { blocklistStore } from './store'; +import { filterContentListStore } from './store'; import { get } from 'svelte/store'; const zFilterOperatorEnum = z.enum([ @@ -33,62 +33,58 @@ const zFilterGroup = z.object({ operator: z.enum(['AND', 'OR']).optional() // Logical grouping of conditions }); -export const zBlockListSchema = z.array(zFilterGroup); +export const zFilterSchema = z.array(zFilterGroup); -const zBlocklistRootSchema = z.object({ +const zFilterRootSchema = z.object({ version: z.literal('v1'), for: z.literal('materialious'), - blocklist: zBlockListSchema + filterBy: zFilterSchema }); -export function filterByBlocklist(data: T[]): T[] { - const blocklist = get(blocklistStore); - if (!blocklist) return data; +export function isItemFiltered(item: FeedItem): boolean { + const filteredContent = get(filterContentListStore); + if (!filteredContent) return false; - return data.filter((item) => { - return blocklist.every((filterGroup) => { - return filterGroup.conditions.every((condition) => { - if (!(condition.field in item)) return false; + return filteredContent.every((filterGroup) => { + return filterGroup.conditions.every((condition) => { + if (!(condition.field in item)) return false; - const fieldValue = item[condition.field as keyof T]; + const fieldValue = item[condition.field as keyof FeedItem]; - switch (condition.operator) { - case 'equals': - return fieldValue === condition.value; - case 'in': - return ( - Array.isArray(condition.value) && (condition.value as any[]).includes(fieldValue) - ); - case 'like': - return ( - typeof fieldValue === 'string' && - typeof condition.value === 'string' && - fieldValue.includes(condition.value) - ); - case 'gt': - return ( - typeof fieldValue === 'number' && - typeof condition.value === 'number' && - fieldValue > condition.value - ); - case 'lt': - return ( - typeof fieldValue === 'number' && - typeof condition.value === 'number' && - fieldValue < condition.value - ); - case 'regex': - if (typeof condition.value !== 'string' || !isSafeRegex(condition.value)) return false; - return typeof fieldValue === 'string' && new RegExp(condition.value).test(fieldValue); - default: - return false; - } - }); + switch (condition.operator) { + case 'equals': + return fieldValue === condition.value; + case 'in': + return Array.isArray(condition.value) && (condition.value as any[]).includes(fieldValue); + case 'like': + return ( + typeof fieldValue === 'string' && + typeof condition.value === 'string' && + fieldValue.includes(condition.value) + ); + case 'gt': + return ( + typeof fieldValue === 'number' && + typeof condition.value === 'number' && + fieldValue > condition.value + ); + case 'lt': + return ( + typeof fieldValue === 'number' && + typeof condition.value === 'number' && + fieldValue < condition.value + ); + case 'regex': + if (typeof condition.value !== 'string' || !isSafeRegex(condition.value)) return false; + return typeof fieldValue === 'string' && new RegExp(condition.value).test(fieldValue); + default: + return false; + } }); }); } -export async function loadBlockListFromRemote(url: string) { +export async function loadContentFilterFromURL(url: string) { const resp = await fetch(url, { method: 'GET', credentials: 'omit' }); if (!resp.ok) throw new Error('Response status code'); @@ -101,9 +97,9 @@ export async function loadBlockListFromRemote(url: string) { if (!respJson) throw new Error('Invalid JSON'); - const parsedBlocklist = zBlocklistRootSchema.safeParse(respJson); + const parsedFilterList = zFilterRootSchema.safeParse(respJson); - if (!parsedBlocklist.success) throw new Error(parsedBlocklist.error.message); + if (!parsedFilterList.success) throw new Error(parsedFilterList.error.message); - blocklistStore.set(parsedBlocklist.data.blocklist); + filterContentListStore.set(parsedFilterList.data.filterBy); } diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index d699db3b..63e5e3c5 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -23,7 +23,7 @@ import type { ParsedDescription } from './description'; import { ensureNoTrailingSlash, getPublicEnv } from './misc'; import type { EngineFallback } from './api/misc'; import type z from 'zod'; -import type { zBlockListSchema } from './filtering'; +import type { zFilterSchema } from './filtering'; function createListenerFunctions(): { callListeners: (eventKey: string, newValue: any) => void; @@ -341,10 +341,10 @@ export const searchHistoryStore: Writable = persist( 'searchHistory' ); -export const blocklistStore: Writable | undefined> = persist( +export const filterContentListStore: Writable | undefined> = persist( writable(), createStorage(), - 'blocklist' + 'filterContentList' ); export const feedLoadingStore: Writable = writable(false);