diff --git a/README.md b/README.md index 76ea2006..26d31d1a 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,30 @@ Open an issue to add your instance. Deploy Materialious for your Invidious instance using Docker. Follow the steps outlined [here](./docs/DOCKER.md) for easy setup. # Desktop (Windows/MacOS/Linux) -The latest release can be downloaded [here](https://github.com/Materialious/Materialious/releases/latest). +
+ + Get it on GitHub + + + Get it from the Snap Store + +
# Android -Due to Google's policies on custom YouTube frontends, Materialious isn't available on Google play. The best way to install is via the [release page](https://github.com/Materialious/Materialious/releases/latest) or through [F-Droid](https://f-droid.org/packages/us.materialio.app/). It is recommended to use [Obtainium](https://github.com/ImranR98/Obtainium) if you downloaded the app through the release page, to keep Materialious up to date. +
+ + Get it on GitHub + + + Get it on Obtainium + + + Get it on F-Droid + + + Get it on Izzysoft + +
# Previews diff --git a/docs/BUILDING.md b/docs/BUILDING.md index a0e3dbd9..e2f080f2 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -19,11 +19,7 @@ Materialious desktop builds are handled through GitHub using [prod-desktop.yml]( - From here, the process follows standard [electron-builder](https://www.electron.build/) steps to complete the build. ### Desktop Release (via Flatpak) -Flatpak builds for desktop releases are managed via [us.materailio.app](../flatpak/us.materialio.app.json). - -- We generate the necessary remote npm packages with `flatpak-node-generator npm -r ../materialious/package-lock.json -R ../materialious/electron/package-lock.json`, which creates [generated-sources.json](../flatpak/generated-sources.json). -- The Linux x64 runtime is downloaded as part of the Flatpak build sources and placed in `main/materialious/electron/.electron-cache`. The [electron-builder.config.json](../materialious/electron/electron-builder.config.json) is configured to use this cache directory to avoid downloading the runtime again during the build. -- The build process is similar to [prod-desktop.yml](../.github/workflows/prod-desktop.yml), following the guidelines for [Flatpak Electron](https://docs.flatpak.org/en/latest/electron.html). +TODO ### Android Release Android builds are handled using the workflow [prod-android.yml](../.github/workflows/prod-android.yml). diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 78aa8960..1902a748 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 71 - versionName "1.6.20" + versionCode 72 + versionName "1.6.21" 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/package-lock.json b/materialious/electron/package-lock.json index 8fd1984c..0ce2c2e3 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.6.18", + "version": "1.6.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.6.18", + "version": "1.6.21", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", @@ -1154,20 +1154,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/app-builder-lib/node_modules/builder-util-runtime": { - "version": "9.2.10", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", - "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -1527,9 +1513,10 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz", - "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==", + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "license": "MIT", "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -1551,20 +1538,6 @@ "node": ">= 14" } }, - "node_modules/builder-util/node_modules/builder-util-runtime": { - "version": "9.2.10", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", - "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/builder-util/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -2333,20 +2306,6 @@ "dmg-license": "^1.0.11" } }, - "node_modules/dmg-builder/node_modules/builder-util-runtime": { - "version": "9.2.10", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", - "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/dmg-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -2516,20 +2475,6 @@ "node": ">=12" } }, - "node_modules/electron-builder/node_modules/builder-util-runtime": { - "version": "9.2.10", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", - "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/electron-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -2578,20 +2523,6 @@ "@types/node": "*" } }, - "node_modules/electron-publish/node_modules/builder-util-runtime": { - "version": "9.2.10", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", - "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/electron-publish/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -2671,11 +2602,12 @@ } }, "node_modules/electron-updater": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.4.tgz", - "integrity": "sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.9.tgz", + "integrity": "sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==", + "license": "MIT", "dependencies": { - "builder-util-runtime": "9.2.5", + "builder-util-runtime": "9.2.10", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", @@ -5304,10 +5236,11 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/materialious/electron/package.json b/materialious/electron/package.json index f1c6c59c..5a807d17 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.6.20", + "version": "1.6.21", "description": "Modern material design for Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package-lock.json b/materialious/package-lock.json index a3b5ccb5..3085dc1c 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -1,12 +1,12 @@ { "name": "materialious", - "version": "1.6.18", + "version": "1.6.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "materialious", - "version": "1.6.18", + "version": "1.6.21", "dependencies": { "@capacitor-community/electron": "^5.0.1", "@capacitor/android": "6.1.2", @@ -3140,10 +3140,11 @@ } }, "node_modules/@sveltejs/adapter-auto": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.5.tgz", - "integrity": "sha512-27LR+uKccZ62lgq4N/hvyU2G+hTP9fxWEAfnZcl70HnyfAjMSsGk1z/SjAPXNCD1mVJIE7IFu3TQ8cQ/UH3c0A==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", + "integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==", "dev": true, + "license": "MIT", "dependencies": { "import-meta-resolve": "^4.1.0" }, diff --git a/materialious/package.json b/materialious/package.json index 1df19659..1c11eca1 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.6.20", + "version": "1.6.21", "private": true, "scripts": { "dev": "vite dev", diff --git a/materialious/src/lib/Comment.svelte b/materialious/src/lib/Comment.svelte index f1cdf954..3fbc02c1 100644 --- a/materialious/src/lib/Comment.svelte +++ b/materialious/src/lib/Comment.svelte @@ -22,8 +22,8 @@ function commentTimestamps(html: string): string { const regex = - /(\d+:\d+(?::\d+)?)<\/a>\s*(.+)/; - const replacement = `$3`; + /([^<]+)<\/a>\s*(.+)/g; + const replacement = `$3 $4`; const processedHtml = html.replace(regex, replacement); @@ -107,6 +107,7 @@ .comment { display: flex; margin-bottom: 0.8em; + white-space: pre-line; } .channel-owner { diff --git a/materialious/src/lib/HashtagThumbnail.svelte b/materialious/src/lib/HashtagThumbnail.svelte new file mode 100644 index 00000000..7a9cec19 --- /dev/null +++ b/materialious/src/lib/HashtagThumbnail.svelte @@ -0,0 +1,23 @@ + + + +
+
{truncate(hashtag.title, 14)}
+
+ {cleanNumber(hashtag.channelCount)} + {$_('channels')} +
+
+ {cleanNumber(hashtag.videoCount)} + {$_('videos')} +
+
+
diff --git a/materialious/src/lib/Settings.svelte b/materialious/src/lib/Settings.svelte index 6c02ccb3..0af63c00 100644 --- a/materialious/src/lib/Settings.svelte +++ b/materialious/src/lib/Settings.svelte @@ -8,7 +8,7 @@ import { onMount } from 'svelte'; import { _ } from 'svelte-i18n'; import { get } from 'svelte/store'; - import { ensureNoTrailingSlash, titleCase, titleCases } from './misc'; + import { ensureNoTrailingSlash, letterCase, titleCases } from './misc'; import { authStore, darkModeStore, @@ -343,7 +343,7 @@ {#each titleCases as caseType} {letterCase(`${caseType}`, caseType)} {/each} diff --git a/materialious/src/lib/Thumbnail.svelte b/materialious/src/lib/Thumbnail.svelte index cbca3b55..5bab9bcc 100644 --- a/materialious/src/lib/Thumbnail.svelte +++ b/materialious/src/lib/Thumbnail.svelte @@ -86,13 +86,15 @@ } async function loadAuthor() { - const channel = await getChannel(video.authorId, { priority: 'low' }); - const img = new Image(); - img.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails, 75, 75)); + try { + const channel = await getChannel(video.authorId, { priority: 'low' }); + const img = new Image(); + img.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails, 75, 75)); - img.onload = () => { - authorImg = img; - }; + img.onload = () => { + authorImg = img; + }; + } catch {} } onMount(async () => { diff --git a/materialious/src/lib/api/index.ts b/materialious/src/lib/api/index.ts index d0bbef1b..859da2b9 100644 --- a/materialious/src/lib/api/index.ts +++ b/materialious/src/lib/api/index.ts @@ -20,8 +20,10 @@ import type { Comments, DeArrow, Feed, + HashTag, Playlist, PlaylistPage, + ResolvedUrl, ReturnYTDislikes, SearchSuggestion, Subscription, @@ -71,6 +73,11 @@ export async function getPopular(fetchOptions?: RequestInit): Promise { return await resp.json(); } +export async function getResolveUrl(url: string): Promise { + const resp = await fetchErrorHandle(await fetch(`${buildPath('resolveurl')}?url=${url}`)); + return await resp.json(); +} + export async function getVideo(videoId: string, local: boolean = false, fetchOptions?: RequestInit): Promise { const resp = await fetch(setRegion(buildPath(`videos/${videoId}?local=${local}`)), fetchOptions); if ( @@ -144,6 +151,11 @@ export async function getSearchSuggestions(search: string, fetchOptions?: Reques return await resp.json(); } +export async function getHashtag(tag: string, page: number = 0): Promise<{ results: Video[]; }> { + const resp = await fetchErrorHandle(await fetch(buildPath(`hashtag/${tag}?page=${page}`))); + return await resp.json(); +} + export async function getSearch( search: string, options: { @@ -152,7 +164,7 @@ export async function getSearch( page?: string; }, fetchOptions?: RequestInit -): Promise<(Channel | Video | Playlist)[]> { +): Promise<(Channel | Video | Playlist | HashTag)[]> { if (typeof options.sort_by === 'undefined') { options.sort_by = 'relevance'; } diff --git a/materialious/src/lib/api/model.ts b/materialious/src/lib/api/model.ts index ffae475d..22053502 100644 --- a/materialious/src/lib/api/model.ts +++ b/materialious/src/lib/api/model.ts @@ -21,6 +21,12 @@ export interface VideoBase { viewCountText: string; } +export interface ResolvedUrl { + ucid: string; + params: string; + pageType: string; +} + export interface Video extends VideoBase { type: 'video'; title: string; @@ -221,6 +227,14 @@ export interface SearchSuggestion { suggestions: string[]; } +export interface HashTag { + channelCount: number; + title: string; + type: 'hashtag'; + url: string; + videoCount: number; +} + export interface Notification extends VideoBase { type: 'video' | 'shortVideo' | 'stream'; } diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index 23ecc371..0214cbea 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -17,6 +17,7 @@ "region": "Region", "noResult": "No results", "language": "Language", + "channels": "channels", "transcript": "Transcript", "noCaptionData": "Captions don't have any data.", "selectLang": "Select language", diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index 3ac979f0..b75ab48d 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -16,8 +16,8 @@ export const titleCases: TitleCase[] = [ 'sentence case' ]; -export function letterCase(text: string): string { - const casing = get(interfaceForceCase); +export function letterCase(text: string, caseTypeOverwrite?: TitleCase): string { + const casing = caseTypeOverwrite ? caseTypeOverwrite : get(interfaceForceCase); if (!casing) return text; switch (casing) { @@ -47,11 +47,20 @@ export function sentenceCase(text: string): string { export function titleCase(text: string): string { if (!text) return ''; - let words: string[] = text.split(/\s+/); + let words: string[] = text.split(/(\s+)/).filter(word => word.trim().length > 0); let titleCasedWords: string[] = words.map(word => { if (word.length === 0) return ''; - return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + + 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(' '); diff --git a/materialious/src/routes/(app)/hashtag/[slug]/+page.svelte b/materialious/src/routes/(app)/hashtag/[slug]/+page.svelte new file mode 100644 index 00000000..3d3bd511 --- /dev/null +++ b/materialious/src/routes/(app)/hashtag/[slug]/+page.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/materialious/src/routes/(app)/hashtag/[slug]/+page.ts b/materialious/src/routes/(app)/hashtag/[slug]/+page.ts new file mode 100644 index 00000000..8613d6a7 --- /dev/null +++ b/materialious/src/routes/(app)/hashtag/[slug]/+page.ts @@ -0,0 +1,8 @@ +import { getHashtag } from '$lib/api'; + +export async function load({ params }) { + return { + hashtag: params.slug, + hashTagVideos: await getHashtag(params.slug) + }; +} \ No newline at end of file diff --git a/materialious/src/routes/(app)/search/[slug]/+page.svelte b/materialious/src/routes/(app)/search/[slug]/+page.svelte index b7b29666..2eaec8cf 100644 --- a/materialious/src/routes/(app)/search/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/search/[slug]/+page.svelte @@ -2,6 +2,7 @@ import { getSearch } from '$lib/api'; import ChannelThumbnail from '$lib/ChannelThumbnail.svelte'; import ContentColumn from '$lib/ContentColumn.svelte'; + import HashtagThumbnail from '$lib/HashtagThumbnail.svelte'; import PageLoading from '$lib/PageLoading.svelte'; import PlaylistThumbnail from '$lib/PlaylistThumbnail.svelte'; import { activePageStore } from '$lib/store'; @@ -89,6 +90,8 @@ {:else if item.type === 'playlist'} + {:else if item.type === 'hashtag'} + {/if} {/key} diff --git a/materialious/src/routes/+layout.ts b/materialious/src/routes/+layout.ts index d7f14dac..2879758a 100644 --- a/materialious/src/routes/+layout.ts +++ b/materialious/src/routes/+layout.ts @@ -1,13 +1,25 @@ import { browser } from '$app/environment'; +import { goto } from '$app/navigation'; +import { getResolveUrl } from '$lib/api'; import '$lib/i18n'; // Import to initialize. Important :) import { locale, waitLocale } from 'svelte-i18n'; -import type { LayoutLoad } from './$types'; export let ssr = false; -export const load: LayoutLoad = async () => { +export async function load({ url }) { + if (url.pathname.startsWith('/@')) { + const username = url.pathname.split('/')[1]; + + try { + const resolvedUrl = await getResolveUrl(`www.youtube.com/${username}`); + if (resolvedUrl.pageType === 'WEB_PAGE_TYPE_CHANNEL') { + goto(`/channel/${resolvedUrl.ucid}`); + } + } catch { } + } + if (browser) { locale.set(window.navigator.language); } await waitLocale(); -}; +} diff --git a/update_versions.py b/update_versions.py index 057e781f..720674a3 100644 --- a/update_versions.py +++ b/update_versions.py @@ -5,7 +5,7 @@ import json import os import re -LATEST_VERSION = "1.6.20" +LATEST_VERSION = "1.6.21" WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious") ROOT_PACKAGE = os.path.join(WORKING_DIR, "package.json")