Test filter list by item instead of sorting the whole array of items
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user