@@ -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).
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<a href="https://github.com/Materialious/Materialious/releases/latest">
|
||||
<img src="https://raw.githubusercontent.com/NeoApplications/Neo-Backup/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub" height="80"/>
|
||||
</a>
|
||||
<a href="https://snapcraft.io/materialious">
|
||||
<img height="60" alt="Get it from the Snap Store" src="https://snapcraft.io/en/dark/install.svg" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
# 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.
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<a href="https://github.com/Materialious/Materialious/releases/latest">
|
||||
<img src="https://raw.githubusercontent.com/NeoApplications/Neo-Backup/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub" height="80"/>
|
||||
</a>
|
||||
<a href="http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Materialious/Materialious">
|
||||
<img height="60" alt="Get it on Obtainium" src="https://raw.githubusercontent.com/ImranR98/Obtainium/e3fcf6e0b5187445a76462b4042aba6b2fc15047/assets/graphics/badge_obtainium.png" />
|
||||
</a>
|
||||
<a href="https://f-droid.org/packages/us.materialio.app/">
|
||||
<img height="80" alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" />
|
||||
</a>
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/us.materialio.app">
|
||||
<img height="80" alt="Get it on Izzysoft" src="https://codeberg.org/IzzyOnDroid/repo/raw/commit/9873f08e282332a231e64d9729f810f427a521e4/assets/IzzyOnDroid.png" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
# Previews
|
||||
|
||||
|
||||
+1
-5
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
+15
-82
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.6.20",
|
||||
"version": "1.6.21",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
Generated
+6
-5
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.6.20",
|
||||
"version": "1.6.21",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
|
||||
function commentTimestamps(html: string): string {
|
||||
const regex =
|
||||
/<a href="([^"]+)" data-onclick="jump_to_time" data-jump-time="(\d+)">(\d+:\d+(?::\d+)?)<\/a>\s*(.+)/;
|
||||
const replacement = `<a href="/watch/${videoId}?time=$2" data-sveltekit-preload-data="off" class="link">$3</a>`;
|
||||
/<a href="([^"]+)" data-onclick="jump_to_time" data-jump-time="(\d+)">([^<]+)<\/a>\s*(.+)/g;
|
||||
const replacement = `<a href="/watch/${videoId}?time=$2" data-sveltekit-preload-data="off" class="link">$3 $4</a>`;
|
||||
|
||||
const processedHtml = html.replace(regex, replacement);
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
.comment {
|
||||
display: flex;
|
||||
margin-bottom: 0.8em;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.channel-owner {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import type { HashTag } from './api/model';
|
||||
import { cleanNumber, truncate } from './misc';
|
||||
|
||||
export let hashtag: HashTag;
|
||||
|
||||
let channelPfp: HTMLImageElement | undefined;
|
||||
</script>
|
||||
|
||||
<a href={hashtag.url} class="wave" style="min-width: 100%;min-height: 100%;">
|
||||
<div class="padding">
|
||||
<h5 class="center-align">{truncate(hashtag.title, 14)}</h5>
|
||||
<h6 style="margin-top: 0;" class="center-align grey-text medium-text">
|
||||
{cleanNumber(hashtag.channelCount)}
|
||||
{$_('channels')}
|
||||
</h6>
|
||||
<h6 style="margin-top: 0;" class="center-align grey-text medium-text">
|
||||
{cleanNumber(hashtag.videoCount)}
|
||||
{$_('videos')}
|
||||
</h6>
|
||||
</div>
|
||||
</a>
|
||||
@@ -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 @@
|
||||
<option selected={$interfaceForceCase === null} value={null}>Default</option>
|
||||
{#each titleCases as caseType}
|
||||
<option selected={$interfaceForceCase === caseType} value={caseType}
|
||||
>{titleCase(`${caseType}`)}</option
|
||||
>{letterCase(`${caseType}`, caseType)}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<Video[]> {
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function getResolveUrl(url: string): Promise<ResolvedUrl> {
|
||||
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<VideoPlay> {
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(' ');
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { getHashtag } from '$lib/api/index.js';
|
||||
import { activePageStore } from '$lib/store.js';
|
||||
import VideoList from '$lib/VideoList.svelte';
|
||||
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
|
||||
|
||||
export let data;
|
||||
|
||||
let currentPage = 1;
|
||||
$: videos = [...data.hashTagVideos.results];
|
||||
|
||||
activePageStore.set('hashtag');
|
||||
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
currentPage++;
|
||||
const hashtagVideos = (await getHashtag(data.hashtag, currentPage)).results;
|
||||
if (hashtagVideos.length === 0) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
videos = [...videos, ...hashtagVideos];
|
||||
event.detail.loaded();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<VideoList {videos} />
|
||||
|
||||
<InfiniteLoading on:infinite={loadMore} />
|
||||
@@ -0,0 +1,8 @@
|
||||
import { getHashtag } from '$lib/api';
|
||||
|
||||
export async function load({ params }) {
|
||||
return {
|
||||
hashtag: params.slug,
|
||||
hashTagVideos: await getHashtag(params.slug)
|
||||
};
|
||||
}
|
||||
@@ -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 @@
|
||||
<ChannelThumbnail channel={item} />
|
||||
{:else if item.type === 'playlist'}
|
||||
<PlaylistThumbnail playlist={item} />
|
||||
{:else if item.type === 'hashtag'}
|
||||
<HashtagThumbnail hashtag={item} />
|
||||
{/if}
|
||||
</article>
|
||||
{/key}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user