@@ -0,0 +1 @@
|
||||
words = ["favourite"]
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 174
|
||||
versionName "1.12.2"
|
||||
versionCode 175
|
||||
versionName "1.12.3"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -71,7 +71,11 @@
|
||||
|
||||
|
||||
|
||||
<release version="1.12.2" date="2025-12-08">
|
||||
|
||||
<release version="1.12.3" date="2025-12-08">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.12.3</url>
|
||||
</release>
|
||||
<release version="1.12.2" date="2025-12-08">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.12.2</url>
|
||||
</release>
|
||||
<release version="1.12.1" date="2025-12-08">
|
||||
|
||||
Generated
+12
-2
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.12.0",
|
||||
"version": "1.12.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.12.0",
|
||||
"version": "1.12.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
|
||||
"chokidar": "~5.0.0",
|
||||
"electron-is-dev": "~2.0.0",
|
||||
"electron-serve": "~3.0.0",
|
||||
@@ -1885,6 +1886,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/capacitor-nodejs": {
|
||||
"version": "1.0.0-beta.9",
|
||||
"resolved": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
|
||||
"integrity": "sha512-4EU/QBolfuyHiMtwd1nxZ4wsK243tGE4VttKvVBG+x1yQuK9EIlEf3lbRjFXfjrJMTZNCqaP6glmdfj9Zo7KVg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.12.2",
|
||||
"version": "1.12.3",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
@@ -24,6 +24,7 @@
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
|
||||
"chokidar": "~5.0.0",
|
||||
"electron-is-dev": "~2.0.0",
|
||||
"electron-serve": "~3.0.0",
|
||||
@@ -43,4 +44,4 @@
|
||||
"capacitor",
|
||||
"electron"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const CapacitorNodejs = require('../../../node_modules/capacitor-nodejs/electron/dist/plugin.js');
|
||||
|
||||
module.exports = {
|
||||
CapacitorNodejs,
|
||||
}
|
||||
Generated
+9
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.12.0",
|
||||
"version": "1.12.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "materialious",
|
||||
"version": "1.12.0",
|
||||
"version": "1.12.3",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
@@ -25,6 +25,7 @@
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
|
||||
"dayjs": "^1.11.19",
|
||||
"dexie": "^4.2.1",
|
||||
"fuse.js": "^7.0.0",
|
||||
"googlevideo": "^4.0.4",
|
||||
"he": "^1.2.0",
|
||||
@@ -5669,6 +5670,12 @@
|
||||
"integrity": "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dexie": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz",
|
||||
"integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.12.2",
|
||||
"version": "1.12.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -57,6 +57,7 @@
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
|
||||
"dayjs": "^1.11.19",
|
||||
"dexie": "^4.2.1",
|
||||
"fuse.js": "^7.0.0",
|
||||
"googlevideo": "^4.0.4",
|
||||
"he": "^1.2.0",
|
||||
@@ -73,4 +74,4 @@
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"youtubei.js": "^16.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ export interface VideoBase {
|
||||
lengthSeconds: number;
|
||||
viewCountText: string;
|
||||
viewCount?: number;
|
||||
// Means it was given preference in rankings due to
|
||||
// a users settings
|
||||
promotedBy?: 'favourited';
|
||||
}
|
||||
|
||||
export interface ResolvedUrl {
|
||||
@@ -205,6 +208,9 @@ export interface PlaylistPageVideo extends Omit<PlaylistVideo, 'type'> {
|
||||
indexId: string;
|
||||
authorId: string;
|
||||
viewCount: number;
|
||||
// Means it was given preference in rankings due to
|
||||
// a users settings
|
||||
promotedBy?: 'favourited';
|
||||
}
|
||||
|
||||
export interface ChannelContentVideos {
|
||||
|
||||
@@ -229,6 +229,10 @@
|
||||
<p>{video.author}</p>
|
||||
{/if}
|
||||
|
||||
{#if video.promotedBy === 'favourited'}
|
||||
<i>star</i>
|
||||
{/if}
|
||||
|
||||
{#if !('publishedText' in video) && 'viewCountText' in video}
|
||||
•
|
||||
{video.viewCountText ?? cleanNumber(video.viewCount ?? 0)}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { truncate } from '$lib/misc';
|
||||
import { authStore, interfaceLowBandwidthMode, isAndroidTvStore } from '$lib/store';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { localDb } from '$lib/dexie';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
video,
|
||||
@@ -13,6 +15,16 @@
|
||||
hideSubscribe = false
|
||||
}: { video: VideoPlay; subscribed?: boolean; hideSubscribe?: boolean } = $props();
|
||||
|
||||
let favoritedChannel = $state(false);
|
||||
|
||||
const favoriteChannelItem = localDb.favouriteChannels.filter((item) => {
|
||||
return item.channelId === video.authorId;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
favoritedChannel = (await favoriteChannelItem.count()) > 0;
|
||||
});
|
||||
|
||||
async function toggleSubscribed() {
|
||||
if (subscribed) {
|
||||
await deleteUnsubscribe(video.authorId);
|
||||
@@ -22,6 +34,16 @@
|
||||
|
||||
subscribed = !subscribed;
|
||||
}
|
||||
|
||||
async function toggleFavourited() {
|
||||
if (favoritedChannel) {
|
||||
await favoriteChannelItem.delete();
|
||||
favoritedChannel = false;
|
||||
} else {
|
||||
await localDb.favouriteChannels.add({ channelId: video.authorId, created: new Date() });
|
||||
favoritedChannel = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
@@ -56,6 +78,18 @@
|
||||
{$_('unsubscribe')}
|
||||
{/if}
|
||||
</button>
|
||||
{#if window.indexedDB}
|
||||
<button
|
||||
class:inverse-surface={!favoritedChannel}
|
||||
class:border={favoritedChannel}
|
||||
onclick={toggleFavourited}
|
||||
>
|
||||
<i>star</i>
|
||||
<div class="tooltip">
|
||||
{favoritedChannel ? $_('unfavouriteChannel') : $_('favouriteChannel')}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button class="inverse-surface" disabled>
|
||||
{$_('subscribe')}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Table } from 'dexie';
|
||||
import Dexie from 'dexie';
|
||||
|
||||
export interface FavouriteChannels {
|
||||
channelId: string;
|
||||
created: Date;
|
||||
}
|
||||
|
||||
export class MaterialiousDb extends Dexie {
|
||||
favouriteChannels!: Table<FavouriteChannels>;
|
||||
|
||||
constructor() {
|
||||
super('materialious');
|
||||
this.version(1).stores({
|
||||
favouriteChannels: 'channelId, created'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const localDb = new MaterialiousDb();
|
||||
@@ -200,5 +200,7 @@
|
||||
"titleOnly": "Title only"
|
||||
}
|
||||
},
|
||||
"subscribe": "Subscribe"
|
||||
"subscribe": "Subscribe",
|
||||
"favouriteChannel": "Favourite channel",
|
||||
"unfavouriteChannel": "Unfavourite channel"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import { feedCacheStore } from '$lib/store';
|
||||
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
|
||||
import ItemsList from '$lib/components/ItemsList.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import { _ } from '$lib/i18n';
|
||||
|
||||
let currentPage = 1;
|
||||
let videos: (VideoBase | Video | PlaylistPageVideo)[] = $state($feedCacheStore.subscription);
|
||||
@@ -21,6 +23,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="right-align">
|
||||
<a class="button outline" href={resolve('/subscriptions/manage', {})}>
|
||||
{$_('subscriptions.manageSubscriptions')}
|
||||
</a>
|
||||
</nav>
|
||||
<div class="space"></div>
|
||||
|
||||
<ItemsList items={videos ?? []} />
|
||||
|
||||
<InfiniteLoading on:infinite={loadMore} />
|
||||
|
||||
@@ -1,10 +1,39 @@
|
||||
import { getFeed } from '$lib/api/index';
|
||||
import type { PlaylistPageVideo, Video, VideoBase } from '$lib/api/model';
|
||||
import { localDb } from '$lib/dexie';
|
||||
import { excludeDuplicateFeeds } from '$lib/misc';
|
||||
import { feedCacheStore } from '$lib/store';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
type supportedVideos = (VideoBase | Video | PlaylistPageVideo)[];
|
||||
|
||||
async function sortVideosByFavourites(videos: supportedVideos): Promise<supportedVideos> {
|
||||
if (!window.indexedDB) return videos;
|
||||
|
||||
const favouritedChannels = (await localDb.favouriteChannels.toArray()).map(
|
||||
(channel) => channel.channelId
|
||||
);
|
||||
|
||||
if (favouritedChannels.length === 0) {
|
||||
return videos;
|
||||
}
|
||||
|
||||
const regularVideos: supportedVideos = [];
|
||||
const favouriteVideos: supportedVideos = [];
|
||||
|
||||
videos.forEach((video) => {
|
||||
if (favouritedChannels.includes(video.authorId)) {
|
||||
video.promotedBy = 'favourited';
|
||||
favouriteVideos.push(video);
|
||||
} else {
|
||||
regularVideos.push(video);
|
||||
}
|
||||
});
|
||||
|
||||
return [...favouriteVideos, ...regularVideos];
|
||||
}
|
||||
|
||||
export async function load() {
|
||||
let videos = get(feedCacheStore).subscription;
|
||||
|
||||
@@ -16,12 +45,16 @@ export async function load() {
|
||||
error(500, errorMessage);
|
||||
}
|
||||
|
||||
videos = [...feeds.notifications, ...feeds.videos];
|
||||
videos = await sortVideosByFavourites([...feeds.notifications, ...feeds.videos]);
|
||||
|
||||
feedCacheStore.set({ ...get(feedCacheStore), subscription: videos });
|
||||
} else {
|
||||
await getFeed(100, 1).then((feeds) => {
|
||||
const newVideos = [...feeds.notifications, ...feeds.videos, ...videos];
|
||||
await getFeed(100, 1).then(async (feeds) => {
|
||||
const newVideos = await sortVideosByFavourites([
|
||||
...feeds.notifications,
|
||||
...feeds.videos,
|
||||
...videos
|
||||
]);
|
||||
feedCacheStore.set({
|
||||
...get(feedCacheStore),
|
||||
subscription: excludeDuplicateFeeds(videos, newVideos) as (
|
||||
|
||||
@@ -34,31 +34,31 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space"></div>
|
||||
<div class="padding">
|
||||
<div class="max field suffix prefix small no-margin surface-variant">
|
||||
<i class="front">search</i><input
|
||||
bind:value={search}
|
||||
oninput={searchSubs}
|
||||
type="text"
|
||||
placeholder={$_('searchPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="max field suffix prefix small no-margin surface-variant">
|
||||
<i class="front">search</i><input
|
||||
bind:value={search}
|
||||
oninput={searchSubs}
|
||||
type="text"
|
||||
placeholder={$_('searchPlaceholder')}
|
||||
/>
|
||||
{#each subscriptions as sub (sub.authorId)}
|
||||
<article>
|
||||
<nav>
|
||||
<a href={resolve(`/channel/[authorId]`, { authorId: sub.authorId })}
|
||||
><h6>
|
||||
{sub.author}
|
||||
</h6></a
|
||||
>
|
||||
<div class="max"></div>
|
||||
<button onclick={async () => unsubscribe(sub.authorId)} class="border">Unsubscribe</button>
|
||||
</nav>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#each subscriptions as sub (sub.authorId)}
|
||||
<article>
|
||||
<nav>
|
||||
<a href={resolve(`/channel/[authorId]`, { authorId: sub.authorId })}
|
||||
><h6>
|
||||
{sub.author}
|
||||
</h6></a
|
||||
>
|
||||
<div class="max"></div>
|
||||
<button onclick={async () => unsubscribe(sub.authorId)} class="border">Unsubscribe</button>
|
||||
</nav>
|
||||
</article>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
@media screen and (max-width: 590px) {
|
||||
nav {
|
||||
|
||||
@@ -251,7 +251,9 @@
|
||||
playerCurrentTime = playerElement.currentTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (data.video.premiereTimestamp) {
|
||||
premiereTime = humanFriendlyTimestamp(data.video.premiereTimestamp);
|
||||
premiereUpdateInterval = setInterval(async () => {
|
||||
@@ -265,9 +267,7 @@
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Required due to needing the playerElement
|
||||
if ($playerState?.playerElement) {
|
||||
await load($playerState);
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
LATEST_VERSION = "1.12.2"
|
||||
LATEST_VERSION = "1.12.3"
|
||||
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
|
||||
|
||||
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")
|
||||
|
||||
Reference in New Issue
Block a user