@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 101
|
||||
versionName "1.7.19"
|
||||
versionCode 102
|
||||
versionName "1.7.20"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -14,7 +14,6 @@ dependencies {
|
||||
implementation project(':capacitor-clipboard')
|
||||
implementation project(':capacitor-screen-orientation')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capgo-inappbrowser')
|
||||
implementation project(':hugotomazi-capacitor-navigation-bar')
|
||||
implementation project(':capacitor-nodejs')
|
||||
|
||||
|
||||
@@ -17,9 +17,6 @@ project(':capacitor-screen-orientation').projectDir = new File('../node_modules/
|
||||
include ':capacitor-status-bar'
|
||||
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||
|
||||
include ':capgo-inappbrowser'
|
||||
project(':capgo-inappbrowser').projectDir = new File('../node_modules/@capgo/inappbrowser/android')
|
||||
|
||||
include ':hugotomazi-capacitor-navigation-bar'
|
||||
project(':hugotomazi-capacitor-navigation-bar').projectDir = new File('../node_modules/@hugotomazi/capacitor-navigation-bar/android')
|
||||
|
||||
|
||||
@@ -62,7 +62,11 @@
|
||||
|
||||
|
||||
|
||||
<release version="1.7.19" date="2025-4-04">
|
||||
|
||||
<release version="1.7.20" date="2025-4-08">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.7.20</url>
|
||||
</release>
|
||||
<release version="1.7.19" date="2025-4-04">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.7.19</url>
|
||||
</release>
|
||||
<release version="1.7.18" date="2025-4-04">
|
||||
|
||||
Generated
+8
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.7.19",
|
||||
"version": "1.7.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.7.19",
|
||||
"version": "1.7.20",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
@@ -2506,9 +2506,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "34.4.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-34.4.1.tgz",
|
||||
"integrity": "sha512-iYzeLBdCrAR3i0RVSLa+mzuFZwH6HGxTGKsI+SS41sg2anZj4R5mHjOiHsxcZ50/ih47NJbuVRJgPIVlTF+USg==",
|
||||
"version": "34.5.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-34.5.0.tgz",
|
||||
"integrity": "sha512-GabFMG7r2P1NQf5DYp6mnCXo5CcatxXb8YQo54VTStql6weeEv7tsqvl3lAssGwDdd4iMc8QpTCFjErBSVRWeQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -5751,9 +5751,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.7.19",
|
||||
"version": "1.7.20",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { USER_AGENT } from 'bgutils-js';
|
||||
import chokidar from 'chokidar';
|
||||
import type { MenuItemConstructorOptions } from 'electron';
|
||||
import { app, BrowserWindow, Menu, MenuItem, nativeImage, session, Tray } from 'electron';
|
||||
import { app, BrowserWindow, Menu, MenuItem, nativeImage, session, shell, Tray } from 'electron';
|
||||
import electronIsDev from 'electron-is-dev';
|
||||
import electronServe from 'electron-serve';
|
||||
import windowStateKeeper from 'electron-window-state';
|
||||
@@ -185,18 +185,14 @@ export class ElectronCapacitorApp {
|
||||
|
||||
// Security
|
||||
this.MainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
if (!details.url.includes(this.customScheme)) {
|
||||
return { action: 'deny' };
|
||||
} else {
|
||||
return { action: 'allow' };
|
||||
}
|
||||
shell.openExternal(details.url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
this.MainWindow.webContents.on('will-navigate', (event, _newURL) => {
|
||||
if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Link electron plugins into the system.
|
||||
setupCapacitorElectronPlugins();
|
||||
|
||||
|
||||
Generated
+8
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.7.19",
|
||||
"version": "1.7.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "materialious",
|
||||
"version": "1.7.19",
|
||||
"version": "1.7.20",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
@@ -3423,9 +3423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.2.tgz",
|
||||
"integrity": "sha512-Dv8TOAZC9vyfcAB9TMsvUEJsRbklRTeNfcYBPaeH6KnABJ99i3CvCB2eNx8fiiliIqe+9GIchBg4RodRH5p1BQ==",
|
||||
"version": "2.20.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.4.tgz",
|
||||
"integrity": "sha512-B3Y1mb1Qjt57zXLVch5tfqsK/ebHe6uYTcFSnGFNwRpId3+fplLgQK6Z2zhDVBezSsPuhDq6Pry+9PA88ocN6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -12904,9 +12904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
|
||||
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
|
||||
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.7.19",
|
||||
"version": "1.7.20",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { cleanNumber } from '$lib/time';
|
||||
import { cleanNumber } from '$lib/numbers';
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { getComments } from '$lib/api';
|
||||
import { type Comment, type Comments } from '$lib/api/model';
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { numberWithCommas } from '$lib/numbers';
|
||||
import { interfaceLowBandwidthMode } from '$lib/store';
|
||||
import { numberWithCommas } from '$lib/time';
|
||||
import CommentSelf from './Comment.svelte';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cleanNumber } from '$lib/time';
|
||||
import { cleanNumber } from '$lib/numbers';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import type { HashTag } from '../api/model';
|
||||
import { truncate } from '../misc';
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { page } from '$app/stores';
|
||||
import '$lib/css/shaka-player-theme.css';
|
||||
import { getBestThumbnail } from '$lib/images';
|
||||
import { padTime, videoLength } from '$lib/time';
|
||||
import { padTime, videoLength } from '$lib/numbers';
|
||||
import { type PhasedDescription } from '$lib/timestamps';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation';
|
||||
import { StatusBar, Style } from '@capacitor/status-bar';
|
||||
@@ -19,7 +20,6 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { deleteVideoProgress, getVideoProgress, saveVideoProgress } from '../api';
|
||||
import type { VideoPlay } from '../api/model';
|
||||
import { type PhasedDescription } from '../misc';
|
||||
import {
|
||||
authStore,
|
||||
instanceStore,
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
|
||||
selectedSuggestionIndex = -1;
|
||||
goto(`/search/${encodeURIComponent(search)}`);
|
||||
dispatch('searchSubmitted');
|
||||
|
||||
suggestionsForSearch = [];
|
||||
showSearchBox = false;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { letterCase } from '$lib/letterCasing';
|
||||
import { cleanNumber, videoLength } from '$lib/time';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { cleanNumber, videoLength } from '$lib/numbers';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { getChannel, getDeArrow, getThumbnail, getVideo, getVideoProgress } from '../api';
|
||||
import { getChannel, getDeArrow, getThumbnail, getVideoProgress } from '../api';
|
||||
import type { Notification, PlaylistPageVideo, Video, VideoBase } from '../api/model';
|
||||
import { proxyVideoUrl, truncate } from '../misc';
|
||||
import { truncate } from '../misc';
|
||||
import type { PlayerEvents } from '../player';
|
||||
import {
|
||||
authStore,
|
||||
deArrowEnabledStore,
|
||||
deArrowTitlesOnly,
|
||||
interfaceDisplayThumbnailAvatars,
|
||||
interfaceLowBandwidthMode,
|
||||
playerSavePlaybackPositionStore,
|
||||
@@ -22,8 +21,6 @@
|
||||
synciousStore
|
||||
} from '../store';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
interface Props {
|
||||
video: VideoBase | Video | Notification | PlaylistPageVideo;
|
||||
playlistId?: string;
|
||||
@@ -32,8 +29,6 @@
|
||||
|
||||
let { video = $bindable(), playlistId = '', sideways = $bindable(false) }: Props = $props();
|
||||
|
||||
let thumbnailHidden: boolean = $state(false);
|
||||
|
||||
let authorImg: HTMLImageElement | undefined = $state();
|
||||
|
||||
let placeholderHeight: number = $state(0);
|
||||
@@ -88,15 +83,6 @@
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
thumbnailHidden = localStorage.getItem(`v_h_${video.videoId}`) === '1';
|
||||
} catch {}
|
||||
|
||||
if (thumbnailHidden) {
|
||||
dispatch('videoHidden');
|
||||
return;
|
||||
}
|
||||
|
||||
calcThumbnailPlaceholderHeight();
|
||||
|
||||
// Check if sideways should be enabled or disabled.
|
||||
@@ -138,46 +124,6 @@
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (!locatedThumbnail && !get(deArrowTitlesOnly)) {
|
||||
// Process thumbnail locally.
|
||||
const canvas = document.getElementById('canvas') as HTMLCanvasElement | null;
|
||||
function generateThumbnail(): Promise<string> {
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
if (canvas) {
|
||||
const context = canvas.getContext('2d');
|
||||
const mockVideo = document.createElement('video');
|
||||
const videoContainer = document.getElementById('video-container');
|
||||
videoContainer?.appendChild(mockVideo);
|
||||
|
||||
mockVideo.preload = 'auto';
|
||||
mockVideo.id = 'video';
|
||||
mockVideo.crossOrigin = 'anonymous';
|
||||
|
||||
const videoDetails = (await getVideo(video.videoId)).formatStreams[0];
|
||||
|
||||
mockVideo.src = proxyVideoUrl(videoDetails.url);
|
||||
|
||||
mockVideo.addEventListener('loadeddata', () => {
|
||||
mockVideo.currentTime = mockVideo.duration / 50;
|
||||
|
||||
mockVideo.addEventListener('seeked', () => {
|
||||
if (context) {
|
||||
context.drawImage(mockVideo, 0, 0, canvas.width, canvas.height);
|
||||
resolve(canvas.toDataURL('image/png'));
|
||||
mockVideo.preload = 'none';
|
||||
mockVideo.pause();
|
||||
videoContainer?.removeChild(mockVideo);
|
||||
}
|
||||
});
|
||||
});
|
||||
mockVideo.load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
imageSrc = await generateThumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
@@ -233,99 +179,89 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !thumbnailHidden}
|
||||
<div class:sideways-root={sideways}>
|
||||
<div onfocus={() => {}} id="thumbnail-container" role="region">
|
||||
<a
|
||||
class="wave thumbnail"
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
onclick={syncChangeVideo}
|
||||
>
|
||||
{#if !$interfaceLowBandwidthMode}
|
||||
{#if !thumbnail}
|
||||
<div
|
||||
class="secondary-container"
|
||||
style="width: 100%;height: {placeholderHeight}px;"
|
||||
></div>
|
||||
{:else}
|
||||
<img class="responsive" src={thumbnail.src} alt="Thumbnail for video" />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if progress}
|
||||
<progress
|
||||
class="absolute right bottom"
|
||||
style="z-index: 1;"
|
||||
value={progress}
|
||||
max={video.lengthSeconds}
|
||||
></progress>
|
||||
{/if}
|
||||
{#if !('liveVideo' in video) || !video.liveVideo}
|
||||
{#if video.lengthSeconds !== 0}
|
||||
{#if !$interfaceLowBandwidthMode}
|
||||
<div
|
||||
class="absolute right bottom small-margin black white-text small-text thumbnail-corner"
|
||||
>
|
||||
{videoLength(video.lengthSeconds)}
|
||||
</div>
|
||||
{:else}
|
||||
<h3>{videoLength(video.lengthSeconds)}</h3>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if video.lengthSeconds !== 0}
|
||||
<div
|
||||
class="absolute right bottom small-margin red white-text small-text thumbnail-corner"
|
||||
>
|
||||
{$_('thumbnail.live')}
|
||||
</div>
|
||||
<div class:sideways-root={sideways}>
|
||||
<div onfocus={() => {}} id="thumbnail-container" role="region">
|
||||
<a
|
||||
class="wave thumbnail"
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
onclick={syncChangeVideo}
|
||||
>
|
||||
{#if !$interfaceLowBandwidthMode}
|
||||
{#if !thumbnail}
|
||||
<div class="secondary-container" style="width: 100%;height: {placeholderHeight}px;"></div>
|
||||
{:else}
|
||||
<h3>{$_('thumbnail.live')}</h3>
|
||||
<img class="responsive" src={thumbnail.src} alt="Thumbnail for video" />
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="thumbnail-details video-title">
|
||||
{#if !sideways && !$interfaceLowBandwidthMode && $interfaceDisplayThumbnailAvatars}
|
||||
<div style="margin-right: 1em;">
|
||||
{#if authorImg}
|
||||
<img src={authorImg.src} alt="Author" class="circle small" />
|
||||
{:else}
|
||||
<progress style="padding: 15px;" class="circle small"></progress>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="video-title">
|
||||
<a
|
||||
style="padding-left: 1px;"
|
||||
class="video-title"
|
||||
data-sveltekit-preload-data="off"
|
||||
href={watchUrl.toString()}
|
||||
>
|
||||
<span class="bold">{letterCase(truncate(video.title.trimEnd(), 80))}</span>
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<a class:author={!sideways} href={`/channel/${video.authorId}`}>{video.author}</a>
|
||||
{#if !('publishedText' in video) && 'viewCountText' in video}
|
||||
<span style="margin-top: 1em;">
|
||||
• {video.viewCountText}
|
||||
{$_('views')}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if 'publishedText' in video}
|
||||
<div class="max">
|
||||
{cleanNumber(video.viewCount)} • {video.publishedText}
|
||||
{#if progress}
|
||||
<progress
|
||||
class="absolute right bottom"
|
||||
style="z-index: 1;"
|
||||
value={progress}
|
||||
max={video.lengthSeconds}
|
||||
></progress>
|
||||
{/if}
|
||||
{#if !('liveVideo' in video) || !video.liveVideo}
|
||||
{#if video.lengthSeconds !== 0}
|
||||
{#if !$interfaceLowBandwidthMode}
|
||||
<div
|
||||
class="absolute right bottom small-margin black white-text small-text thumbnail-corner"
|
||||
>
|
||||
{videoLength(video.lengthSeconds)}
|
||||
</div>
|
||||
{:else}
|
||||
<h3>{videoLength(video.lengthSeconds)}</h3>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if video.lengthSeconds !== 0}
|
||||
<div class="absolute right bottom small-margin red white-text small-text thumbnail-corner">
|
||||
{$_('thumbnail.live')}
|
||||
</div>
|
||||
{:else}
|
||||
<h3>{$_('thumbnail.live')}</h3>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="thumbnail-details video-title">
|
||||
{#if !sideways && !$interfaceLowBandwidthMode && $interfaceDisplayThumbnailAvatars}
|
||||
<div style="margin-right: 1em;">
|
||||
{#if authorImg}
|
||||
<img src={authorImg.src} alt="Author" class="circle small" />
|
||||
{:else}
|
||||
<progress style="padding: 15px;" class="circle small"></progress>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="video-title">
|
||||
<a
|
||||
style="padding-left: 1px;"
|
||||
class="video-title"
|
||||
data-sveltekit-preload-data="off"
|
||||
href={watchUrl.toString()}
|
||||
>
|
||||
<span class="bold">{letterCase(truncate(video.title.trimEnd(), 80))}</span>
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<a class:author={!sideways} href={`/channel/${video.authorId}`}>{video.author}</a>
|
||||
{#if !('publishedText' in video) && 'viewCountText' in video}
|
||||
<span style="margin-top: 1em;">
|
||||
• {video.viewCountText}
|
||||
{$_('views')}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if 'publishedText' in video}
|
||||
<div class="max">
|
||||
{cleanNumber(video.viewCount)} • {video.publishedText}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<canvas id="canvas" style="display: none;"></canvas>
|
||||
<div id="video-container" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.thumbnail {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { videoLength } from '$lib/time';
|
||||
import { videoLength } from '$lib/numbers';
|
||||
import Fuse from 'fuse.js';
|
||||
import { VTTCue, parseText, type ParsedCaptionsResult } from 'media-captions';
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
@@ -15,13 +15,11 @@
|
||||
|
||||
let { videos = [], playlistId = '', playlistAuthor = '' }: Props = $props();
|
||||
|
||||
let hiddenVideos: string[] = $state([]);
|
||||
let auth = get(authStore);
|
||||
|
||||
async function removePlaylistItem(indexId: string, videoId: string) {
|
||||
if (!playlistId) return;
|
||||
await removePlaylistVideo(playlistId, indexId);
|
||||
hiddenVideos = [...hiddenVideos, videoId];
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -29,11 +27,9 @@
|
||||
<div class="space"></div>
|
||||
<div class="grid">
|
||||
{#each videos as video}
|
||||
{#if !hiddenVideos.includes(video.videoId)}
|
||||
<ContentColumn>
|
||||
<article class="no-padding" style="height: 100%;">
|
||||
<Thumbnail
|
||||
on:videoHidden={() => (hiddenVideos = [...hiddenVideos, video.videoId])}
|
||||
{video}
|
||||
{playlistId}
|
||||
/>
|
||||
@@ -50,7 +46,6 @@
|
||||
{/if}
|
||||
</article>
|
||||
</ContentColumn>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,6 @@ export function truncate(value: string, maxLength: number = 50): string {
|
||||
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value;
|
||||
}
|
||||
|
||||
export interface PhasedDescription {
|
||||
description: string;
|
||||
timestamps: { title: string; time: number; timePretty: string; }[];
|
||||
}
|
||||
|
||||
export function decodeHtmlCharCodes(str: string): string {
|
||||
const { decode } = he;
|
||||
return decode(str);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { androidPoTokenMinter } from '$lib/android/youtube/minter';
|
||||
import type { AdaptiveFormats, Captions, Image, StoryBoard, Thumbnail, VideoBase, VideoPlay } from '$lib/api/model';
|
||||
import { numberWithCommas } from '$lib/numbers';
|
||||
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
|
||||
import { numberWithCommas } from '$lib/time';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { USER_AGENT } from 'bgutils-js';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
@@ -1,33 +1,69 @@
|
||||
import { type PhasedDescription, decodeHtmlCharCodes } from "./misc";
|
||||
import { convertToSeconds } from "./time";
|
||||
import { decodeHtmlCharCodes } from "./misc";
|
||||
import { convertToSeconds } from "./numbers";
|
||||
|
||||
export function phaseDescription(content: string, usingYoutubeJs: boolean = false): PhasedDescription {
|
||||
export interface PhasedDescription {
|
||||
description: string;
|
||||
timestamps: { title: string; time: number; timePretty: string; }[];
|
||||
}
|
||||
|
||||
export function extractActualLink(url: string): string {
|
||||
const urlParams = new URLSearchParams(url.split('?')[1]);
|
||||
const actualLink = urlParams.get('q');
|
||||
if (actualLink) {
|
||||
return decodeURIComponent(actualLink);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function processYoutubeLink(line: string): string {
|
||||
// Regex to match the <a> tag and extract the href (YouTube redirect link)
|
||||
const urlRegex = /<a href="https:\/\/www\.youtube\.com\/redirect\?([^"]+)"/;
|
||||
|
||||
const urlMatch = urlRegex.exec(line);
|
||||
|
||||
if (urlMatch) {
|
||||
// Extract the YouTube redirect URL and get the actual URL from the `q` parameter
|
||||
const redirectUrl = urlMatch[0]; // the full redirect URL with the `q` parameter
|
||||
const actualUrl = extractActualLink(redirectUrl);
|
||||
return line.replace(urlRegex, `<a href="${actualUrl}"`);
|
||||
} else {
|
||||
// If no match found, just return the original line
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
export function phaseDescription(videoId: string, content: string, fallbackPatch?: 'youtubejs' | 'piped'): PhasedDescription {
|
||||
const timestamps: { title: string; time: number; timePretty: string; }[] = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Regular expressions for different timestamp formats
|
||||
const urlRegex = /<a href="([^"]+)"/;
|
||||
const timestampRegexInvidious = /<a href="([^"]+)" data-onclick="jump_to_time" data-jump-time="(\d+)">(\d+:\d+(?::\d+)?)<\/a>\s*(.+)/;
|
||||
const timestampRegexYtJs = /&(?:\S*?&)?t=(\d+)\s*s.*?<span[^>]*>([^<]*)<\/span>.*?>(.*?)<\/span>/;
|
||||
const timestampRegexYtJs = new RegExp(
|
||||
`href="https://www\\.youtube\\.com/watch\\?v=${videoId}(?:&t=(\\d+)s)?"[^>]*>\\s*<span[^>]*>\\s*([^<]+)\\s*</span>\\s*</a>\\s*<span[^>]*>\\s*([^<]+)\\s*</span>`,
|
||||
'i'
|
||||
);
|
||||
|
||||
let filteredLines: string[] = [];
|
||||
lines.forEach((line) => {
|
||||
const urlMatch = urlRegex.exec(line);
|
||||
// Use appropriate regex based on the `usingYoutubeJs` flag
|
||||
const timestampMatch = (usingYoutubeJs ? timestampRegexYtJs : timestampRegexInvidious).exec(usingYoutubeJs ? line + '</span>' : line);
|
||||
const timestampMatch = (fallbackPatch === 'youtubejs' ? timestampRegexYtJs : timestampRegexInvidious).exec(fallbackPatch === 'youtubejs' ? line + '</span>' : line);
|
||||
|
||||
if (urlMatch !== null && timestampMatch === null) {
|
||||
// If line contains a URL but not a timestamp, modify the URL
|
||||
const modifiedLine = line.replace(
|
||||
const modifiedLine = processYoutubeLink(line).replace(
|
||||
/<a href="([^"]+)"/,
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer" class="link"'
|
||||
);
|
||||
console.log(modifiedLine);
|
||||
filteredLines.push(modifiedLine);
|
||||
} else if (timestampMatch !== null) {
|
||||
// If line contains a timestamp, extract details and push into timestamps array
|
||||
const time = usingYoutubeJs ? timestampMatch[1] : timestampMatch[2];
|
||||
const timestamp = usingYoutubeJs ? timestampMatch[2] : timestampMatch[3];
|
||||
const title = usingYoutubeJs ? timestampMatch[3] || '' : timestampMatch[4] || '';
|
||||
const time = (fallbackPatch === 'youtubejs' ? timestampMatch[1] : timestampMatch[2]) || '0';
|
||||
const timestamp = fallbackPatch === 'youtubejs' ? timestampMatch[2] : timestampMatch[3];
|
||||
const title = fallbackPatch === 'youtubejs' ? timestampMatch[3] || '' : timestampMatch[4] || '';
|
||||
|
||||
timestamps.push({
|
||||
time: convertToSeconds(time),
|
||||
// Remove any HTML in the timestamp title.
|
||||
|
||||
@@ -153,6 +153,8 @@
|
||||
onMount(async () => {
|
||||
ui();
|
||||
|
||||
document.addEventListener('click', linkClickOverwrite);
|
||||
|
||||
scrollableRoot = document.querySelector('.root');
|
||||
|
||||
loadSettingsFromEnv();
|
||||
@@ -183,6 +185,21 @@
|
||||
}
|
||||
});
|
||||
|
||||
function linkClickOverwrite(event: MouseEvent) {
|
||||
// Handles opening links in browser for android.
|
||||
|
||||
if (Capacitor.getPlatform() !== 'android') return;
|
||||
|
||||
const link = (event.target as HTMLElement).closest('a');
|
||||
|
||||
if (link && link.href) {
|
||||
if (link.href && link.href.startsWith('http') && link.target === '_blank') {
|
||||
event.preventDefault();
|
||||
Browser.open({ url: link.href });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let webManifestLink = $derived(pwaInfo ? pwaInfo.webManifest.linkTag : '');
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
import PlaylistThumbnail from '$lib/components/PlaylistThumbnail.svelte';
|
||||
import VideoList from '$lib/components/VideoList.svelte';
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { cleanNumber } from '$lib/numbers';
|
||||
import { activePageStore, authStore, interfaceLowBandwidthMode } from '$lib/store';
|
||||
import { cleanNumber } from '$lib/time';
|
||||
import { Clipboard } from '@capacitor/clipboard';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -137,21 +137,24 @@
|
||||
<span>{$_('player.share.title')}</span>
|
||||
<menu class="no-wrap mobile">
|
||||
{#if !Capacitor.isNativePlatform()}
|
||||
<a
|
||||
href="#share"
|
||||
<button
|
||||
class="row"
|
||||
onclick={async () => {
|
||||
await Clipboard.write({ string: location.href });
|
||||
}}>{$_('player.share.materialiousLink')}</a
|
||||
}}>{$_('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
|
||||
>
|
||||
{/if}
|
||||
<a
|
||||
href="#share"
|
||||
onclick={async () => {
|
||||
await Clipboard.write({
|
||||
string: `https://www.youtube.com/channel/${data.channel.authorId}`
|
||||
});
|
||||
}}>{$_('player.share.youtubeLink')}</a
|
||||
>
|
||||
</menu>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import type { PlaylistPageVideo } from '$lib/api/model';
|
||||
import VideoList from '$lib/components/VideoList.svelte';
|
||||
import { unsafeRandomItem } from '$lib/misc';
|
||||
import { cleanNumber } from '$lib/numbers';
|
||||
import { activePageStore, playlistSettingsStore } from '$lib/store';
|
||||
import { cleanNumber } from '$lib/time';
|
||||
import { Clipboard } from '@capacitor/clipboard';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { letterCase } from '$lib/letterCasing';
|
||||
import { truncate, unsafeRandomItem } from '$lib/misc';
|
||||
import { cleanNumber, humanizeSeconds, numberWithCommas } from '$lib/numbers';
|
||||
import type { PlayerEvents } from '$lib/player.js';
|
||||
import {
|
||||
activePageStore,
|
||||
@@ -31,7 +32,6 @@
|
||||
syncPartyConnectionsStore,
|
||||
syncPartyPeerStore
|
||||
} from '$lib/store';
|
||||
import { cleanNumber, humanizeSeconds, numberWithCommas } from '$lib/time';
|
||||
import ui from 'beercss';
|
||||
import type { DataConnection } from 'peerjs';
|
||||
import { type Segment } from 'sponsorblock-api';
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function load({ params, url }) {
|
||||
|
||||
return {
|
||||
video: video,
|
||||
content: phaseDescription(video.descriptionHtml, video.fallbackPatch === 'youtubejs'),
|
||||
content: phaseDescription(video.videoId, video.descriptionHtml, video.fallbackPatch),
|
||||
playlistId: url.searchParams.get('playlist'),
|
||||
streamed: {
|
||||
personalPlaylists: personalPlaylists,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getVideo } from '$lib/api/index';
|
||||
import type { PhasedDescription } from '$lib/misc';
|
||||
import { playerProxyVideosStore } from '$lib/store';
|
||||
import type { PhasedDescription } from '$lib/timestamps';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
LATEST_VERSION = "1.7.19"
|
||||
LATEST_VERSION = "1.7.20"
|
||||
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