@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10
|
||||
versionName "1.0.10"
|
||||
versionCode 11
|
||||
versionName "1.0.11"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
Generated
+12
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "materialious",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor/android": "^6.0.0",
|
||||
@@ -17,6 +17,7 @@
|
||||
"beercss": "^3.5.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"human-number": "^2.0.4",
|
||||
"iso-3166": "^4.3.0",
|
||||
"material-dynamic-colors": "^1.1.0",
|
||||
"media-icons": "^0.10.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -7870,6 +7871,15 @@
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/iso-3166": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/iso-3166/-/iso-3166-4.3.0.tgz",
|
||||
"integrity": "sha512-H4kM/sVbxTjSl9xnQCYOrNWdpN0R8Uz26j1BuXI9E6U+kw5wmd3HyPgr/v4+NCuvV/NcvwTfZxd5XZ4lPKvBNA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -46,6 +46,7 @@
|
||||
"beercss": "^3.5.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"human-number": "^2.0.4",
|
||||
"iso-3166": "^4.3.0",
|
||||
"material-dynamic-colors": "^1.1.0",
|
||||
"media-icons": "^0.10.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -56,4 +57,4 @@
|
||||
"svelte-persisted-store": "^0.9.2",
|
||||
"vidstack": "^1.11.21"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
deArrowInstanceStore,
|
||||
deArrowThumbnailInstanceStore,
|
||||
instanceStore,
|
||||
interfaceRegionStore,
|
||||
returnYTDislikesInstanceStore,
|
||||
synciousInstanceStore
|
||||
} from '../store';
|
||||
@@ -25,8 +26,18 @@ import type {
|
||||
VideoPlay
|
||||
} from './model';
|
||||
|
||||
export function buildPath(path: string): string {
|
||||
return `${get(instanceStore)}/api/v1/${path}`;
|
||||
export function buildPath(path: string): URL {
|
||||
return new URL(`${get(instanceStore)}/api/v1/${path}`);
|
||||
}
|
||||
|
||||
export function setRegion(url: URL): URL {
|
||||
const region = get(interfaceRegionStore);
|
||||
|
||||
if (region) {
|
||||
url.searchParams.set('region', region);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export async function fetchErrorHandle(response: Response): Promise<Response> {
|
||||
@@ -47,7 +58,7 @@ export function buildAuthHeaders(): { headers: { Authorization: string; }; } {
|
||||
}
|
||||
|
||||
export async function getTrending(): Promise<Video[]> {
|
||||
const resp = await fetchErrorHandle(await fetch(buildPath('trending')));
|
||||
const resp = await fetchErrorHandle(await fetch(setRegion(buildPath('trending'))));
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
@@ -57,7 +68,7 @@ export async function getPopular(): Promise<Video[]> {
|
||||
}
|
||||
|
||||
export async function getVideo(videoId: string, local: boolean = false): Promise<VideoPlay> {
|
||||
const resp = await fetchErrorHandle(await fetch(buildPath(`videos/${videoId}?local=${local}`)));
|
||||
const resp = await fetchErrorHandle(await fetch(setRegion(buildPath(`videos/${videoId}?local=${local}`))));
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
@@ -84,7 +95,7 @@ export async function getComments(
|
||||
parameters.source = 'youtube';
|
||||
}
|
||||
|
||||
const path = new URL(buildPath(`comments/${videoId}`));
|
||||
const path = buildPath(`comments/${videoId}`);
|
||||
path.search = new URLSearchParams(parameters).toString();
|
||||
const resp = await fetchErrorHandle(await fetch(path));
|
||||
return await resp.json();
|
||||
@@ -104,7 +115,7 @@ export async function getChannelContent(
|
||||
): Promise<ChannelContentVideos | ChannelContentPlaylists> {
|
||||
if (typeof parameters.type === 'undefined') parameters.type = 'videos';
|
||||
|
||||
const url = new URL(buildPath(`channels/${channelId}/${parameters.type}`));
|
||||
const url = buildPath(`channels/${channelId}/${parameters.type}`);
|
||||
|
||||
if (typeof parameters.continuation !== 'undefined')
|
||||
url.searchParams.set('continuation', parameters.continuation);
|
||||
@@ -114,7 +125,7 @@ export async function getChannelContent(
|
||||
}
|
||||
|
||||
export async function getSearchSuggestions(search: string): Promise<SearchSuggestion> {
|
||||
const path = new URL(buildPath('search/suggestions'));
|
||||
const path = buildPath('search/suggestions');
|
||||
path.search = new URLSearchParams({ q: search }).toString();
|
||||
const resp = await fetchErrorHandle(await fetch(path));
|
||||
return await resp.json();
|
||||
@@ -140,14 +151,14 @@ export async function getSearch(
|
||||
options.page = '1';
|
||||
}
|
||||
|
||||
const path = new URL(buildPath('search'));
|
||||
const path = buildPath('search');
|
||||
path.search = new URLSearchParams({ ...options, q: search }).toString();
|
||||
const resp = await fetchErrorHandle(await fetch(path));
|
||||
const resp = await fetchErrorHandle(await fetch(setRegion(path)));
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export async function getFeed(maxResults: number, page: number): Promise<Feed> {
|
||||
const path = new URL(buildPath('auth/feed'));
|
||||
const path = buildPath('auth/feed');
|
||||
path.search = new URLSearchParams({
|
||||
max_results: maxResults.toString(),
|
||||
page: page.toString()
|
||||
|
||||
@@ -117,7 +117,7 @@ export interface ReturnYTDislikes {
|
||||
export interface Comment {
|
||||
author: string;
|
||||
authorThumbnails: Image[];
|
||||
authorID: string;
|
||||
authorId: string;
|
||||
authorUrl: string;
|
||||
isEdited: boolean;
|
||||
isPinned: boolean;
|
||||
|
||||
@@ -38,12 +38,14 @@
|
||||
/>
|
||||
<div>
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class:bold={true} class:channel-owner={comment.authorIsChannelOwner}
|
||||
>{comment.author}</span
|
||||
>
|
||||
<span class="secondary-text">{comment.publishedText}</span>
|
||||
</p>
|
||||
<a href={`/channel/${comment.authorId}`}>
|
||||
<p class="no-margin">
|
||||
<span class="bold" class:channel-owner={comment.authorIsChannelOwner}
|
||||
>{comment.author}</span
|
||||
>
|
||||
<span class="secondary-text">{comment.publishedText}</span>
|
||||
</p>
|
||||
</a>
|
||||
{#if comment.isPinned}
|
||||
<i>push_pin</i>
|
||||
{/if}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import 'vidstack/bundle';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import type { Page } from '@sveltejs/kit';
|
||||
import { SponsorBlock, type Category } from 'sponsorblock-api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
@@ -138,7 +139,11 @@
|
||||
}
|
||||
|
||||
if (get(playerDashStore)) {
|
||||
src = [{ src: data.video.dashUrl + '?local=true', type: 'application/dash+xml' }];
|
||||
src = [{ src: data.video.dashUrl, type: 'application/dash+xml' }];
|
||||
|
||||
if (Capacitor.getPlatform() !== 'electron' || proxyVideos) {
|
||||
(src[0] as { src: string }).src += '?local=true';
|
||||
}
|
||||
|
||||
player.addEventListener('dash-can-play', async () => {
|
||||
await loadPlayerPos();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import type { Playlist } from './Api/model';
|
||||
import { getBestThumbnail, truncate } from './misc';
|
||||
import { getBestThumbnail, getLetterCaseClass, truncate } from './misc';
|
||||
|
||||
export let playlist: Playlist;
|
||||
export let disabled: boolean = false;
|
||||
@@ -66,7 +66,7 @@
|
||||
<nav class="no-margin">
|
||||
<div class="max">
|
||||
<a class:link-disabled={disabled} href={playlistLink}
|
||||
><div class="bold">{truncate(playlist.title)}</div></a
|
||||
><div class={`bold ${getLetterCaseClass()}`}>{truncate(playlist.title)}</div></a
|
||||
>
|
||||
<div>
|
||||
<a class:link-disabled={playlist.authorId === null} href={`/channel/${playlist.authorId}`}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { bookmarkletSaveToUrl } from '$lib/externalSettings';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { iso31661 } from 'iso-3166';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { ensureNoTrailingSlash } from './misc';
|
||||
import { ensureNoTrailingSlash, titleCases, truncate } from './misc';
|
||||
import {
|
||||
authStore,
|
||||
darkModeStore,
|
||||
@@ -11,7 +12,9 @@
|
||||
deArrowInstanceStore,
|
||||
deArrowThumbnailInstanceStore,
|
||||
instanceStore,
|
||||
interfaceForceCase,
|
||||
interfacePreviewVideoOnHoverStore,
|
||||
interfaceRegionStore,
|
||||
interfaceSearchSuggestionsStore,
|
||||
playerAlwaysLoopStore,
|
||||
playerAndroidBackgroundPlayStore,
|
||||
@@ -42,6 +45,8 @@
|
||||
let invidiousInstance = get(instanceStore);
|
||||
let deArrowUrl = get(deArrowInstanceStore);
|
||||
let deArrowThumbnailUrl = get(deArrowThumbnailInstanceStore);
|
||||
let region = get(interfaceRegionStore);
|
||||
let forceCase = get(interfaceForceCase);
|
||||
|
||||
const sponsorCategories = [
|
||||
{ name: $_('layout.sponsors.sponsor'), category: 'sponsor' },
|
||||
@@ -131,6 +136,7 @@
|
||||
|
||||
<div class="settings">
|
||||
<h6>Interface</h6>
|
||||
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
@@ -163,6 +169,33 @@
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="field label suffix border">
|
||||
<select name="region" bind:value={region} on:change={() => interfaceRegionStore.set(region)}>
|
||||
{#each iso31661 as region}
|
||||
<option selected={$interfaceRegionStore === region.alpha2} value={region.alpha2}
|
||||
>{region.alpha2} - {truncate(region.name, 13)}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
<label for="region">{$_('region')}</label>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
|
||||
<div class="field label suffix border">
|
||||
<select
|
||||
name="case"
|
||||
bind:value={forceCase}
|
||||
on:change={() => interfaceForceCase.set(forceCase)}
|
||||
>
|
||||
<option selected={$interfaceForceCase === null} value={null}>Default</option>
|
||||
{#each titleCases as caseType}
|
||||
<option selected={$interfaceForceCase === caseType} value={caseType}>{caseType}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<label for="case">{$_('letterCase')}</label>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="row"
|
||||
href="#copy"
|
||||
on:click={async () =>
|
||||
await navigator.clipboard.writeText(`${location.origin}watch/${video.videoId}`)}
|
||||
await navigator.clipboard.writeText(`${location.origin}/watch/${video.videoId}`)}
|
||||
>
|
||||
<div class="min">{$_('player.share.materialiousLink')}</div></a
|
||||
>
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
import { getDeArrow, getThumbnail, getVideo, getVideoProgress } from './Api';
|
||||
import type { Notification, PlaylistPageVideo, Video, VideoBase, VideoPlay } from './Api/model';
|
||||
import ShareVideo from './ShareVideo.svelte';
|
||||
import { cleanNumber, getBestThumbnail, proxyVideoUrl, videoLength } from './misc';
|
||||
import {
|
||||
cleanNumber,
|
||||
getBestThumbnail,
|
||||
getLetterCaseClass,
|
||||
proxyVideoUrl,
|
||||
videoLength
|
||||
} from './misc';
|
||||
import type { PlayerEvents } from './player';
|
||||
import {
|
||||
authStore,
|
||||
@@ -292,11 +298,14 @@
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
style="display: flex; justify-content:flex-start; position: absolute; width: 100%;"
|
||||
><div class="bold" style="white-space: nowrap; overflow: hidden;text-overflow: ellipsis;">
|
||||
><div
|
||||
class={`${getLetterCaseClass()} bold`}
|
||||
style="white-space: nowrap; overflow: hidden;text-overflow: ellipsis;"
|
||||
>
|
||||
{video.title}
|
||||
</div>
|
||||
|
||||
<div class="tooltip bottom small">{video.title}</div>
|
||||
<div class={`tooltip bottom small ${getLetterCaseClass()}`}>{video.title}</div>
|
||||
</a>
|
||||
<div style="margin-top: 1.4em;">
|
||||
<a href={`/channel/${video.authorId}`}>{video.author}</a
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
deArrowEnabledStore,
|
||||
deArrowInstanceStore,
|
||||
deArrowThumbnailInstanceStore,
|
||||
interfaceForceCase,
|
||||
interfacePreviewVideoOnHoverStore,
|
||||
interfaceRegionStore,
|
||||
interfaceSearchSuggestionsStore,
|
||||
playerAlwaysLoopStore,
|
||||
playerAutoPlayStore,
|
||||
@@ -141,6 +143,16 @@ const persistedStores = [
|
||||
name: 'synciousInstance',
|
||||
store: synciousInstanceStore,
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'region',
|
||||
store: interfaceRegionStore,
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'forceCase',
|
||||
store: interfaceForceCase,
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"deleteAllHistory": "Delete all history",
|
||||
"skipping": "Skipping",
|
||||
"replies": "replies",
|
||||
"region": "Region",
|
||||
"letterCase": "Letter case for titles",
|
||||
"hideReplies": "Hide replies",
|
||||
"hideVideo": "Hide video",
|
||||
"syncParty": {
|
||||
|
||||
@@ -4,7 +4,23 @@ import humanNumber from 'human-number';
|
||||
import type Peer from 'peerjs';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Image } from './Api/model';
|
||||
import { instanceStore } from './store';
|
||||
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 getLetterCaseClass(): string {
|
||||
const casing = get(interfaceForceCase);
|
||||
if (!casing) return '';
|
||||
|
||||
return casing.toLowerCase().replaceAll(' ', '-');
|
||||
}
|
||||
|
||||
export function truncate(value: string, maxLength: number = 50): string {
|
||||
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value;
|
||||
@@ -79,7 +95,8 @@ export function phaseDescription(content: string): PhasedDescription {
|
||||
const title = timestampMatch[4] || '';
|
||||
timestamps.push({
|
||||
time: convertToSeconds(time),
|
||||
title: decodeHtmlCharCodes(title),
|
||||
// Remove any URL in the timestamp title.
|
||||
title: decodeHtmlCharCodes(title.replace(/<a\b[^>]*>(.*?)<\/a>/gi, '')),
|
||||
timePretty: timestamp
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,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';
|
||||
|
||||
export const instanceStore: Writable<string> = persisted(
|
||||
"invidiousInstance",
|
||||
@@ -42,8 +43,10 @@ export const synciousInstanceStore: Writable<string | null | undefined> = persis
|
||||
import.meta.env.VITE_DEFAULT_SYNCIOUS_INSTANCE || 'https://syncious.materialio.us'
|
||||
);
|
||||
|
||||
export const interfaceRegionStore: Writable<string> = persisted('interfaceRegion', 'US');
|
||||
export const interfaceSearchSuggestionsStore = persisted('searchSuggestions', true);
|
||||
export const interfacePreviewVideoOnHoverStore = persisted('previewVideoOnHover', true);
|
||||
export const interfaceForceCase: Writable<TitleCase> = persisted('forceCase', null);
|
||||
|
||||
export const sponsorBlockStore = persisted('sponsorBlock', true);
|
||||
export const sponsorBlockUrlStore: Writable<string | null | undefined> = persisted(
|
||||
@@ -73,3 +76,4 @@ export const playlistSettingsStore: Writable<Record<string, { shuffle: boolean;
|
||||
|
||||
export const miniPlayerSrcStore: Writable<{ video: VideoPlay; time: number; } | null> =
|
||||
writable(null);
|
||||
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
import Player from '$lib/Player.svelte';
|
||||
import ShareVideo from '$lib/ShareVideo.svelte';
|
||||
import Thumbnail from '$lib/Thumbnail.svelte';
|
||||
import { cleanNumber, getBestThumbnail, numberWithCommas, unsafeRandomItem } from '$lib/misc';
|
||||
import {
|
||||
cleanNumber,
|
||||
getBestThumbnail,
|
||||
getLetterCaseClass,
|
||||
numberWithCommas,
|
||||
unsafeRandomItem
|
||||
} from '$lib/misc';
|
||||
import type { PlayerEvents } from '$lib/player';
|
||||
import {
|
||||
activePageStore,
|
||||
@@ -408,7 +414,7 @@
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
<h5>{data.video.title}</h5>
|
||||
<h5 class={getLetterCaseClass()}>{data.video.title}</h5>
|
||||
|
||||
<div class="grid no-padding">
|
||||
<div class="s12 m12 l5">
|
||||
|
||||
@@ -38,6 +38,27 @@ main.root {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
|
||||
.lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.title-case {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.sentence-case {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.sentence-case::first-letter {
|
||||
text-transform: uppercase !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
dialog.right {
|
||||
width: 100% !important;
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
|
||||
LATEST_VERSION = "1.0.10"
|
||||
LATEST_VERSION = "1.0.11"
|
||||
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