Merge pull request #956 from Materialious/update/1.9.2

Update/1.9.2
This commit is contained in:
Ward
2025-06-05 21:51:59 +12:00
committed by GitHub
17 changed files with 129 additions and 186 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 118
versionName "1.9.1"
versionCode 119
versionName "1.9.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -63,7 +63,11 @@
<release version="1.9.1" date="2025-6-05">
<release version="1.9.2" date="2025-6-05">
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.2</url>
</release>
<release version="1.9.1" date="2025-6-05">
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.1</url>
</release>
<release version="1.9.0" date="2025-6-04">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.9.1",
"version": "1.9.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.9.1",
"version": "1.9.2",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.9.1",
"version": "1.9.2",
"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.1",
"version": "1.9.2",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -70,4 +70,4 @@
"svelte-persisted-store": "^0.12.0",
"youtubei.js": "^13.4.0"
}
}
}
@@ -17,6 +17,7 @@
instanceStore,
interfaceAllowInsecureRequests,
interfaceAmoledTheme,
interfaceAutoExpandChapters,
interfaceAutoExpandComments,
interfaceAutoExpandDesc,
interfaceDefaultPage,
@@ -250,6 +251,22 @@
</nav>
</div>
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
<div>{$_('layout.expandChapters')}</div>
</div>
<label class="switch">
<input
type="checkbox"
bind:checked={$interfaceAutoExpandChapters}
onclick={() => interfaceAutoExpandChapters.set(!$interfaceAutoExpandChapters)}
/>
<span></span>
</label>
</nav>
</div>
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
+28 -117
View File
@@ -40,118 +40,6 @@
}
});
export function dedupeAndMergeOverlappingCues(cues: VTTCue[], timeOffset = -1): VTTCue[] {
const merged: VTTCue[] = [];
for (const cue of cues) {
const trimmedText = cue.text.trim();
if (!trimmedText) {
continue;
}
let wasMerged = false;
for (let i = 0; i < merged.length; i++) {
const existing = merged[i];
const timeGap = Math.abs(cue.startTime - existing.endTime);
const isTimeAdjacent = timeGap <= 1.5;
if (isTimeAdjacent && areCuesTextSimilar(existing.text, cue.text)) {
const mergedText = mergeCueText(existing.text, cue.text);
const newStart = Math.max(0, Math.min(existing.startTime, cue.startTime) + timeOffset);
const newEnd = Math.max(existing.endTime, cue.endTime) + timeOffset;
const mergedCue = new VTTCue(newStart, newEnd, mergedText);
copyCueMeta(mergedCue, cue);
merged[i] = mergedCue;
wasMerged = true;
break;
}
}
if (!wasMerged) {
const offsetCue = new VTTCue(
Math.max(0, cue.startTime + timeOffset),
cue.endTime + timeOffset,
cue.text
);
copyCueMeta(offsetCue, cue);
merged.push(offsetCue);
}
}
// Ensure no overlapping cues (each cue ends before the next starts)
for (let i = 0; i < merged.length - 1; i++) {
const current = merged[i];
const next = merged[i + 1];
if (current.endTime > next.startTime) {
current.endTime = Math.max(current.startTime, next.startTime - 0.01); // 10ms gap
}
}
return merged;
}
function areCuesTextSimilar(a: string, b: string): boolean {
if (a === b) return true;
// Normalize and compare overlap ratio
const cleanA = normalizeText(a);
const cleanB = normalizeText(b);
const overlap = getTextOverlap(cleanA, cleanB);
const minLength = Math.min(cleanA.length, cleanB.length);
// Consider similar if 50% or more overlap
return overlap / minLength >= 0.5;
}
function getTextOverlap(a: string, b: string): number {
const maxOverlap = Math.min(a.length, b.length);
for (let i = maxOverlap; i > 0; i--) {
if (a.endsWith(b.slice(0, i)) || b.endsWith(a.slice(0, i))) {
return i;
}
}
return 0;
}
function normalizeText(text: string): string {
return text
.trim()
.replace(/\s+/g, ' ')
.replace(/[.,?!"'”“]/g, '')
.toLowerCase();
}
function mergeCueText(a: string, b: string): string {
// Avoid duplicating overlap
const overlapLen = getTextOverlap(a, b);
if (a.endsWith(b.slice(0, overlapLen))) {
return a + b.slice(overlapLen);
}
if (b.endsWith(a.slice(0, overlapLen))) {
return b + a.slice(overlapLen);
}
return `${a} ${b}`;
}
function copyCueMeta(target: VTTCue, source: VTTCue): void {
target.region = source.region;
target.vertical = source.vertical;
target.snapToLines = source.snapToLines;
target.line = source.line;
target.lineAlign = source.lineAlign;
target.position = source.position;
target.positionAlign = source.positionAlign;
target.size = source.size;
target.align = source.align;
target.style = source.style;
}
async function loadTranscript() {
if (!url) {
transcript = null;
@@ -160,11 +48,31 @@
isLoading = true;
transcript = null;
const resp = await fetch(`${!video.fallbackPatch ? get(instanceStore) : ''}${url}`);
transcript = await parseText(await resp.text(), { strict: false });
transcriptCues = dedupeAndMergeOverlappingCues(transcript.cues);
// Group cues by Math.ceil(startTime)
const startTimeMap = new Map<number, VTTCue[]>();
for (const cue of transcript.cues) {
const roundedTime = Math.ceil(cue.startTime);
if (!startTimeMap.has(roundedTime)) {
startTimeMap.set(roundedTime, []);
}
startTimeMap.get(roundedTime)!.push(cue);
}
// Keep only the second cue if multiple exist for the same rounded time
transcriptCues = [];
for (const [, cues] of startTimeMap.entries()) {
if (cues.length === 1) {
transcriptCues.push(cues[0]);
} else if (cues.length >= 2) {
transcriptCues.push(cues[1]); // Keep the second one
}
}
transcript.cues = transcriptCues;
isLoading = false;
}
@@ -172,7 +80,7 @@
if (!transcript) return;
if (search.trim() === '') {
transcriptCues = dedupeAndMergeOverlappingCues(transcript.cues);
transcriptCues = transcript.cues;
return;
}
@@ -180,8 +88,7 @@
keys: ['text']
});
const results = fuse.search(search).map((item) => item.item);
transcriptCues = dedupeAndMergeOverlappingCues(results);
transcriptCues = fuse.search(search).map((item) => item.item);
}
</script>
@@ -260,4 +167,8 @@
.transcript-text {
margin-left: 1em;
}
.transcript-line .chip {
padding: 0 1.5em;
}
</style>
+1
View File
@@ -13,6 +13,7 @@
main.root {
padding-left: 7em !important;
padding-top: 5em !important;
padding-bottom: 5em !important;
max-height: 100vh;
overflow-y: auto;
}
+7 -1
View File
@@ -31,7 +31,8 @@ import {
sponsorBlockUrlStore,
synciousInstanceStore,
synciousStore,
themeColorStore
themeColorStore,
interfaceAutoExpandChapters
} from '$lib/store';
import { get, type Writable } from 'svelte/store';
@@ -199,6 +200,11 @@ const persistedStores: {
name: 'defaultQuality',
store: playerDefaultQualityStore,
type: 'string'
},
{
name: 'autoExpandChapters',
store: interfaceAutoExpandChapters,
type: 'boolean'
}
];
@@ -72,6 +72,7 @@
"pauseTimer": "Pause timer",
"pauseVideoIn": "Pause video in ",
"preferredQuality": "Preferred quality",
"chapters": "Chapters",
"share": {
"title": "Share",
"materialiousLink": "Copy Materialious link",
@@ -126,6 +127,7 @@
"searchSuggestions": "Search suggestions",
"previewVideoOnHover": "Preview video on hover",
"expandDescription": "Expand description by default",
"expandChapters": "Expand chapters by default",
"expandComments": "Expand comments by default",
"allowInsecureRequests": "Allow insecure requests",
"dataPreferences": {
+4
View File
@@ -71,6 +71,10 @@ export const interfaceSearchSuggestionsStore = persisted('searchSuggestions', tr
export const interfaceForceCase: Writable<TitleCase> = persisted('forceCase', null);
export const interfaceAutoExpandComments: Writable<boolean> = persisted('autoExpandComments', true);
export const interfaceAutoExpandDesc: Writable<boolean> = persisted('autoExpandDesc', false);
export const interfaceAutoExpandChapters: Writable<boolean> = persisted(
'autoExpandChapters',
false
);
export const interfaceAmoledTheme = persisted('amoledTheme', false);
export const interfaceLowBandwidthMode = persisted('lowBandwidthMode', false);
export const interfaceDisplayThumbnailAvatars = persisted('disableThumbnailAvatars', false);
@@ -22,6 +22,7 @@
import type { PlayerEvents } from '$lib/player.js';
import {
authStore,
interfaceAutoExpandChapters,
interfaceAutoExpandComments,
interfaceAutoExpandDesc,
interfaceLowBandwidthMode,
@@ -71,12 +72,16 @@
let playerCurrentTime: number = $state(0);
function expandSummery(id: string) {
const element = document.getElementById(id);
if (element) {
element.click();
}
}
$effect(() => {
if ($interfaceAutoExpandComments && comments) {
const commentSectionElement = document.getElementById('comment-section');
if (commentSectionElement) {
commentSectionElement.click();
}
expandSummery('comment-section');
}
});
@@ -221,10 +226,11 @@
onMount(async () => {
if ($interfaceAutoExpandDesc) {
const descriptionElement = document.getElementById('description');
if (descriptionElement) {
descriptionElement.click();
}
expandSummery('description');
}
if ($interfaceAutoExpandChapters) {
expandSummery('chapter-section');
}
if ($syncPartyConnectionsStore) {
@@ -403,9 +409,6 @@
ui('#pause-timer');
}
let downloadStage: string | undefined;
let downloadProgress: number = 0;
</script>
<svelte:head>
@@ -422,18 +425,6 @@
{/key}
</div>
{#if downloadStage}
<article>
<h6>
{downloadStage}
{#if downloadProgress > 0}
({Math.round(downloadProgress)}%)
{/if}
</h6>
<progress class="max" value={downloadProgress} max="100"></progress>
</article>
{/if}
<h5>{letterCase(data.video.title)}</h5>
<div class="grid no-padding">
@@ -580,7 +571,8 @@
<summary id="description" class="bold none">
<nav>
<div class="max">
{numberWithCommas(data.video.viewCount)} views • {data.video.publishedText}
{numberWithCommas(data.video.viewCount)}
{$_('views')}{data.video.publishedText}
</div>
<i>expand_more</i>
</nav>
@@ -590,24 +582,6 @@
<div style="white-space: pre-line; overflow-wrap: break-word;">
{@html data.content.description}
</div>
{#if data.content}
{#if data.content.timestamps.length > 0}
<h6 style="margin-bottom: .3em;">Chapters</h6>
{#each data.content.timestamps as timestamp}
<button
onclick={() => {
if (playerElement) playerElement.currentTime = timestamp.time;
}}
class="timestamps"
>{timestamp.timePretty}
{#if !timestamp.title.startsWith('-')}
-
{/if}
{timestamp.title}</button
>
{/each}
{/if}
{/if}
</div>
<nav class="scroll">
@@ -620,6 +594,43 @@
</details>
</article>
{#if data.content.timestamps.length > 0}
<article>
<details>
<summary id="chapter-section" class="bold none">
<nav>
<div class="max">
<p>{$_('player.chapters')}</p>
</div>
<i>expand_more</i>
</nav>
</summary>
<div class="space"></div>
<div class="chapter-list">
<ul class="list">
{#each data.content.timestamps as timestamp}
<li
onclick={() => {
if (playerElement) playerElement.currentTime = timestamp.time;
}}
>
<img
class="round large"
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>
</div>
</li>
{/each}
</ul>
</div>
</details>
</article>
{/if}
{#if comments && comments.comments.length > 0}
<article>
<details>
@@ -767,22 +778,9 @@
</dialog>
<style>
:root {
--plyr-color-main: var(--primary);
}
.timestamps {
margin-left: 0;
margin-bottom: 0.4em;
display: block;
padding: 0;
text-align: left;
background-color: transparent;
color: var(--on-secondary-container);
}
.timestamps:hover {
padding: 0 0.5em;
.chapter-list {
max-height: 300px;
overflow-x: scroll;
}
.video-actions {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.9.1"
LATEST_VERSION = "1.9.2"
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
WORKING_DIR = os.path.join(