@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 109
|
||||
versionName "1.8.5"
|
||||
versionCode 110
|
||||
versionName "1.8.6"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -48,6 +48,22 @@
|
||||
android:scheme="https"
|
||||
android:host="youtu.be"
|
||||
android:pathPrefix="/watch" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="youtube.com"
|
||||
android:pathPrefix="/shorts" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.youtube.com"
|
||||
android:pathPrefix="/shorts" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="m.youtube.com"
|
||||
android:pathPrefix="/shorts" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="youtu.be"
|
||||
android:pathPrefix="/shorts" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
@@ -54,7 +54,11 @@
|
||||
</content_rating>
|
||||
<releases>
|
||||
|
||||
<release version="1.8.5" date="2025-5-29">
|
||||
|
||||
<release version="1.8.6" date="2025-5-29">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.8.6</url>
|
||||
</release>
|
||||
<release version="1.8.5" date="2025-5-29">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.8.5</url>
|
||||
</release>
|
||||
<release version="1.8.4" date="2025-5-05">
|
||||
|
||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.8.5",
|
||||
"version": "1.8.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.8.5",
|
||||
"version": "1.8.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
@@ -2492,9 +2492,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "35.5.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-35.5.0.tgz",
|
||||
"integrity": "sha512-16ScwDuKgnuL7tSrEgBvQe1Hm4CSK0vbOusPFrDs4oIs3QOdEFtrP9i8+4yKQGXpszj4f4F0MQjKv1tu9E4Gvg==",
|
||||
"version": "35.2.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-35.2.1.tgz",
|
||||
"integrity": "sha512-LO4xXLpzkPPUVLvAbdHMlW7N9Z+Qqz+7QsmSWXluTIQMeJk+v7o36nUTZghyA2I1s//tlMvuaCtle6Qo2W0Ktg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.8.5",
|
||||
"version": "1.8.6",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
@@ -44,4 +44,4 @@
|
||||
"capacitor",
|
||||
"electron"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "materialious",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.6",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.8.5",
|
||||
"version": "1.8.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from 'svelte/legacy';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { bookmarkletSaveToUrl } from '$lib/externalSettings';
|
||||
import { letterCase, titleCases } from '$lib/letterCasing';
|
||||
@@ -73,8 +71,16 @@
|
||||
|
||||
if (invalidInstance) return;
|
||||
|
||||
const resp = await fetch(`${instance}/api/v1/trending`);
|
||||
if (!resp.ok) {
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(`${instance}/api/v1/trending`);
|
||||
} catch {
|
||||
invalidInstance = true;
|
||||
}
|
||||
|
||||
if (invalidInstance) return;
|
||||
|
||||
if (resp && !resp.ok) {
|
||||
invalidInstance = true;
|
||||
return;
|
||||
}
|
||||
@@ -87,7 +93,7 @@
|
||||
</script>
|
||||
|
||||
{#if Capacitor.isNativePlatform()}
|
||||
<form style="margin-bottom: 2em;" onsubmit={setInstance}>
|
||||
<form onsubmit={setInstance}>
|
||||
<nav>
|
||||
<div class="field label border max" class:invalid={invalidInstance}>
|
||||
<input bind:value={invidiousInstance} name="invidious-instance" type="text" />
|
||||
@@ -101,6 +107,9 @@
|
||||
</button>
|
||||
</nav>
|
||||
</form>
|
||||
{#if invalidInstance}
|
||||
<div style="margin-bottom: 4em;"></div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<button onclick={toggleDarkMode} class="no-margin">
|
||||
|
||||
@@ -3,19 +3,6 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import {
|
||||
deArrowInstanceStore,
|
||||
deArrowThumbnailInstanceStore,
|
||||
instanceStore,
|
||||
interfaceDefaultPage,
|
||||
interfaceForceCase,
|
||||
interfaceRegionStore,
|
||||
playerDefaultLanguage,
|
||||
returnYTDislikesInstanceStore,
|
||||
sponsorBlockCategoriesStore,
|
||||
sponsorBlockUrlStore,
|
||||
synciousInstanceStore
|
||||
} from '../../store';
|
||||
import ApiExtended from './ApiExtended.svelte';
|
||||
import DataPreferences from './DataPreferences.svelte';
|
||||
import DeArrow from './DeArrow.svelte';
|
||||
@@ -24,43 +11,6 @@
|
||||
import Ryd from './RYD.svelte';
|
||||
import SponsorBlock from './SponsorBlock.svelte';
|
||||
|
||||
let sponsorCategoriesList: string[] = [];
|
||||
sponsorBlockCategoriesStore.subscribe((value) => (sponsorCategoriesList = value));
|
||||
|
||||
let sponsorBlockInstance = get(sponsorBlockUrlStore);
|
||||
let synciousInstance = get(synciousInstanceStore);
|
||||
let returnYTInstance = get(returnYTDislikesInstanceStore);
|
||||
let invidiousInstance = get(instanceStore);
|
||||
let deArrowUrl = get(deArrowInstanceStore);
|
||||
let deArrowThumbnailUrl = get(deArrowThumbnailInstanceStore);
|
||||
let region = get(interfaceRegionStore);
|
||||
let forceCase = get(interfaceForceCase);
|
||||
let defaultLanguage = get(playerDefaultLanguage);
|
||||
let defaultPage = get(interfaceDefaultPage);
|
||||
|
||||
const languageNames = ISO6391.getAllCodes().map((code) =>
|
||||
ISO6391.getName(code).toLocaleLowerCase()
|
||||
);
|
||||
|
||||
const sponsorCategories = [
|
||||
{ name: $_('layout.sponsors.sponsor'), category: 'sponsor' },
|
||||
{ name: $_('layout.sponsors.unpaidSelfPromotion'), category: 'selfpromo' },
|
||||
{ name: $_('layout.sponsors.interactionReminder'), category: 'interaction' },
|
||||
{ name: $_('layout.sponsors.intermissionIntroAnimation'), category: 'intro' },
|
||||
{ name: $_('layout.sponsors.credits'), category: 'outro' },
|
||||
{ name: $_('layout.sponsors.preViewRecapHook'), category: 'preview' },
|
||||
{ name: $_('layout.sponsors.tangentJokes'), category: 'filler' }
|
||||
];
|
||||
|
||||
function toggleSponsor(category: string) {
|
||||
if (sponsorCategoriesList.includes(category)) {
|
||||
sponsorBlockCategoriesStore.set(sponsorCategoriesList.filter((value) => value !== category));
|
||||
} else {
|
||||
sponsorCategoriesList.push(category);
|
||||
sponsorBlockCategoriesStore.set(sponsorCategoriesList);
|
||||
}
|
||||
}
|
||||
|
||||
let activeTab:
|
||||
| 'interface'
|
||||
| 'player'
|
||||
@@ -84,15 +34,6 @@
|
||||
checkWidth();
|
||||
|
||||
addEventListener('resize', () => checkWidth());
|
||||
|
||||
// Set default language if not set already based off browser.
|
||||
if (!get(playerDefaultLanguage)) {
|
||||
const languageCode = navigator.language || navigator.languages[0];
|
||||
const languageName = ISO6391.getName(languageCode.split('-')[0]);
|
||||
if (languageName) {
|
||||
playerDefaultLanguage.set(languageName.toLowerCase());
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -181,12 +122,11 @@
|
||||
@media screen and (max-width: 1320px) {
|
||||
.tabbed {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 0 !important;
|
||||
flex: 0 !important;
|
||||
margin-bottom: 1em !important;
|
||||
align-items: start !important;
|
||||
padding: 1em 0 !important;
|
||||
overflow-x: scroll !important;
|
||||
background-color: transparent;
|
||||
border-bottom: solid 1px var(--outline-variant);
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
}
|
||||
|
||||
.sideways-root {
|
||||
height: 115px;
|
||||
height: 110px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,118 @@
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -48,9 +160,11 @@
|
||||
|
||||
isLoading = true;
|
||||
transcript = null;
|
||||
|
||||
const resp = await fetch(`${!video.fallbackPatch ? get(instanceStore) : ''}${url}`);
|
||||
transcript = await parseText(await resp.text(), { strict: false });
|
||||
transcriptCues = transcript.cues;
|
||||
|
||||
transcriptCues = dedupeAndMergeOverlappingCues(transcript.cues);
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
@@ -58,7 +172,7 @@
|
||||
if (!transcript) return;
|
||||
|
||||
if (search.trim() === '') {
|
||||
transcriptCues = transcript.cues;
|
||||
transcriptCues = dedupeAndMergeOverlappingCues(transcript.cues);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,7 +180,8 @@
|
||||
keys: ['text']
|
||||
});
|
||||
|
||||
transcriptCues = fuse.search(search).map((item) => item.item);
|
||||
const results = fuse.search(search).map((item) => item.item);
|
||||
transcriptCues = dedupeAndMergeOverlappingCues(results);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -59,16 +59,6 @@ button.row:hover {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-width: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
menu.mobile {
|
||||
z-index: 9999 !important;
|
||||
|
||||
@@ -285,7 +285,6 @@
|
||||
height: 50vh !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
pointer-events: all !important;
|
||||
z-index: 99999999 !important;
|
||||
pointer-events: auto !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
@@ -320,20 +319,18 @@
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
background: transparent !important;
|
||||
z-index: 99999998 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.youtube-theme .shaka-overflow-menu:hover,
|
||||
.youtube-theme .shaka-settings-menu:hover {
|
||||
z-index: 99999998 !important;
|
||||
pointer-events: auto !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
#shaka-container {
|
||||
z-index: 99999998 !important;
|
||||
z-index: 98 !important;
|
||||
pointer-events: auto !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
: window.electronAPI.generatePoToken;
|
||||
|
||||
poTokenCacheStore.set(
|
||||
await platformMinter(challengeResponse.bg_challenge, requestKey, visitorData)
|
||||
await platformMinter(challengeResponse.bg_challenge, requestKey, visitorData)
|
||||
);
|
||||
|
||||
const extraArgs: Record<string, any> = {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
darkModeStore,
|
||||
instanceStore,
|
||||
interfaceAmoledTheme,
|
||||
interfaceDefaultPage,
|
||||
showWarningStore,
|
||||
syncPartyPeerStore,
|
||||
themeColorStore
|
||||
@@ -36,11 +37,8 @@
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
let { children } = $props();
|
||||
|
||||
let mobileSearchShow = $state(false);
|
||||
|
||||
@@ -238,7 +236,7 @@
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<nav onclick={() => goto('/')} style="cursor: pointer;" class="m l">
|
||||
<nav onclick={() => goto($interfaceDefaultPage)} style="cursor: pointer;" class="m l">
|
||||
<Logo />
|
||||
<h6 class="l">Materialious</h6>
|
||||
</nav>
|
||||
|
||||
@@ -443,7 +443,7 @@
|
||||
<div class="s12 m12 l5">
|
||||
<nav>
|
||||
<a href={`/channel/${data.video.authorId}`}>
|
||||
<nav>
|
||||
<nav style="gap: 0.5em;">
|
||||
{#if !$interfaceLowBandwidthMode}
|
||||
<img
|
||||
class="circle large"
|
||||
@@ -482,18 +482,18 @@
|
||||
<div class="s12 m12 l7 video-actions">
|
||||
{#await data.streamed.returnYTDislikes then returnYTDislikes}
|
||||
{#if returnYTDislikes}
|
||||
<nav class="no-space" style="margin-right: .5em;">
|
||||
<nav class="no-space">
|
||||
<button style="cursor: default;" class="border left-round">
|
||||
<i class="small">thumb_up</i>
|
||||
<span>{cleanNumber(returnYTDislikes.likes)}</span>
|
||||
</button>
|
||||
<button style="cursor: default;" class="border right-round">
|
||||
<button style="cursor: default;margin-right: 0.5em;" class="border right-round">
|
||||
<i class="small">thumb_down_alt</i>
|
||||
<span>{cleanNumber(returnYTDislikes.dislikes)}</span>
|
||||
</button>
|
||||
</nav>
|
||||
{:else}
|
||||
<button style="cursor: default;" class="border">
|
||||
<button style="cursor: default;margin-right: 0.5em;" class="border">
|
||||
<i class="small">thumb_up</i>
|
||||
<span>{cleanNumber(data.video.likeCount)}</span>
|
||||
</button>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { authStore, interfaceDefaultPage } from '$lib/store.js';
|
||||
import { locale, waitLocale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export let ssr = false;
|
||||
export const ssr = false;
|
||||
|
||||
export async function load({ url }) {
|
||||
if (url.pathname.startsWith('/@')) {
|
||||
@@ -18,7 +18,7 @@ export async function load({ url }) {
|
||||
if (resolvedUrl.pageType === 'WEB_PAGE_TYPE_CHANNEL') {
|
||||
goto(`/channel/${resolvedUrl.ucid}`);
|
||||
}
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
LATEST_VERSION = "1.8.5"
|
||||
LATEST_VERSION = "1.8.6"
|
||||
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
|
||||
|
||||
WORKING_DIR = os.path.join(
|
||||
|
||||
Reference in New Issue
Block a user