Merge pull request #1252 from Materialious/update/1.12.5

Update/1.12.5
This commit is contained in:
Ward
2025-12-13 00:09:44 +00:00
committed by GitHub
16 changed files with 192 additions and 66 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 176
versionName "1.12.4"
versionCode 177
versionName "1.12.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -16,6 +16,7 @@ dependencies {
implementation project(':capacitor-preferences')
implementation project(':capacitor-screen-orientation')
implementation project(':capacitor-share')
implementation project(':capacitor-music-controls-plugin')
implementation project(':capacitor-nodejs')
}
@@ -23,5 +23,8 @@ project(':capacitor-screen-orientation').projectDir = new File('../node_modules/
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
include ':capacitor-music-controls-plugin'
project(':capacitor-music-controls-plugin').projectDir = new File('../node_modules/capacitor-music-controls-plugin/android')
include ':capacitor-nodejs'
project(':capacitor-nodejs').projectDir = new File('../node_modules/capacitor-nodejs/android')
@@ -73,7 +73,11 @@
<release version="1.12.4" date="2025-12-11">
<release version="1.12.5" date="2025-12-13">
<url>https://github.com/Materialious/Materialious/releases/tag/1.12.5</url>
</release>
<release version="1.12.4" date="2025-12-11">
<url>https://github.com/Materialious/Materialious/releases/tag/1.12.4</url>
</release>
<release version="1.12.3" date="2025-12-08">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.12.4",
"version": "1.12.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.12.4",
"version": "1.12.5",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.12.4",
"version": "1.12.5",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
+21 -2
View File
@@ -1,12 +1,12 @@
{
"name": "materialious",
"version": "1.12.4",
"version": "1.12.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "materialious",
"version": "1.12.4",
"version": "1.12.5",
"hasInstallScript": true,
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
@@ -23,6 +23,7 @@
"@macfja/svelte-persistent-store": "^2.4.2",
"beercss": "^3.13.1",
"bgutils-js": "^3.2.0",
"capacitor-music-controls-plugin": "^6.1.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",
@@ -4728,6 +4729,24 @@
],
"license": "CC-BY-4.0"
},
"node_modules/capacitor-music-controls-plugin": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/capacitor-music-controls-plugin/-/capacitor-music-controls-plugin-6.1.0.tgz",
"integrity": "sha512-j9NwUtUqiIAJQx+t8Zmf/zbYi10eQ37nrXIPbdnYA0B80sCXuX4MGdO+DTkyXCxP/c8BpM5rAJUqR9hZyTnSyQ==",
"license": "MIT",
"dependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/capacitor-music-controls-plugin/node_modules/@capacitor/core": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.1.0"
}
},
"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",
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.12.4",
"version": "1.12.5",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -55,6 +55,7 @@
"@macfja/svelte-persistent-store": "^2.4.2",
"beercss": "^3.13.1",
"bgutils-js": "^3.2.0",
"capacitor-music-controls-plugin": "^6.1.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",
@@ -74,4 +75,4 @@
"svelte-infinite-loading": "^1.4.0",
"youtubei.js": "^16.0.1"
}
}
}
+60 -7
View File
@@ -12,6 +12,7 @@
import Mousetrap from 'mousetrap';
import 'shaka-player/dist/controls.css';
import '$lib/css/shaka-player-theme.css';
import { CapacitorMusicControls } from 'capacitor-music-controls-plugin';
import shaka from 'shaka-player/dist/shaka-player.ui';
import { SponsorBlock, type Category, type Segment } from 'sponsorblock-api';
import { onDestroy, onMount, tick } from 'svelte';
@@ -570,7 +571,55 @@
await androidHandleRotate();
if ('mediaSession' in navigator) {
if (Capacitor.getPlatform() === 'android') {
await CapacitorMusicControls.create({
track: data.video.title,
artist: data.video.author,
cover: getBestThumbnail(data.video.videoThumbnails, 800, 800),
hasPrev: data.playlistId !== null,
hasNext: data.playlistId !== null,
isPlaying: true,
hasClose: true,
dismissable: true,
playIcon: 'media_play',
pauseIcon: 'media_pause',
prevIcon: 'media_prev',
nextIcon: 'media_next',
closeIcon: 'media_close',
notificationIcon: 'notification',
ticker: `Now playing "${data.video.title}"`
});
// @ts-expect-error: Is handle correctly via https://github.com/ingageco/capacitor-music-controls-plugin
document.addEventListener('controlsNotification', (event: { message: string }) => {
if (
event.message === 'music-controls-headset-unplugged' ||
event.message === 'music-controls-pause'
) {
playerElement?.pause();
return;
} else if (event.message === 'music-controls-play') {
playerElement?.play();
return;
}
if (data.playlistId) {
if (event.message === 'music-controls-next') {
goToNextVideo(data.video, data.playlistId);
} else if (event.message === 'music-controls-previous') {
goToPreviousVideo(data.playlistId);
}
}
});
playerElement?.addEventListener('play', () => {
CapacitorMusicControls.updateIsPlaying({ isPlaying: true });
});
playerElement?.addEventListener('pause', () => {
CapacitorMusicControls.updateIsPlaying({ isPlaying: false });
});
} else if ('mediaSession' in navigator) {
const metadataArtwork: MediaImage[] = [];
data.video.videoThumbnails.forEach((thumbnail) => {
metadataArtwork.push({
@@ -800,14 +849,18 @@
}
onDestroy(async () => {
if (Capacitor.getPlatform() === 'android' && !$isAndroidTvStore) {
document.removeEventListener('fullscreenchange', onAndroidFullscreenChange);
if (Capacitor.getPlatform() === 'android') {
if (!$isAndroidTvStore) {
document.removeEventListener('fullscreenchange', onAndroidFullscreenChange);
if (originalOrigination) {
await ScreenOrientation.lock({
orientation: originalOrigination.type
});
if (originalOrigination) {
await ScreenOrientation.lock({
orientation: originalOrigination.type
});
}
}
await CapacitorMusicControls.destroy();
}
try {
+10
View File
@@ -13,6 +13,9 @@ import type { TitleCase } from './letterCasing';
import { serialize, deserialize } from '@macfja/serializer';
import type {
Channel,
ChannelContentPlaylists,
ChannelContentVideos,
ChannelPage,
HashTag,
Playlist,
PlaylistPage,
@@ -334,3 +337,10 @@ export const playlistCacheStore: Writable<{
}> = writable({});
export const isAndroidTvStore: Writable<boolean> = writable(false);
export const channelCacheStore: Writable<{
[key: string]: {
channel: ChannelPage;
displayContent: { [key: string]: ChannelContentVideos | ChannelContentPlaylists };
};
}> = writable({});
+22 -18
View File
@@ -411,13 +411,24 @@
>
<div class="pip-info">
{#if playerIsPip}
<nav>
<h6 class="max">{truncate($playerState.data.video.title, 20)}</h6>
<button class="border m l" onclick={() => playerState.set(undefined)}>
<i>close</i>
</button>
</nav>
<div class="space"></div>
<div>
<nav>
<h6 class="max">{truncate($playerState.data.video.title, 25)}</h6>
<button class="border m l" onclick={() => playerState.set(undefined)}>
<i>close</i>
</button>
</nav>
<nav class="s">
<a
class="button border"
href={resolve(`/watch/[videoId]`, { videoId: $playerState.data.video.videoId })}
><i>keyboard_arrow_right</i></a
>
<button class="border" onclick={() => playerState.set(undefined)}>
<i>close</i>
</button>
</nav>
</div>
{/if}
<div class="player">
{#key $playerState.data.video.videoId}
@@ -426,17 +437,6 @@
</div>
</div>
{#if playerIsPip}
<nav class="s">
<a
class="button border"
href={resolve(`/watch/[videoId]`, { videoId: $playerState.data.video.videoId })}
><i>keyboard_arrow_right</i></a
>
<button class="border" onclick={() => playerState.set(undefined)}>
<i>close</i>
</button>
</nav>
<nav class="m l">
<Author channel={$playerState.data.video} hideSubscribe={true} />
<div class="max"></div>
@@ -533,5 +533,9 @@
display: flex;
justify-content: space-between;
}
.pip h6 {
font-size: 1em;
}
}
</style>
@@ -9,16 +9,15 @@
import PageLoading from '$lib/components/PageLoading.svelte';
import { proxyGoogleImage } from '$lib/images';
import { cleanNumber } from '$lib/numbers';
import { interfaceLowBandwidthMode, isAndroidTvStore } from '$lib/store';
import { channelCacheStore, interfaceLowBandwidthMode, isAndroidTvStore } from '$lib/store';
import { Clipboard } from '@capacitor/clipboard';
import { Capacitor } from '@capacitor/core';
import { onMount } from 'svelte';
import { _ } from '$lib/i18n';
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
import ItemsList from '$lib/components/ItemsList.svelte';
import Author from '$lib/components/Author.svelte';
let { data } = $props();
import { onMount } from 'svelte';
import { page } from '$app/state';
let tab: channelContentTypes = $state('videos');
@@ -31,8 +30,12 @@
let displayContent: ChannelContentPlaylists | ChannelContentVideos | undefined =
$state(undefined);
onMount(() => {
displayContent = $channelCacheStore[page.params.slug].displayContent.videos;
});
async function searchChannel() {
displayContent = await searchChannelContent(data.channel.authorId, channelSearch);
displayContent = await searchChannelContent(page.params.slug, channelSearch);
}
async function loadMore(event: InfiniteEvent) {
@@ -43,7 +46,7 @@
return;
}
const newContent = await getChannelContent(data.channel.authorId, {
const newContent = await getChannelContent(page.params.slug, {
type: tab,
continuation: displayContent.continuation,
sortBy: sortBy
@@ -69,30 +72,28 @@
async function changeTab(newTab: 'videos' | 'playlists' | 'streams' | 'shorts') {
tab = newTab;
displayContent = undefined;
displayContent = await getChannelContent(data.channel.authorId, { type: tab });
displayContent = await getChannelContent(page.params.slug, { type: tab });
}
onMount(async () => {
displayContent = await getChannelContent(data.channel.authorId, {
type: 'videos',
sortBy: sortBy
});
});
</script>
<div class="padding">
{#if data.channel.authorBanners.length > 0 && !$interfaceLowBandwidthMode}
{#if $channelCacheStore[page.params.slug].channel.authorBanners.length > 0 && !$interfaceLowBandwidthMode}
<img
loading="lazy"
src={proxyGoogleImage(data.channel.authorBanners[0].url)}
src={proxyGoogleImage($channelCacheStore[page.params.slug].channel.authorBanners[0].url)}
width="100%"
alt="Channel banner"
/>
{/if}
<div class="description">
<div>
<Author channel={{ ...data.channel, subCountText: cleanNumber(data.channel.subCount) }} />
<p style="width: 60vw;">{data.channel.description}</p>
<Author
channel={{
...$channelCacheStore[page.params.slug].channel,
subCountText: cleanNumber($channelCacheStore[page.params.slug].channel.subCount)
}}
/>
<p style="width: 60vw;">{$channelCacheStore[page.params.slug].channel.description}</p>
</div>
{#if !$isAndroidTvStore}
<button class="border">
@@ -117,7 +118,7 @@
role="presentation"
onclick={async () => {
await Clipboard.write({
string: `https://www.youtube.com/channel/${data.channel.authorId}`
string: `https://www.youtube.com/channel/${page.params.slug}`
});
(document.activeElement as HTMLElement)?.blur();
}}
@@ -130,25 +131,25 @@
</div>
<div class="tabs left-align scroll">
{#if data.channel.tabs.includes('videos')}
{#if $channelCacheStore[page.params.slug].channel.tabs.includes('videos')}
<a class:active={tab === 'videos'} onclick={() => changeTab('videos')} href="#video">
<i>movie</i>
<span>{$_('videoTabs.videos')}</span>
</a>
{/if}
{#if data.channel.tabs.includes('shorts')}
{#if $channelCacheStore[page.params.slug].channel.tabs.includes('shorts')}
<a class:active={tab === 'shorts'} onclick={() => changeTab('shorts')} href="#short">
<i>smartphone</i>
<span>{$_('videoTabs.shorts')}</span>
</a>
{/if}
{#if data.channel.tabs.includes('streams')}
{#if $channelCacheStore[page.params.slug].channel.tabs.includes('streams')}
<a class:active={tab === 'streams'} onclick={() => changeTab('streams')} href="#stream">
<i>stream</i>
<span>{$_('videoTabs.streams')}</span>
</a>
{/if}
{#if data.channel.tabs.includes('playlists')}
{#if $channelCacheStore[page.params.slug].channel.tabs.includes('playlists')}
<a class:active={tab === 'playlists'} onclick={() => changeTab('playlists')} href="#playlist">
<i>playlist_add_check</i>
<span>{$_('videoTabs.playlists')}</span>
@@ -165,7 +166,7 @@
onclick={async () => {
sortBy = sortingOption;
displayContent = await getChannelContent(data.channel.authorId, {
displayContent = await getChannelContent(page.params.slug, {
type: tab,
sortBy: sortBy
});
@@ -1,7 +1,28 @@
import { getChannel } from '$lib/api/index';
import { getChannel, getChannelContent } from '$lib/api/index';
import type { ChannelContentVideos, Video } from '$lib/api/model';
import { excludeDuplicateFeeds } from '$lib/misc.js';
import { channelCacheStore } from '$lib/store.js';
import { error } from '@sveltejs/kit';
import { get } from 'svelte/store';
export async function load({ params }) {
const currentChannelCache = get(channelCacheStore);
if (params.slug in currentChannelCache) {
getChannelContent(params.slug, {
type: 'videos'
}).then((channelContent) => {
if (!('videos' in currentChannelCache[params.slug].displayContent)) return;
(currentChannelCache[params.slug].displayContent.videos as ChannelContentVideos).videos =
excludeDuplicateFeeds(
(currentChannelCache[params.slug].displayContent.videos as ChannelContentVideos).videos,
(channelContent as ChannelContentVideos).videos
) as Video[];
});
return;
}
let channel;
try {
@@ -10,7 +31,15 @@ export async function load({ params }) {
error(500, errorMessage);
}
return {
channel: channel
};
const displayContent = await getChannelContent(params.slug, {
type: 'videos'
});
channelCacheStore.set({
...currentChannelCache,
[params.slug]: {
channel: channel,
displayContent: { videos: displayContent }
}
});
}
@@ -339,7 +339,7 @@
await addPlaylistVideo(playlistId, data.video.videoId);
}
setTimeout(async () => (personalPlaylists = await getPersonalPlaylists()), 500);
personalPlaylists = await getPersonalPlaylists();
}
async function loadMoreComments() {
@@ -444,7 +444,7 @@
</div>
<ShareVideo bind:currentTime={playerCurrentTime} video={data.video} />
</button>
{#if personalPlaylists}
{#if personalPlaylists && personalPlaylists.length > 0}
<button class="border">
<i>add</i>
<div class="tooltip">{$_('player.addToPlaylist')}</div>
@@ -119,6 +119,7 @@
<article id="shown-info" transition:fade>
<h5>{letterCase(data.video.title)}</h5>
<Author channel={data.video} />
<div class="space"></div>
<LikesDislikes video={data.video} returnYTDislikes={data.streamed.returnYTDislikes} />
<article class="border">
<Description video={data.video} description={data.content.description} />
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.12.4"
LATEST_VERSION = "1.12.5"
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")