Lazy loading & shows active timestamp (#964)
This commit is contained in:
@@ -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">
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user