diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index d885f88f..173f01de 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 204 - versionName "1.14.0" + versionCode 205 + versionName "1.14.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index 835956c6..281bc578 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -3,7 +3,7 @@ us.materialio.Materialious CC0-1.0 Materialious - Materialious is a privacy respecting frontend for YouTube built ontop of Invidious. + Materialious is a modern material design frontend for YouTube & Invidious, focused on a clean, privacy-friendly YouTube experience.

Materialious is a client for Invidious using material design. It lets you browse and watch videos, manage playlists and visit channels.

Features:

@@ -48,7 +48,7 @@ Watching a video - https://raw.githubusercontent.com/Materialious/Materialious/4baa7b897a46d4e71aaca7d322e4f7dafc870a33/previews/player-preview.png + https://raw.githubusercontent.com/Materialious/Materialious/43e6eb78edae030ffd372af5b9a8dcf92564be46/previews/player-preview.png Modifying settings @@ -69,41 +69,10 @@ intense - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + https://github.com/Materialious/Materialious/releases/tag/1.14.1 + + https://github.com/Materialious/Materialious/releases/tag/1.14.0 diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index b5175bc0..a70604ec 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.14.0", + "version": "1.14.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.14.0", + "version": "1.14.1", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 6fbd8259..509d2480 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.14.0", + "version": "1.14.1", "description": "Modern material design for YouTube and Invidious.", "author": { "name": "Ward Pearce", @@ -45,4 +45,4 @@ "capacitor", "electron" ] -} +} \ No newline at end of file diff --git a/materialious/package.json b/materialious/package.json index f34cd510..b154e86c 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.14.0", + "version": "1.14.1", "private": true, "scripts": { "dev": "npm run patch:github && vite dev", diff --git a/materialious/src/lib/api/index.ts b/materialious/src/lib/api/index.ts index b60b9481..bc920a6c 100644 --- a/materialious/src/lib/api/index.ts +++ b/materialious/src/lib/api/index.ts @@ -252,7 +252,7 @@ export async function getFeed( fetchOptions: RequestInit = {} ): Promise { if (isYTBackend()) { - return getFeedYTjs(maxResults, page); + return getFeedYTjs(); } const path = buildPath('auth/feed'); diff --git a/materialious/src/lib/api/youtubejs/subscriptions.ts b/materialious/src/lib/api/youtubejs/subscriptions.ts index 66d513ed..ffb1f783 100644 --- a/materialious/src/lib/api/youtubejs/subscriptions.ts +++ b/materialious/src/lib/api/youtubejs/subscriptions.ts @@ -128,9 +128,7 @@ export async function parseChannelRSS(channelId: string): Promise { } } -// Ignored to match non ytjs version of getfeed. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export async function getFeedYTjs(maxResults: number, page: number): Promise { +export async function getFeedYTjs(): Promise { const channelSubscriptions = await localDb.channelSubscriptions.toArray(); const toUpdatePromises: Promise[] = []; diff --git a/materialious/src/lib/api/youtubejs/video.ts b/materialious/src/lib/api/youtubejs/video.ts index 992b5a82..548577e1 100644 --- a/materialious/src/lib/api/youtubejs/video.ts +++ b/materialious/src/lib/api/youtubejs/video.ts @@ -15,7 +15,6 @@ import { get } from 'svelte/store'; import type { Types } from 'youtubei.js'; import { Utils, YT, YTNodes, Platform } from 'youtubei.js'; import { getInnertube } from '.'; -import { cleanNumber, extractNumber } from '$lib/numbers'; Platform.shim.eval = async ( data: Types.BuildScriptResult, @@ -174,33 +173,31 @@ export async function getVideoYTjs(videoId: string): Promise { ) return; - const durationOverlay = recommended.content_image.overlays - ?.find( - (overlay) => - overlay.is(YTNodes.ThumbnailOverlayBadgeView) && - overlay.position === 'THUMBNAIL_OVERLAY_BADGE_POSITION_BOTTOM_END' - ) - ?.as(YTNodes.ThumbnailOverlayBadgeView); + let lengthSeconds: number = 0; + recommended.content_image.overlays.forEach((overlay) => { + if (overlay.is(YTNodes.ThumbnailBottomOverlayView)) { + overlay.badges.forEach((badge) => { + if ( + badge.is(YTNodes.ThumbnailBadgeView) && + badge.badge_style === 'THUMBNAIL_OVERLAY_BADGE_STYLE_DEFAULT' + ) { + lengthSeconds = convertToSeconds(badge.text); + } + }); + } + }); + + const viewCountText = ( + recommended.metadata.metadata.metadata_rows[1]?.metadata_parts?.[0]?.text?.text ?? '' + ).split(' ')[0]; recommendedVideos.push({ videoThumbnails: (recommended?.content_image.image as Thumbnail[]) || [], videoId: recommended.content_id, title: recommended.metadata.title.toString(), - viewCountText: cleanNumber( - extractNumber( - ( - recommended.metadata.metadata.metadata_rows[1]?.metadata_parts?.[0]?.text ?? '' - ).toString() - ) - ), - author: - ( - recommended.metadata.metadata.metadata_rows[0]?.metadata_parts?.[0]?.text ?? '' - ).toString() || '', - - lengthSeconds: durationOverlay?.badges[0].text - ? convertToSeconds(durationOverlay?.badges[0].text) - : 0, + viewCountText, + author: recommended.metadata.metadata.metadata_rows[0]?.metadata_parts?.[0]?.text?.text ?? '', + lengthSeconds, authorId: recommended.metadata?.image?.renderer_context?.command_context?.on_tap?.payload?.browseId || '', diff --git a/materialious/src/lib/components/ItemsList.svelte b/materialious/src/lib/components/ItemsList.svelte index 9addbf77..a6372b00 100644 --- a/materialious/src/lib/components/ItemsList.svelte +++ b/materialious/src/lib/components/ItemsList.svelte @@ -2,28 +2,17 @@ import Thumbnail from '$lib/components/Thumbnail.svelte'; import { _ } from '$lib/i18n'; import { removePlaylistVideo } from '../api'; - import type { - Channel, - HashTag, - Playlist, - PlaylistPage, - PlaylistPageVideo, - Video, - VideoBase - } from '../api/model'; import { authStore, feedLastItemId, isAndroidTvStore } from '../store'; import ContentColumn from './ContentColumn.svelte'; import { onMount, onDestroy, tick } from 'svelte'; import Mousetrap from 'mousetrap'; - import { extractUniqueId } from '$lib/misc'; + import { extractUniqueId, type feedItems } from '$lib/misc'; import ChannelThumbnail from './ChannelThumbnail.svelte'; import PlaylistThumbnail from './PlaylistThumbnail.svelte'; import HashtagThumbnail from './HashtagThumbnail.svelte'; interface Props { - items?: - | (VideoBase | Video | PlaylistPageVideo | Channel | Playlist | HashTag)[] - | PlaylistPage[]; + items?: feedItems; playlistId?: string; playlistAuthor?: string; classes?: string; diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index 5293bf73..3146aba0 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -2,7 +2,7 @@ import { page } from '$app/stores'; import { getBestThumbnail, ImageCache } from '$lib/images'; import { videoLength } from '$lib/numbers'; - import { generateChapterWebVTT, type PhasedDescription, type Timestamp } from '$lib/description'; + import { generateChapterWebVTT, type ParsedDescription, type Timestamp } from '$lib/description'; import { SystemBars, SystemBarsStyle, SystemBarType } from '@capacitor/core'; import { Capacitor } from '@capacitor/core'; import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation'; @@ -67,7 +67,7 @@ } from '$lib/timelineThumbnails'; interface Props { - data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null }; + data: { video: VideoPlay; content: ParsedDescription; playlistId: string | null }; currentTime?: number; userManualSeeking?: boolean; isEmbed?: boolean; diff --git a/materialious/src/lib/components/Thumbnail.svelte b/materialious/src/lib/components/Thumbnail.svelte index ee5767c0..25d0ee35 100644 --- a/materialious/src/lib/components/Thumbnail.svelte +++ b/materialious/src/lib/components/Thumbnail.svelte @@ -8,7 +8,7 @@ import { get } from 'svelte/store'; import { getDeArrow, getThumbnail } from '../api'; import type { Notification, PlaylistPageVideo, Video, VideoBase } from '../api/model'; - import { createVideoUrl, insecureRequestImageHandler } from '../misc'; + import { createVideoUrl, insecureRequestImageHandler, isYTBackend } from '../misc'; import type { PlayerEvents } from '../player'; import { authStore, @@ -248,10 +248,9 @@ {#if 'published' in video}
- {video.viewCountText ?? cleanNumber(video.viewCount ?? 0)} • {relativeTimestamp( - video.published, - false - )} + {video.viewCountText ?? cleanNumber(video.viewCount ?? 0)} + • + {isYTBackend() ? relativeTimestamp(video.published, false) : video.publishedText}
{/if} @@ -270,10 +269,10 @@ width: 100%; height: 100%; object-fit: cover; - clip-path: inset(30px 0 30px 0); + clip-path: inset(10% 0 10% 0); display: block; - transform: translateY(-30px); - margin-bottom: -60px; + transform: translateY(-15%); + margin-bottom: -20%; } .thumbnail { diff --git a/materialious/src/lib/components/settings/Settings.svelte b/materialious/src/lib/components/settings/Settings.svelte index 4dac635e..f8bd0a78 100644 --- a/materialious/src/lib/components/settings/Settings.svelte +++ b/materialious/src/lib/components/settings/Settings.svelte @@ -133,8 +133,10 @@ {#each tabs as tab, index (tab)} ([^<]+)<\/a>\s*(.+)/g; - const replacement = `$3 $4`; + const links = doc.querySelectorAll('a'); - const processedHtml = html.replace(regex, replacement); + links.forEach((link) => { + const href = link.getAttribute('href'); + if (!href) return; - return processedHtml; + const realHref = extractActualLink(href); + const dataOnClick = link.getAttribute('data-onclick'); + + link.classList.add('link'); + + if (dataOnClick === 'jump_to_time' || (realHref && realHref.includes('t='))) { + const match = realHref?.match(/t=(\d+)/); + const timestamp = match ? match[1] : ''; + + const newHref = resolve(`/watch/[videoId]?time=${timestamp}`, { videoId }); + + link.setAttribute('href', newHref); + link.removeAttribute('data-onclick'); + link.setAttribute('data-sveltekit-preload-data', 'off'); + } else { + link.setAttribute('href', realHref); + link.setAttribute('target', '_blank'); + link.setAttribute('referrerpolicy', 'no-referrer'); + } + }); + + return doc.documentElement.outerHTML; } let userPfp = $state(''); @@ -77,7 +101,7 @@

- {@html commentTimestamps(comment.contentHtml)} + {@html parseComment(comment.contentHtml)}

diff --git a/materialious/src/lib/description.ts b/materialious/src/lib/description.ts index c49beac2..7da9866b 100644 --- a/materialious/src/lib/description.ts +++ b/materialious/src/lib/description.ts @@ -5,7 +5,7 @@ import { convertToSeconds, padTime } from './time'; export type Timestamp = { title: string; time: number; timePretty: string; endTime: number }; export type Timestamps = Timestamp[]; -export interface PhasedDescription { +export interface ParsedDescription { description: string; timestamps: Timestamps; } @@ -41,11 +41,11 @@ function cleanTimestampTitle(title: string): string { return title.replace(/^[\s\-•|:/\\*#>~]+/, '').trim(); } -export function phaseDescription( +export function parseDescription( videoId: string, content: string, fallbackPatch?: 'youtubejs' | 'piped' -): PhasedDescription { +): ParsedDescription { const timestamps: Timestamps = []; const lines = content.split('\n'); const filteredLines: string[] = []; diff --git a/materialious/src/lib/search.ts b/materialious/src/lib/search.ts index 3b32cabe..a655103a 100644 --- a/materialious/src/lib/search.ts +++ b/materialious/src/lib/search.ts @@ -6,7 +6,17 @@ import { isVideoID } from './misc'; function extractVideoId(url: string): string | null { const urlObj = new URL(url, 'http://example.com'); // Using a base URL in case searchValue is just a query parameter - const videoId = urlObj.searchParams.get('v'); + + let videoId: string | null = null; + if (urlObj.hostname === 'youtu.be') { + videoId = urlObj.pathname.replace('/', ''); + if (videoId === '') { + videoId = null; + } + } else { + videoId = urlObj.searchParams.get('v'); + } + return videoId; } diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index f63772eb..feb5fe7c 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -21,7 +21,7 @@ import type { VideoBase, VideoPlay } from './api/model'; -import type { PhasedDescription } from './description'; +import type { ParsedDescription } from './description'; import { ensureNoTrailingSlash } from './misc'; import type { EngineFallback } from './api/misc'; @@ -182,7 +182,7 @@ export const playerAndroidPauseOnNetworkChange = persist( export const playerPlaylistHistory: Writable = writable([]); export interface PlayerState { - data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null }; + data: { video: VideoPlay; content: ParsedDescription; playlistId: string | null }; playerElement?: HTMLMediaElement | undefined; } diff --git a/materialious/src/lib/watch.ts b/materialious/src/lib/watch.ts index 95a61285..661614ac 100644 --- a/materialious/src/lib/watch.ts +++ b/materialious/src/lib/watch.ts @@ -13,7 +13,7 @@ import { returnYTDislikesInstanceStore, returnYtDislikesStore } from '$lib/store'; -import { phaseDescription } from '$lib/description'; +import { parseDescription } from '$lib/description'; import { error } from '@sveltejs/kit'; import { get } from 'svelte/store'; import { _ } from './i18n'; @@ -71,7 +71,7 @@ export async function getWatchDetails(videoId: string, url: URL) { return { video: video, - content: phaseDescription(video.videoId, video.descriptionHtml, video.fallbackPatch), + content: parseDescription(video.videoId, video.descriptionHtml, video.fallbackPatch), playlistId: playlistId, streamed: { personalPlaylists: personalPlaylists, diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 96e11cac..b909b450 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -449,7 +449,9 @@ {$_('transcript')}
-