Lazy loading & shows active timestamp (#964)

This commit is contained in:
Ward
2025-06-08 19:43:53 +12:00
committed by GitHub
parent 75bba23e66
commit 9b147d0a9a
22 changed files with 112 additions and 64 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 120
versionName "1.9.3"
versionCode 121
versionName "1.9.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -55,7 +55,11 @@
<content_attribute id="social-contacts">intense</content_attribute>
</content_rating>
<releases>
<release version="1.9.3" date="2025-6-05">
<release version="1.9.4" date="2025-6-08">
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.4</url>
</release>
<release version="1.9.3" date="2025-6-05">
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.3</url>
</release>
<release version="1.9.2" date="2025-6-05">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.9.2",
"version": "1.9.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.9.2",
"version": "1.9.3",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.9.3",
"version": "1.9.4",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
@@ -44,4 +44,4 @@
"capacitor",
"electron"
]
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.9.3",
"version": "1.9.4",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -70,4 +70,4 @@
"svelte-persisted-store": "^0.12.0",
"youtubei.js": "^13.4.0"
}
}
}
@@ -36,6 +36,7 @@
<progress class="circle"></progress>
{:else}
<img
loading="lazy"
class="circle"
style="width: 90px;height: 90px;"
src={channelPfp.src}
@@ -56,7 +56,7 @@
{#if !$interfaceLowBandwidthMode}
<div class="comment-header">
{#if userPfp}
<img class="circle small" src={userPfp} alt="comment profile" />
<img loading="lazy" class="circle small" src={userPfp} alt="comment profile" />
{:else}
<progress class="circle"></progress>
{/if}
@@ -58,6 +58,7 @@
<progress class="circle"></progress>
{:else if img && img.src !== ''}
<img
loading="lazy"
class="responsive"
style="max-width: 100%;height: 100%;"
src={img.src}
@@ -115,7 +115,9 @@
{#if showSearchBox}
<menu class="min suggestions-container rounded">
<div class="field large prefix suffix no-margin fixed">
<i class="front" onclick={() => dispatch('searchCancelled')}>arrow_back</i>
<i class="front" role="presentation" onclick={() => dispatch('searchCancelled')}
>arrow_back</i
>
<input
placeholder={$_('searchPlaceholder')}
type="text"
@@ -131,7 +133,7 @@
}
}}
/>
<i class="front" onclick={resetSearch}>close</i>
<i class="front" role="presentation" onclick={resetSearch}>close</i>
</div>
{#if searchSuggestions}
{#each suggestionsForSearch as suggestion, index}
@@ -13,8 +13,9 @@
let { video }: Props = $props();
</script>
<button
<li
class="row"
role="presentation"
onclick={async () => {
if (Capacitor.isNativePlatform()) {
await Clipboard.write({ string: `${get(instanceStore)}/watch/${video.videoId}` });
@@ -23,18 +24,21 @@
}
}}
>
<div class="min">{$_('player.share.materialiousLink')}</div></button
>
<button
<div class="min">{$_('player.share.materialiousLink')}</div>
</li>
<li
class="row"
role="presentation"
onclick={async () =>
await Clipboard.write({ string: `https://redirect.invidious.io/watch?v=${video.videoId}` })}
>
<div class="min">{$_('player.share.invidiousRedirect')}</div></button
><button
<div class="min">{$_('player.share.invidiousRedirect')}</div>
</li>
<li
class="row"
role="presentation"
onclick={async () =>
await Clipboard.write({ string: `https://www.youtube.com/watch?v=${video.videoId}` })}
>
<div class="min">{$_('player.share.youtubeLink')}</div></button
>
<div class="min">{$_('player.share.youtubeLink')}</div>
</li>
@@ -163,7 +163,7 @@
{#if !thumbnail}
<div class="secondary-container" style="width: 100%;height: {placeholderHeight}px;"></div>
{:else}
<img class="responsive" src={thumbnail.src} alt="Thumbnail for video" />
<img class="responsive" loading="lazy" src={thumbnail.src} alt="Thumbnail for video" />
{/if}
{/if}
{#if progress}
@@ -135,6 +135,7 @@
<div
class="transcript-line"
id={`transcript-line-${cue.startTime}`}
role="presentation"
onclick={() => (playerElement.currentTime = cue.startTime)}
class:secondary-container={currentTime >= cue.startTime && currentTime <= cue.endTime}
>
@@ -37,6 +37,7 @@
class="no-padding"
style="height: 100%;"
id={video.videoId}
role="presentation"
onclick={() => feedLastItemId.set(video.videoId)}
>
<Thumbnail {video} {playlistId} />
+1
View File
@@ -63,6 +63,7 @@ export async function insecureRequestImageHandler(source: string): Promise<HTMLI
const imgResp = await fetch(source);
if (!imgResp.ok) {
img.src = '';
return img;
}
img.src = URL.createObjectURL(await imgResp.blob());
+14 -3
View File
@@ -1,9 +1,11 @@
import { decodeHtmlCharCodes } from './misc';
import { convertToSeconds } from './numbers';
type Timestamps = { title: string; time: number; timePretty: string; endTime: number }[];
export interface PhasedDescription {
description: string;
timestamps: { title: string; time: number; timePretty: string }[];
timestamps: Timestamps;
}
export function extractActualLink(url: string): string {
@@ -37,7 +39,7 @@ export function phaseDescription(
content: string,
fallbackPatch?: 'youtubejs' | 'piped'
): PhasedDescription {
const timestamps: { title: string; time: number; timePretty: string }[] = [];
const timestamps: Timestamps = [];
const lines = content.split('\n');
// Regular expressions for different timestamp formats
@@ -80,13 +82,22 @@ export function phaseDescription(
.replace(/\n/g, '')
.trim()
),
timePretty: timestamp
timePretty: timestamp,
endTime: -1
});
} else {
filteredLines.push(line);
}
});
timestamps.forEach((ts, idx) => {
if (idx < timestamps.length - 1) {
ts.endTime = timestamps[idx + 1].time;
} else {
ts.endTime = -1;
}
});
const filteredContent = filteredLines.join('\n');
return { description: filteredContent, timestamps: timestamps };
+6 -1
View File
@@ -204,7 +204,12 @@
</nav>
{/if}
<nav onclick={() => goto($interfaceDefaultPage)} style="cursor: pointer;" class="m l">
<nav
role="presentation"
onclick={() => goto($interfaceDefaultPage)}
style="cursor: pointer;"
class="m l"
>
<Logo />
<h6 class="l">Materialious</h6>
</nav>
@@ -90,6 +90,7 @@
<div class="padding">
{#if data.channel.authorBanners.length > 0 && !$interfaceLowBandwidthMode}
<img
loading="lazy"
src={proxyGoogleImage(data.channel.authorBanners[0].url)}
width="100%"
alt="Channel banner"
@@ -99,6 +100,7 @@
{#if !$interfaceLowBandwidthMode}
{#if channelPfp}
<img
loading="lazy"
style="margin-right: 1em;"
class="circle extra m l"
src={channelPfp}
@@ -135,24 +137,28 @@
<span>{$_('player.share.title')}</span>
<menu class="no-wrap mobile">
{#if !Capacitor.isNativePlatform()}
<button
<li
class="row"
role="presentation"
onclick={async () => {
await Clipboard.write({ string: location.href });
}}>{$_('player.share.materialiousLink')}</button
>
{/if}
<!--Ugly hack to get pass svelte error-->
{#if true}
<button
class="row"
onclick={async () => {
await Clipboard.write({
string: `https://www.youtube.com/channel/${data.channel.authorId}`
});
}}>{$_('player.share.youtubeLink')}</button
}}
>
{$_('player.share.materialiousLink')}
</li>
{/if}
<li
class="row"
role="presentation"
onclick={async () => {
await Clipboard.write({
string: `https://www.youtube.com/channel/${data.channel.authorId}`
});
}}
>
{$_('player.share.youtubeLink')}
</li>
</menu>
</button>
</div>
@@ -14,12 +14,13 @@
let videos: PlaylistPageVideo[] | undefined = $state();
if (data.playlist.videos.length > 0) {
videos = data.playlist.videos.sort((a: PlaylistPageVideo, b: PlaylistPageVideo) => {
return a.index < b.index ? -1 : 1;
});
videos = videos.filter((playlistVideo) => {
return playlistVideo.lengthSeconds > 0;
});
videos = data.playlist.videos
.sort((a: PlaylistPageVideo, b: PlaylistPageVideo) => {
return a.index < b.index ? -1 : 1;
})
.filter((playlistVideo) => {
return playlistVideo.lengthSeconds > 0;
});
onMount(async () => {
for (let page = 1; page++; ) {
@@ -87,26 +88,27 @@
<span>{$_('player.share.title')}</span>
<menu class="no-wrap mobile">
{#if !Capacitor.isNativePlatform()}
<button
<li
class="row"
role="presentation"
onclick={async () => {
await Clipboard.write({ string: location.href });
}}>{$_('player.share.materialiousLink')}</button
>
{/if}
<!--Silly hack so svelte doesnt error-->
{#if true}
<button
class="row"
onclick={async () => {
await Clipboard.write({
string: `https://www.youtube.com/playlist?list=${data.playlist.playlistId}`
});
}}
>
{$_('player.share.youtubeLink')}
</button>
{$_('player.share.materialiousLink')}
</li>
{/if}
<li
class="row"
role="presentation"
onclick={async () => {
await Clipboard.write({
string: `https://www.youtube.com/playlist?list=${data.playlist.playlistId}`
});
}}
>
{$_('player.share.youtubeLink')}
</li>
</menu>
</button>
</article>
@@ -93,6 +93,7 @@
<article
class="no-padding"
style="height: 100%;"
role="presentation"
onclick={() => feedLastItemId.set(extractUniqueId(item))}
id={extractUniqueId(item)}
>
@@ -9,7 +9,7 @@
let search: string = $state('');
const fuse = new Fuse(subscriptions, {
const fuse = new Fuse(data.subscriptions, {
keys: ['author']
});
@@ -434,6 +434,7 @@
<nav style="gap: 0.5em;">
{#if !$interfaceLowBandwidthMode}
<img
loading="lazy"
class="circle large"
src={proxyGoogleImage(getBestThumbnail(data.video.authorThumbnails))}
alt="Channel profile"
@@ -532,7 +533,8 @@
<div class="tooltip">{$_('player.addToPlaylist')}</div>
<menu class="no-wrap mobile">
{#each personalPlaylists as personalPlaylist}
<button
<li
role="presentation"
class="row"
onclick={async () => await toggleVideoToPlaylist(personalPlaylist.playlistId)}
>
@@ -546,7 +548,7 @@
<i>add</i>
{/if}
</nav>
</button>
</li>
{/each}
</menu>
</button>
@@ -610,18 +612,24 @@
<ul class="list">
{#each data.content.timestamps as timestamp}
<li
role="presentation"
onclick={() => {
if (playerElement) playerElement.currentTime = timestamp.time;
}}
>
<img
class="round large"
loading="lazy"
src={getBestThumbnail(data.video.videoThumbnails) as string}
alt="Thumbnail for current video"
/>
<div class="max" style="white-space: pre-line; overflow-wrap: break-word;">
<p>{timestamp.title}</p>
<span class="chip fill no-margin">{timestamp.timePretty}</span>
<p style="no-margin no-padding">{timestamp.title}</p>
<span
class:primary={playerCurrentTime >= timestamp.time &&
(playerCurrentTime <= timestamp.endTime || timestamp.endTime === -1)}
class="chip no-margin">{timestamp.timePretty}</span
>
</div>
</li>
{/each}
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.9.3"
LATEST_VERSION = "1.9.4"
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
WORKING_DIR = os.path.join(