Improved link handling

This commit is contained in:
WardPearce
2025-04-08 19:54:36 +12:00
parent 858e0c8043
commit e2988d5be9
12 changed files with 81 additions and 35 deletions
@@ -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')
+2 -2
View File
@@ -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",
-1
View File
@@ -196,7 +196,6 @@ export class ElectronCapacitorApp {
event.preventDefault();
}
});
// Link electron plugins into the system.
setupCapacitorElectronPlugins();
+1 -1
View File
@@ -72,4 +72,4 @@
"terser": "^5.34.1",
"youtubei.js": "^13.3.0"
}
}
}
@@ -3,6 +3,7 @@
import '$lib/css/shaka-player-theme.css';
import { getBestThumbnail } from '$lib/images';
import { padTime, videoLength } from '$lib/time';
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,
-5
View File
@@ -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);
+44 -8
View File
@@ -1,33 +1,69 @@
import { type PhasedDescription, decodeHtmlCharCodes } from "./misc";
import { decodeHtmlCharCodes } from "./misc";
import { convertToSeconds } from "./time";
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>
@@ -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>
@@ -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';