@@ -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">
|
||||
|
||||
Generated
+2
-2
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.12.4",
|
||||
"version": "1.12.5",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
Generated
+21
-2
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user