Refactoring misc
This commit is contained in:
@@ -106,7 +106,7 @@ export class ElectronCapacitorApp {
|
||||
defaultHeight: 800,
|
||||
});
|
||||
// Setup preload script path and construct our main window.
|
||||
const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js');
|
||||
const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload);
|
||||
this.MainWindow = new BrowserWindow({
|
||||
icon,
|
||||
show: false,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { cleanNumber } from '$lib/time';
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Channel } from '../api/model';
|
||||
import { cleanNumber, getBestThumbnail, proxyGoogleImage, truncate } from '../misc';
|
||||
import { truncate } from '../misc';
|
||||
import { interfaceLowBandwidthMode } from '../store';
|
||||
|
||||
export let channel: Channel;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getComments } from '$lib/api';
|
||||
import { type Comment, type Comments } from '$lib/api/model';
|
||||
import { getBestThumbnail, numberWithCommas, proxyGoogleImage } from '$lib/misc';
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { interfaceLowBandwidthMode } from '$lib/store';
|
||||
import { numberWithCommas } from '$lib/time';
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
export let comment: Comment;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { cleanNumber } from '$lib/time';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import type { HashTag } from '../api/model';
|
||||
import { cleanNumber, truncate } from '../misc';
|
||||
import { truncate } from '../misc';
|
||||
|
||||
export let hashtag: HashTag;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import 'vidstack/bundle';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { padTime, videoLength } from '$lib/time';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation';
|
||||
import { StatusBar, Style } from '@capacitor/status-bar';
|
||||
@@ -16,14 +18,7 @@
|
||||
import type { MediaPlayerElement } from 'vidstack/elements';
|
||||
import { deleteVideoProgress, getVideoProgress, saveVideoProgress } from '../api';
|
||||
import type { VideoPlay } from '../api/model';
|
||||
import {
|
||||
getBestThumbnail,
|
||||
padTime,
|
||||
proxyVideoUrl,
|
||||
pullBitratePreference,
|
||||
videoLength,
|
||||
type PhasedDescription
|
||||
} from '../misc';
|
||||
import { proxyVideoUrl, pullBitratePreference, type PhasedDescription } from '../misc';
|
||||
import {
|
||||
authStore,
|
||||
instanceStore,
|
||||
@@ -610,7 +605,9 @@
|
||||
>
|
||||
<media-provider>
|
||||
{#if !audioMode}
|
||||
<media-poster class="vds-poster" src={getBestThumbnail(data.video.videoThumbnails, 1251, 781)}
|
||||
<media-poster
|
||||
class="vds-poster"
|
||||
src={proxyGoogleImage(getBestThumbnail(data.video.videoThumbnails, 1251, 781))}
|
||||
></media-poster>
|
||||
{/if}
|
||||
</media-provider>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { getBestThumbnail } from '$lib/images';
|
||||
import { letterCase } from '$lib/letterCasing';
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Playlist } from '../api/model';
|
||||
import { getBestThumbnail, letterCase, truncate } from '../misc';
|
||||
import { truncate } from '../misc';
|
||||
import { interfaceLowBandwidthMode } from '../store';
|
||||
|
||||
export let playlist: Playlist;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { bookmarkletSaveToUrl } from '$lib/externalSettings';
|
||||
import { letterCase, titleCase, titleCases } from '$lib/letterCasing';
|
||||
import { Clipboard } from '@capacitor/clipboard';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import ui from 'beercss';
|
||||
@@ -9,7 +10,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { ensureNoTrailingSlash, letterCase, titleCase, titleCases } from '../misc';
|
||||
import { ensureNoTrailingSlash } from '../misc';
|
||||
import { getPages } from '../navPages';
|
||||
import {
|
||||
authStore,
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
<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 { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { getChannel, getDeArrow, getThumbnail, getVideo, getVideoProgress } from '../api';
|
||||
import type { Notification, PlaylistPageVideo, Video, VideoBase, VideoPlay } from '../api/model';
|
||||
import {
|
||||
cleanNumber,
|
||||
getBestThumbnail,
|
||||
letterCase,
|
||||
proxyGoogleImage,
|
||||
proxyVideoUrl,
|
||||
truncate,
|
||||
videoLength
|
||||
} from '../misc';
|
||||
import { proxyVideoUrl, truncate } from '../misc';
|
||||
import type { PlayerEvents } from '../player';
|
||||
import {
|
||||
authStore,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { videoLength } from '$lib/time';
|
||||
import Fuse from 'fuse.js';
|
||||
import { VTTCue, parseText, type ParsedCaptionsResult } from 'media-captions';
|
||||
import { _ } from 'svelte-i18n';
|
||||
@@ -6,7 +7,7 @@
|
||||
import type { MediaTimeUpdateEvent } from 'vidstack';
|
||||
import type { MediaPlayerElement } from 'vidstack/elements';
|
||||
import type { VideoPlay } from '../api/model';
|
||||
import { decodeHtmlCharCodes, videoLength } from '../misc';
|
||||
import { decodeHtmlCharCodes } from '../misc';
|
||||
import { instanceStore } from '../store';
|
||||
|
||||
export let video: VideoPlay;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { get } from "svelte/store";
|
||||
import type { Image } from "./api/model";
|
||||
import { instanceStore } from "./store";
|
||||
|
||||
export function getBestThumbnail(
|
||||
images: Image[] | null,
|
||||
maxWidthDimension: number = 480,
|
||||
maxHeightDimension = 360
|
||||
): string {
|
||||
if (images && images.length > 0) {
|
||||
const imagesFiltered = images.filter(
|
||||
(image) => image.width < maxWidthDimension && image.height < maxHeightDimension
|
||||
);
|
||||
|
||||
if (imagesFiltered.length === 0) {
|
||||
return images[0].url;
|
||||
}
|
||||
|
||||
imagesFiltered.sort((a, b) => {
|
||||
return b.width * b.height - a.width * a.height;
|
||||
});
|
||||
|
||||
return imagesFiltered[0].url;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function proxyGoogleImage(source: string): string {
|
||||
if (source.startsWith('//')) source = `https:${source}`;
|
||||
|
||||
let path: string | undefined;
|
||||
try {
|
||||
path = new URL(source).pathname;
|
||||
} catch { }
|
||||
|
||||
if (typeof path === 'undefined') return '';
|
||||
|
||||
return `${get(instanceStore)}/ggpht${path}`;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { get } from "svelte/store";
|
||||
import { interfaceForceCase } from "./store";
|
||||
|
||||
export type TitleCase = 'uppercase' | 'lowercase' | 'sentence case' | 'title case' | null;
|
||||
export const titleCases: TitleCase[] = [
|
||||
'lowercase',
|
||||
'uppercase',
|
||||
'title case',
|
||||
'sentence case'
|
||||
];
|
||||
|
||||
export function letterCase(text: string, caseTypeOverwrite?: TitleCase): string {
|
||||
const casing = caseTypeOverwrite ? caseTypeOverwrite : get(interfaceForceCase);
|
||||
if (!casing) return text;
|
||||
|
||||
switch (casing) {
|
||||
case 'lowercase':
|
||||
return text.toLowerCase();
|
||||
case 'uppercase':
|
||||
return text.toUpperCase();
|
||||
case 'sentence case':
|
||||
return sentenceCase(text);
|
||||
default:
|
||||
return titleCase(text);
|
||||
}
|
||||
}
|
||||
|
||||
export function sentenceCase(text: string): string {
|
||||
let sentences: string[] = text.match(/[^.!?]*[.!?]*/g) || [];
|
||||
|
||||
let casedSentences: string[] = sentences.map(sentence => {
|
||||
sentence = sentence.trim();
|
||||
if (sentence.length === 0) return '';
|
||||
return sentence.charAt(0).toUpperCase() + sentence.slice(1).toLowerCase();
|
||||
});
|
||||
|
||||
return casedSentences.join(' ');
|
||||
}
|
||||
|
||||
export function titleCase(text: string): string {
|
||||
if (!text) return '';
|
||||
|
||||
let words: string[] = text.split(/(\s+)/).filter(word => word.trim().length > 0);
|
||||
|
||||
let titleCasedWords: string[] = words.map(word => {
|
||||
if (word.length === 0) return '';
|
||||
|
||||
const firstChar = word.charAt(0);
|
||||
const restOfWord = word.slice(1).toLowerCase();
|
||||
|
||||
// Capitalize if the first character is a letter
|
||||
if (/[a-zA-Z]/.test(firstChar)) {
|
||||
return firstChar.toUpperCase() + restOfWord;
|
||||
}
|
||||
|
||||
return word;
|
||||
});
|
||||
|
||||
return titleCasedWords.join(' ');
|
||||
}
|
||||
|
||||
@@ -1,220 +1,24 @@
|
||||
import { pushState } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import he from 'he';
|
||||
import humanNumber from 'human-number';
|
||||
import type Peer from 'peerjs';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Image } from './api/model';
|
||||
import { instanceStore, interfaceForceCase } from './store';
|
||||
|
||||
|
||||
export type TitleCase = 'uppercase' | 'lowercase' | 'sentence case' | 'title case' | null;
|
||||
export const titleCases: TitleCase[] = [
|
||||
'lowercase',
|
||||
'uppercase',
|
||||
'title case',
|
||||
'sentence case'
|
||||
];
|
||||
|
||||
export function letterCase(text: string, caseTypeOverwrite?: TitleCase): string {
|
||||
const casing = caseTypeOverwrite ? caseTypeOverwrite : get(interfaceForceCase);
|
||||
if (!casing) return text;
|
||||
|
||||
switch (casing) {
|
||||
case 'lowercase':
|
||||
return text.toLowerCase();
|
||||
case 'uppercase':
|
||||
return text.toUpperCase();
|
||||
case 'sentence case':
|
||||
return sentenceCase(text);
|
||||
default:
|
||||
return titleCase(text);
|
||||
}
|
||||
}
|
||||
|
||||
export function sentenceCase(text: string): string {
|
||||
let sentences: string[] = text.match(/[^.!?]*[.!?]*/g) || [];
|
||||
|
||||
let casedSentences: string[] = sentences.map(sentence => {
|
||||
sentence = sentence.trim();
|
||||
if (sentence.length === 0) return '';
|
||||
return sentence.charAt(0).toUpperCase() + sentence.slice(1).toLowerCase();
|
||||
});
|
||||
|
||||
return casedSentences.join(' ');
|
||||
}
|
||||
|
||||
export function titleCase(text: string): string {
|
||||
if (!text) return '';
|
||||
|
||||
let words: string[] = text.split(/(\s+)/).filter(word => word.trim().length > 0);
|
||||
|
||||
let titleCasedWords: string[] = words.map(word => {
|
||||
if (word.length === 0) return '';
|
||||
|
||||
const firstChar = word.charAt(0);
|
||||
const restOfWord = word.slice(1).toLowerCase();
|
||||
|
||||
// Capitalize if the first character is a letter
|
||||
if (/[a-zA-Z]/.test(firstChar)) {
|
||||
return firstChar.toUpperCase() + restOfWord;
|
||||
}
|
||||
|
||||
return word;
|
||||
});
|
||||
|
||||
return titleCasedWords.join(' ');
|
||||
}
|
||||
|
||||
import { instanceStore } from './store';
|
||||
|
||||
export function truncate(value: string, maxLength: number = 50): string {
|
||||
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value;
|
||||
}
|
||||
|
||||
export function numberWithCommas(number: number) {
|
||||
if (typeof number === 'undefined') return;
|
||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
export function cleanNumber(number: number): string {
|
||||
return humanNumber(number, (number: number) =>
|
||||
Number.parseFloat(number.toString()).toFixed(1)
|
||||
).replace('.0', '');
|
||||
}
|
||||
|
||||
export function videoLength(lengthSeconds: number): string {
|
||||
const hours = Math.floor(lengthSeconds / 3600);
|
||||
let minutes: number | string = Math.floor((lengthSeconds % 3600) / 60);
|
||||
let seconds: number | string = Math.round(lengthSeconds % 60);
|
||||
|
||||
if (minutes < 10) {
|
||||
minutes = `0${minutes}`;
|
||||
}
|
||||
|
||||
if (seconds < 10) {
|
||||
seconds = `0${seconds}`;
|
||||
}
|
||||
|
||||
if (hours !== 0) {
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
} else {
|
||||
return `${minutes}:${seconds}`;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PhasedDescription {
|
||||
description: string;
|
||||
timestamps: { title: string; time: number; timePretty: string; }[];
|
||||
}
|
||||
|
||||
export function padTime(time: string): string {
|
||||
let timeParts = time.split(':');
|
||||
|
||||
if (timeParts.length < 3) {
|
||||
timeParts = ('00:' + time).split(':');
|
||||
}
|
||||
|
||||
const hours = (timeParts[0] || '0').padStart(2, '0');
|
||||
const minutes = (timeParts[1] || '0').padStart(2, '0');
|
||||
|
||||
let seconds = timeParts[2] || '0';
|
||||
let milliseconds = '';
|
||||
|
||||
if (seconds.includes('.')) {
|
||||
const [sec, ms] = seconds.split('.');
|
||||
seconds = sec.padStart(2, '0');
|
||||
milliseconds = `.${ms.padStart(3, '0')}`;
|
||||
} else {
|
||||
seconds = seconds.padStart(2, '0');
|
||||
}
|
||||
|
||||
return `${hours}:${minutes}:${seconds}${milliseconds}`;
|
||||
}
|
||||
|
||||
export function decodeHtmlCharCodes(str: string): string {
|
||||
const { decode } = he;
|
||||
return decode(str);
|
||||
}
|
||||
|
||||
export function phaseDescription(content: string, usingYoutubeJs: boolean = false): 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>/;
|
||||
|
||||
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);
|
||||
|
||||
if (urlMatch !== null && timestampMatch === null) {
|
||||
// If line contains a URL but not a timestamp, modify the URL
|
||||
const modifiedLine = line.replace(
|
||||
/<a href="([^"]+)"/,
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer" class="link"'
|
||||
);
|
||||
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] || '';
|
||||
timestamps.push({
|
||||
time: convertToSeconds(time),
|
||||
// Remove any HTML in the timestamp title.
|
||||
title: decodeHtmlCharCodes(title.replace(/<[^>]+>/g, '').replace(/\n/g, '').trim()),
|
||||
timePretty: timestamp
|
||||
});
|
||||
} else {
|
||||
filteredLines.push(line);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredContent = filteredLines.join('\n');
|
||||
|
||||
return { description: filteredContent, timestamps: timestamps };
|
||||
}
|
||||
|
||||
|
||||
function convertToSeconds(time: string): number {
|
||||
const parts = time.split(':').map((part) => parseInt(part));
|
||||
let seconds = 0;
|
||||
if (parts.length === 3) {
|
||||
// hh:mm:ss
|
||||
seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
} else if (parts.length === 2) {
|
||||
// hh:ss or m:ss
|
||||
seconds = parts[0] * 60 + parts[1];
|
||||
} else if (parts.length === 1) {
|
||||
// s
|
||||
seconds = parts[0];
|
||||
}
|
||||
return seconds;
|
||||
}
|
||||
|
||||
export function humanizeSeconds(totalSeconds: number): string {
|
||||
const secondsInMinute = 60;
|
||||
const secondsInHour = 3600;
|
||||
|
||||
const hours = Math.floor(totalSeconds / secondsInHour);
|
||||
const minutes = Math.floor((totalSeconds % secondsInHour) / secondsInMinute);
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} hour${hours > 1 ? 's' : ''}`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} minute${minutes > 1 ? 's' : ''}`);
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
export function proxyVideoUrl(source: string): string {
|
||||
const rawSrc = new URL(source);
|
||||
rawSrc.host = get(instanceStore).replace('http://', '').replace(
|
||||
@@ -238,18 +42,6 @@ export function pullBitratePreference(): number {
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function proxyGoogleImage(source: string): string {
|
||||
if (source.startsWith('//')) source = `https:${source}`;
|
||||
|
||||
let path: string | undefined;
|
||||
try {
|
||||
path = new URL(source).pathname;
|
||||
} catch { }
|
||||
|
||||
if (typeof path === 'undefined') return '';
|
||||
|
||||
return `${get(instanceStore)}/ggpht${path}`;
|
||||
}
|
||||
|
||||
export function unsafeRandomItem(array: any[]): any {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
@@ -283,30 +75,6 @@ export async function peerJs(peerId: string): Promise<Peer> {
|
||||
);
|
||||
}
|
||||
|
||||
export function getBestThumbnail(
|
||||
images: Image[] | null,
|
||||
maxWidthDimension: number = 480,
|
||||
maxHeightDimension = 360
|
||||
): string {
|
||||
if (images && images.length > 0) {
|
||||
const imagesFiltered = images.filter(
|
||||
(image) => image.width < maxWidthDimension && image.height < maxHeightDimension
|
||||
);
|
||||
|
||||
if (imagesFiltered.length === 0) {
|
||||
return images[0].url;
|
||||
}
|
||||
|
||||
imagesFiltered.sort((a, b) => {
|
||||
return b.width * b.height - a.width * a.height;
|
||||
});
|
||||
|
||||
return imagesFiltered[0].url;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureNoTrailingSlash(url: any): string {
|
||||
if (typeof url !== 'string') return '';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdaptiveFormats, Captions, Image, StoryBoard, Thumbnail, VideoBase, VideoPlay } from '$lib/api/model';
|
||||
import { numberWithCommas } from '$lib/misc';
|
||||
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
|
||||
import { numberWithCommas } from '$lib/time';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { BG } from 'bgutils-js';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
@@ -4,7 +4,7 @@ import type Peer from 'peerjs';
|
||||
import type { DataConnection } from 'peerjs';
|
||||
import { persisted } from 'svelte-persisted-store';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import type { TitleCase } from './misc';
|
||||
import type { TitleCase } from './letterCasing';
|
||||
import type { PoTokens } from './patches/youtubejs';
|
||||
|
||||
function platformDependentDefault(givenValue: any, defaultValue: any): any {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import humanNumber from 'human-number';
|
||||
|
||||
export function numberWithCommas(number: number) {
|
||||
if (typeof number === 'undefined') return;
|
||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
export function cleanNumber(number: number): string {
|
||||
return humanNumber(number, (number: number) =>
|
||||
Number.parseFloat(number.toString()).toFixed(1)
|
||||
).replace('.0', '');
|
||||
}
|
||||
|
||||
|
||||
export function videoLength(lengthSeconds: number): string {
|
||||
const hours = Math.floor(lengthSeconds / 3600);
|
||||
let minutes: number | string = Math.floor((lengthSeconds % 3600) / 60);
|
||||
let seconds: number | string = Math.round(lengthSeconds % 60);
|
||||
|
||||
if (minutes < 10) {
|
||||
minutes = `0${minutes}`;
|
||||
}
|
||||
|
||||
if (seconds < 10) {
|
||||
seconds = `0${seconds}`;
|
||||
}
|
||||
|
||||
if (hours !== 0) {
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
} else {
|
||||
return `${minutes}:${seconds}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function padTime(time: string): string {
|
||||
let timeParts = time.split(':');
|
||||
|
||||
if (timeParts.length < 3) {
|
||||
timeParts = ('00:' + time).split(':');
|
||||
}
|
||||
|
||||
const hours = (timeParts[0] || '0').padStart(2, '0');
|
||||
const minutes = (timeParts[1] || '0').padStart(2, '0');
|
||||
|
||||
let seconds = timeParts[2] || '0';
|
||||
let milliseconds = '';
|
||||
|
||||
if (seconds.includes('.')) {
|
||||
const [sec, ms] = seconds.split('.');
|
||||
seconds = sec.padStart(2, '0');
|
||||
milliseconds = `.${ms.padStart(3, '0')}`;
|
||||
} else {
|
||||
seconds = seconds.padStart(2, '0');
|
||||
}
|
||||
|
||||
return `${hours}:${minutes}:${seconds}${milliseconds}`;
|
||||
}
|
||||
|
||||
export function convertToSeconds(time: string): number {
|
||||
const parts = time.split(':').map((part) => parseInt(part));
|
||||
let seconds = 0;
|
||||
if (parts.length === 3) {
|
||||
// hh:mm:ss
|
||||
seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
} else if (parts.length === 2) {
|
||||
// hh:ss or m:ss
|
||||
seconds = parts[0] * 60 + parts[1];
|
||||
} else if (parts.length === 1) {
|
||||
// s
|
||||
seconds = parts[0];
|
||||
}
|
||||
return seconds;
|
||||
}
|
||||
|
||||
export function humanizeSeconds(totalSeconds: number): string {
|
||||
const secondsInMinute = 60;
|
||||
const secondsInHour = 3600;
|
||||
|
||||
const hours = Math.floor(totalSeconds / secondsInHour);
|
||||
const minutes = Math.floor((totalSeconds % secondsInHour) / secondsInMinute);
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} hour${hours > 1 ? 's' : ''}`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} minute${minutes > 1 ? 's' : ''}`);
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { type PhasedDescription, decodeHtmlCharCodes } from "./misc";
|
||||
import { convertToSeconds } from "./time";
|
||||
|
||||
export function phaseDescription(content: string, usingYoutubeJs: boolean = false): 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>/;
|
||||
|
||||
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);
|
||||
|
||||
if (urlMatch !== null && timestampMatch === null) {
|
||||
// If line contains a URL but not a timestamp, modify the URL
|
||||
const modifiedLine = line.replace(
|
||||
/<a href="([^"]+)"/,
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer" class="link"'
|
||||
);
|
||||
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] || '';
|
||||
timestamps.push({
|
||||
time: convertToSeconds(time),
|
||||
// Remove any HTML in the timestamp title.
|
||||
title: decodeHtmlCharCodes(title.replace(/<[^>]+>/g, '').replace(/\n/g, '').trim()),
|
||||
timePretty: timestamp
|
||||
});
|
||||
} else {
|
||||
filteredLines.push(line);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredContent = filteredLines.join('\n');
|
||||
|
||||
return { description: filteredContent, timestamps: timestamps };
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
import PageLoading from '$lib/components/PageLoading.svelte';
|
||||
import PlaylistThumbnail from '$lib/components/PlaylistThumbnail.svelte';
|
||||
import VideoList from '$lib/components/VideoList.svelte';
|
||||
import { cleanNumber, getBestThumbnail, proxyGoogleImage } from '$lib/misc';
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
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';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getHashtag } from '$lib/api/index.js';
|
||||
import { getHashtag } from '$lib/api';
|
||||
import VideoList from '$lib/components/VideoList.svelte';
|
||||
import { activePageStore } from '$lib/store.js';
|
||||
import { activePageStore } from '$lib/store';
|
||||
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { getPlaylist } from '$lib/api/index';
|
||||
import type { PlaylistPageVideo } from '$lib/api/model.js';
|
||||
import type { PlaylistPageVideo } from '$lib/api/model';
|
||||
import VideoList from '$lib/components/VideoList.svelte';
|
||||
import { cleanNumber, unsafeRandomItem } from '$lib/misc.js';
|
||||
import { unsafeRandomItem } from '$lib/misc';
|
||||
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';
|
||||
|
||||
@@ -9,22 +9,15 @@
|
||||
postSubscribe,
|
||||
removePlaylistVideo
|
||||
} from '$lib/api/index';
|
||||
import type { Comments, PlaylistPage, PlaylistPageVideo } from '$lib/api/model.js';
|
||||
import type { Comments, PlaylistPage, PlaylistPageVideo } from '$lib/api/model';
|
||||
import Comment from '$lib/components/Comment.svelte';
|
||||
import Player from '$lib/components/Player.svelte';
|
||||
import ShareVideo from '$lib/components/ShareVideo.svelte';
|
||||
import Thumbnail from '$lib/components/Thumbnail.svelte';
|
||||
import Transcript from '$lib/components/Transcript.svelte';
|
||||
import {
|
||||
cleanNumber,
|
||||
getBestThumbnail,
|
||||
humanizeSeconds,
|
||||
letterCase,
|
||||
numberWithCommas,
|
||||
proxyGoogleImage,
|
||||
truncate,
|
||||
unsafeRandomItem
|
||||
} from '$lib/misc';
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { letterCase } from '$lib/letterCasing';
|
||||
import { truncate, unsafeRandomItem } from '$lib/misc';
|
||||
import type { PlayerEvents } from '$lib/player';
|
||||
import {
|
||||
activePageStore,
|
||||
@@ -41,6 +34,7 @@
|
||||
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';
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
getVideo,
|
||||
postHistory
|
||||
} from '$lib/api/index';
|
||||
import { phaseDescription } from '$lib/misc';
|
||||
import {
|
||||
authStore,
|
||||
playerProxyVideosStore,
|
||||
returnYTDislikesInstanceStore,
|
||||
returnYtDislikesStore
|
||||
} from '$lib/store';
|
||||
import { phaseDescription } from '$lib/timestamps';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getVideo } from '$lib/api/index';
|
||||
import type { PhasedDescription } from '$lib/misc';
|
||||
import { playerProxyVideosStore } from '$lib/store';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
import type { PhasedDescription } from '../../../../lib/misc.js';
|
||||
import { playerProxyVideosStore } from '../../../../lib/store.js';
|
||||
|
||||
export async function load({ params }) {
|
||||
let video;
|
||||
|
||||
Reference in New Issue
Block a user