Test filter list by item instead of sorting the whole array of items

This commit is contained in:
WardPearce
2026-03-04 02:07:54 +13:00
parent 865dbbcd28
commit 111be87428
3 changed files with 85 additions and 89 deletions
@@ -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}
<div class="grid" {...spatialMenu.root}>
{#each items as item, index (index)}
{@const uniqueItemId = extractUniqueId(item)}
{@const spatialItem = spatialMenu.getItem(item, { onSelect: () => goToItem(uniqueItemId) })}
<ContentColumn>
<article
{...mergeAttrs(spatialItem.attrs, {
onclick: () => 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}
<Thumbnail video={item} {playlistId} />
{/key}
{#if $invidiousAuthStore && decodeURIComponent($invidiousAuthStore.username) === playlistAuthor && 'indexId' in item}
<div class="right-align" style="margin: 1em .5em;">
<button
onclick={async () => removePlaylistItem(item.indexId)}
class="tertiary circle small"
>
<i>delete</i>
<div class="tooltip">{$_('delete')}</div>
</button>
</div>
{#if !isItemFiltered(item)}
{@const uniqueItemId = extractUniqueId(item)}
{@const spatialItem = spatialMenu.getItem(item, { onSelect: () => goToItem(uniqueItemId) })}
<ContentColumn>
<article
{...mergeAttrs(spatialItem.attrs, {
onclick: () => 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}
<Thumbnail video={item} {playlistId} />
{/key}
{#if $invidiousAuthStore && decodeURIComponent($invidiousAuthStore.username) === playlistAuthor && 'indexId' in item}
<div class="right-align" style="margin: 1em .5em;">
<button
onclick={async () => removePlaylistItem(item.indexId)}
class="tertiary circle small"
>
<i>delete</i>
<div class="tooltip">{$_('delete')}</div>
</button>
</div>
{/if}
{:else if item.type === 'channel'}
<ChannelThumbnail channel={item} />
{:else if item.type === 'playlist'}
<PlaylistThumbnail playlist={item} />
{:else if item.type === 'hashtag'}
<HashtagThumbnail hashtag={item} />
{/if}
{:else if item.type === 'channel'}
<ChannelThumbnail channel={item} />
{:else if item.type === 'playlist'}
<PlaylistThumbnail playlist={item} />
{:else if item.type === 'hashtag'}
<HashtagThumbnail hashtag={item} />
{/if}
</article>
</ContentColumn>
</article>
</ContentColumn>
{/if}
{/each}
</div>
</div>
+44 -48
View File
@@ -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<T extends FeedItem>(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);
}
+3 -3
View File
@@ -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<string[]> = persist(
'searchHistory'
);
export const blocklistStore: Writable<z.infer<typeof zBlockListSchema> | undefined> = persist(
export const filterContentListStore: Writable<z.infer<typeof zFilterSchema> | undefined> = persist(
writable(),
createStorage(),
'blocklist'
'filterContentList'
);
export const feedLoadingStore: Writable<boolean> = writable(false);