diff --git a/docs/CONTENT-FILTERS.md b/docs/CONTENT-FILTERS.md new file mode 100644 index 00000000..d6fc126e --- /dev/null +++ b/docs/CONTENT-FILTERS.md @@ -0,0 +1,64 @@ +# What is Content filters? +Content filters allows you to conditionally filter content on Materialious based off attributes belonging to Channels & Videos. This allows you to hide content you find offensive, dislike or want to avoid. + +# What is "Import Filters from URL" +Materialious allows you to import content filters from other sites, this can be powerful tool as you can write scripts what follows Materialious schema allowing you to generate massive content filters. + +Enabling "Automatically update filter from URL" will mean Materialious automatically pulls any changes to the filter list from the provided URL. + +# Understanding content filters + +## Filter/Content type +Each filter represents a content type ("Video" or "Channel".) This content type dictates what attributes are available like author id, video id etc... + +Each content type has one or more conditionals. These conditionals define what attributes must equal in order to filter a piece of content. + +## Conditionals +### Field +This represents the attribute of the content type. + +### Operators +- `equals` - Value must match exactly. +- `in` - Value is in an array. +- `like` - Value is in the string, e.g. "XYZ" is in "Channel XYZ". +- `gt` - Greater then value. +- `lt` - Less then value. +- `regex` - Define regex expression. + +#### Value +Each conditional can have multiple values, each value acts as an "OR" operator. So if one value matches then the piece of content is filtered. + +# Writing our schema with JSON (Advanced) +```json +{ + "version": "v2", // Must be present + "createdFor": "materialious", // Must be present + "filterBy": [ // Conditionals to filter by + { + "conditions": [ + { + "operator": "equals", // Exact match + "field": "authorId", // authorId attribute from video content type + "values": [ + "UCH-_hzb2ILSCo9ftVSnrCIQ", // Filter this video ID + "UCLqH-U2TXzj1h7lyYQZLNQQ" // or this video ID. + ] + } + ], + "type": "video" // The content type + }, + { + "conditions": [ + { + "operator": "equals", + "field": "authorId", + "values": [ + "UCH-_hzb2ILSCo9ftVSnrCIQ" + ] + } + ], + "type": "channel" + } + ] +} +``` diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index a867ecd3..b29e87e7 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 229 - versionName "1.16.12" + versionCode 230 + versionName "1.16.13" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index 89d245d8..4f7cacea 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -71,7 +71,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.16.13 + + https://github.com/Materialious/Materialious/releases/tag/1.16.12 diff --git a/materialious/electron/package.json b/materialious/electron/package.json index e23383a7..4381d1a3 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.16.12", + "version": "1.16.13", "description": "Modern material design for YouTube and Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package.json b/materialious/package.json index 5f7f56e7..186ddb7b 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.16.12", + "version": "1.16.13", "private": true, "scripts": { "dev": "npm run patch:github && vite dev", @@ -101,4 +101,4 @@ "youtubei.js": "^16.0.1", "zod": "^4.3.6" } -} +} \ No newline at end of file diff --git a/materialious/src/lib/api/invidious/channel.ts b/materialious/src/lib/api/invidious/channel.ts index 98212cb3..a49e4e84 100644 --- a/materialious/src/lib/api/invidious/channel.ts +++ b/materialious/src/lib/api/invidious/channel.ts @@ -1,3 +1,4 @@ +import { associateAvatar } from '$lib/thumbnail'; import type { ChannelContent, ChannelOptions, ChannelPage } from '../model'; import { buildPath, fetchErrorHandle } from './request'; @@ -8,7 +9,13 @@ export async function getChannelInvidious( const resp = await fetchErrorHandle( await fetch(buildPath(`channels/${channelId}`), fetchOptions) ); - return await resp.json(); + const respJson = await resp.json(); + + if (resp.ok) { + associateAvatar(channelId, respJson.authorBanners); + } + + return respJson; } export async function getChannelContentInvidious( diff --git a/materialious/src/lib/api/invidious/video.ts b/materialious/src/lib/api/invidious/video.ts index 0bd15d5b..bfd3fd23 100644 --- a/materialious/src/lib/api/invidious/video.ts +++ b/materialious/src/lib/api/invidious/video.ts @@ -4,6 +4,7 @@ import { buildPath, fetchErrorHandle } from './request'; import { getVideoYTjs } from '../youtubejs/video'; import { playerYouTubeJsFallback } from '$lib/store'; import { isUnrestrictedPlatform } from '$lib/misc'; +import { associateAvatar } from '$lib/thumbnail'; export async function getVideoInvidious( videoId: string, @@ -20,5 +21,12 @@ export async function getVideoInvidious( } else { await fetchErrorHandle(resp); } - return await resp.json(); + + const respJson = await resp.json(); + + if (resp.ok) { + associateAvatar(respJson.authorId, respJson.authorThumbnails); + } + + return respJson; } diff --git a/materialious/src/lib/api/youtubejs/channel.ts b/materialious/src/lib/api/youtubejs/channel.ts index fd77bc6a..82240788 100644 --- a/materialious/src/lib/api/youtubejs/channel.ts +++ b/materialious/src/lib/api/youtubejs/channel.ts @@ -9,6 +9,7 @@ import type { Video } from '../model'; import { invidiousItemSchema } from './schema'; +import { associateAvatar } from '$lib/thumbnail'; export async function getChannelYTjs(channelId: string): Promise { const innertube = await getInnertube(); @@ -23,6 +24,7 @@ export async function getChannelYTjs(channelId: string): Promise { innerResults.header.content?.is(YTNodes.PageHeaderView) ) { authorBanners = innerResults.header.content.banner?.image ?? []; + associateAvatar(channelId, authorBanners); } const description = innerResults.metadata.description ?? ''; diff --git a/materialious/src/lib/api/youtubejs/video.ts b/materialious/src/lib/api/youtubejs/video.ts index f9d89d3e..1c245cf1 100644 --- a/materialious/src/lib/api/youtubejs/video.ts +++ b/materialious/src/lib/api/youtubejs/video.ts @@ -17,6 +17,7 @@ import { Utils, YT, YTNodes, Platform, type IGetChallengeResponse } from 'youtub import { getInnertube } from '.'; import { isUnrestrictedPlatform } from '$lib/misc'; import { webPoTokenMinter } from '$lib/web/youtube/minter'; +import { associateAvatar } from '$lib/thumbnail'; Platform.shim.eval = async ( data: Types.BuildScriptResult, @@ -149,6 +150,8 @@ export async function getVideoYTjs(videoId: string): Promise { if (video.basic_info.channel_id) { const channel = await innertube.getChannel(video.basic_info.channel_id); authorThumbnails = channel.metadata.avatar as Image[]; + + associateAvatar(video.basic_info.channel_id, authorThumbnails); } else { authorThumbnails = []; } diff --git a/materialious/src/lib/components/settings/ExportImport.svelte b/materialious/src/lib/components/settings/ExportImport.svelte index 270839d6..d72ef15b 100644 --- a/materialious/src/lib/components/settings/ExportImport.svelte +++ b/materialious/src/lib/components/settings/ExportImport.svelte @@ -92,7 +92,7 @@ } }); }} - accept=".json,.opml,.csv" + accept=".json,.opml,.csv,.db" type="file" /> diff --git a/materialious/src/lib/components/settings/Filters.svelte b/materialious/src/lib/components/settings/Filters.svelte index b62d647c..8ae738d0 100644 --- a/materialious/src/lib/components/settings/Filters.svelte +++ b/materialious/src/lib/components/settings/Filters.svelte @@ -11,6 +11,7 @@ import type z from 'zod'; import { addToast } from '../Toast.svelte'; import { Clipboard } from '@capacitor/clipboard'; + import { downloadStringAsFile } from '$lib/misc'; let remoteFilterListUrl: string = $state($filterContentUrlStore ?? ''); let remoteError: string = $state(''); @@ -48,17 +49,21 @@ } } - async function exportAsJSON() { + function filtersAsJSON() { + return JSON.stringify( + { + version: 'v2', + createdFor: 'materialious', + filterBy: contentFilters + }, + null, + 2 + ); + } + + async function exportToClipboard() { await Clipboard.write({ - string: JSON.stringify( - { - version: 'v1', - createdFor: 'materialious', - filterBy: contentFilters - }, - null, - 2 - ) + string: filtersAsJSON() }); addToast({ @@ -93,6 +98,14 @@

{$_('layout.backendEngine.warning')}

+

+ Need Help? Check out our guide. +

@@ -134,9 +147,16 @@ {#if contentFilters} {#if contentFilters.length > 0} - +
{/if} @@ -167,9 +187,9 @@
{#if filter.conditions}
    - {#each filter.conditions as condition (condition)} + {#each filter.conditions as condition, conditionIndex (condition)}
  • - + + {#if condition.values.length > 0 && index < condition.values.length - 1} +
    OR
    + {/if} + {/each} + +
    +
  • + + {#if filter.conditions.length > 0 && conditionIndex < filter.conditions.length - 1} +
    AND
    + {/if} {/each}
{/if} @@ -291,8 +348,9 @@ filter.conditions.push({ operator: 'equals', field: 'author', - value: '' + values: [''] }); + filterContentListStore.set(contentFilters); filterContentUrlAutoUpdateStore.set(false); }} class="surface-container-highest" diff --git a/materialious/src/lib/components/thumbnail/VideoThumbnail.svelte b/materialious/src/lib/components/thumbnail/VideoThumbnail.svelte index d433d59a..d00a75a1 100644 --- a/materialious/src/lib/components/thumbnail/VideoThumbnail.svelte +++ b/materialious/src/lib/components/thumbnail/VideoThumbnail.svelte @@ -19,6 +19,8 @@ import { queueGetWatchHistory } from '$lib/api/historyPool'; import { page } from '$app/state'; import { getDeArrow, getThumbnailDeArrow } from '$lib/api/dearrow'; + import { avatarFromChannelId } from '$lib/thumbnail'; + import { mergeAttrs } from 'melt'; interface Props { video: VideoBase | Video | Notification | PlaylistPageVideo | VideoWatchHistory; @@ -69,9 +71,21 @@ } } - const thumbnail = new Avatar({ src: () => thumbnailSrc }); + let thumbnailHeight = $state(0); let thumbnailHTMLElement: HTMLImageElement | undefined = $state(); + const thumbnail = new Avatar({ + src: () => thumbnailSrc, + onLoadingStatusChange: () => { + if (thumbnailHTMLElement) thumbnailHeight = thumbnailHTMLElement.naturalHeight; + } + }); + + let authorAvatarSrc = $state(''); + const authorAvatar = new Avatar({ + src: () => authorAvatarSrc + }); + let startedSideways = sideways === true; function disableSideways() { if (!startedSideways) return; @@ -82,6 +96,12 @@ } onMount(async () => { + if ('authorId' in video) { + avatarFromChannelId(video.authorId).then((url) => { + if (url) authorAvatarSrc = url; + }); + } + // Check if sideways should be enabled or disabled. disableSideways(); @@ -113,7 +133,7 @@ onclick={onVideoSelected} >
-
300 : false}> +
300}> {letterCase(video.title.trimEnd())} -
- {#if 'authorId' in video && video.authorId} - {video.author} - - {:else} -

{video.author}

+
+
@@ -224,7 +255,7 @@ clip-path: inset(10% 0 10% 0); display: block; transform: translateY(-15%); - margin-bottom: -20%; + margin-bottom: -22%; } .thumbnail { diff --git a/materialious/src/lib/dexie.ts b/materialious/src/lib/dexie.ts index 982d4714..80851826 100644 --- a/materialious/src/lib/dexie.ts +++ b/materialious/src/lib/dexie.ts @@ -13,19 +13,27 @@ export interface ChannelSubscriptions { lastRSSFetch: Date; } +export interface ChannelAvatars { + channelId: string; + avatarUrl: string; + updated: Date; +} + export class MaterialiousDb extends Dexie { favouriteChannels!: Table; channelSubscriptions!: Table; subscriptionFeed!: Table