@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 12
|
||||
versionName "1.1.0"
|
||||
versionCode 13
|
||||
versionName "1.1.1"
|
||||
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.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
Generated
+1869
-1814
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -60,4 +60,4 @@
|
||||
"svelte-persisted-store": "^0.11.0",
|
||||
"vidstack": "^1.11.21"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-1
@@ -1,2 +1 @@
|
||||
declare module 'virtual:pwa-info';
|
||||
declare type ui = import('beercss').ui;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { bookmarkletSaveToUrl } from '$lib/externalSettings';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import ui from 'beercss';
|
||||
import { iso31661 } from 'iso-3166';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -11,7 +12,10 @@
|
||||
deArrowEnabledStore,
|
||||
deArrowInstanceStore,
|
||||
deArrowThumbnailInstanceStore,
|
||||
deArrowTitlesOnly,
|
||||
instanceStore,
|
||||
interfaceAutoExpandComments,
|
||||
interfaceAutoExpandDesc,
|
||||
interfaceForceCase,
|
||||
interfacePreviewVideoOnHoverStore,
|
||||
interfaceRegionStore,
|
||||
@@ -170,6 +174,38 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.expandDescription')}</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$interfaceAutoExpandDesc}
|
||||
on:click={() => interfaceAutoExpandDesc.set(!$interfaceAutoExpandDesc)}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.expandComments')}</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$interfaceAutoExpandComments}
|
||||
on:click={() => interfaceAutoExpandComments.set(!$interfaceAutoExpandComments)}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="field label suffix border">
|
||||
<select name="region" bind:value={region} on:change={() => interfaceRegionStore.set(region)}>
|
||||
{#each iso31661 as region}
|
||||
@@ -533,6 +569,20 @@
|
||||
</nav>
|
||||
</form>
|
||||
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<p>{$_('layout.deArrow.titleOnly')}</p>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input
|
||||
bind:checked={$deArrowTitlesOnly}
|
||||
on:click={() => deArrowTitlesOnly.set(!$deArrowTitlesOnly)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<p>{$_('enabled')}</p>
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
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, letterCase, proxyVideoUrl, videoLength } from './misc';
|
||||
import {
|
||||
cleanNumber,
|
||||
getBestThumbnail,
|
||||
letterCase,
|
||||
proxyVideoUrl,
|
||||
truncate,
|
||||
videoLength
|
||||
} from './misc';
|
||||
import type { PlayerEvents } from './player';
|
||||
import {
|
||||
authStore,
|
||||
deArrowEnabledStore,
|
||||
deArrowTitlesOnly,
|
||||
interfacePreviewVideoOnHoverStore,
|
||||
playerProxyVideosStore,
|
||||
playerSavePlaybackPositionStore,
|
||||
@@ -22,6 +30,7 @@
|
||||
|
||||
export let video: VideoBase | Video | Notification | PlaylistPageVideo;
|
||||
export let playlistId: string = '';
|
||||
export let sideways: boolean = false;
|
||||
|
||||
let thumbnailHidden: boolean = false;
|
||||
let showVideoPreview: boolean = false;
|
||||
@@ -60,6 +69,14 @@
|
||||
progress = null;
|
||||
}
|
||||
|
||||
let startedSideways = sideways === true;
|
||||
function disableSideways() {
|
||||
if (!startedSideways) return;
|
||||
|
||||
if (window.innerWidth <= 1000) sideways = false;
|
||||
else sideways = true;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
thumbnailHidden = localStorage.getItem(`v_h_${video.videoId}`) === '1';
|
||||
@@ -70,6 +87,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
addEventListener('resize', disableSideways);
|
||||
|
||||
let imageSrc = getBestThumbnail(video.videoThumbnails) as string;
|
||||
|
||||
if (get(deArrowEnabledStore)) {
|
||||
@@ -95,7 +114,7 @@
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (!locatedThumbnail) {
|
||||
if (!locatedThumbnail && !get(deArrowTitlesOnly)) {
|
||||
// Process thumbnail locally.
|
||||
const canvas = document.getElementById('canvas') as HTMLCanvasElement | null;
|
||||
function generateThumbnail(): Promise<string> {
|
||||
@@ -154,6 +173,10 @@
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
removeEventListener('resize', disableSideways);
|
||||
});
|
||||
|
||||
function syncChangeVideo() {
|
||||
if ($syncPartyConnectionsStore) {
|
||||
const events = {
|
||||
@@ -206,124 +229,129 @@
|
||||
</script>
|
||||
|
||||
{#if !thumbnailHidden}
|
||||
<div
|
||||
style="position: relative;"
|
||||
on:mouseover={previewVideo}
|
||||
on:mouseleave={() => (showVideoPreview = false)}
|
||||
on:focus={() => {}}
|
||||
bind:clientHeight={imgHeight}
|
||||
role="region"
|
||||
>
|
||||
<a
|
||||
class="wave thumbnail"
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
on:click={syncChangeVideo}
|
||||
<div class:side-ways={sideways}>
|
||||
<div
|
||||
style="position: relative;"
|
||||
on:mouseover={previewVideo}
|
||||
on:mouseleave={() => (showVideoPreview = false)}
|
||||
on:focus={() => {}}
|
||||
bind:clientHeight={imgHeight}
|
||||
role="region"
|
||||
>
|
||||
{#if loading}
|
||||
<progress class="circle"></progress>
|
||||
{:else if loaded}
|
||||
{#if showVideoPreview && videoPreview}
|
||||
<div style="max-width: 100%; max-height: {imgHeight}px;">
|
||||
<video
|
||||
style="max-width: 100%; height: {imgHeight}px;"
|
||||
autoplay
|
||||
poster={img.src}
|
||||
width="100%"
|
||||
height="100%"
|
||||
muted={videoPreviewMuted}
|
||||
controls={false}
|
||||
volume={videoPreviewVolume}
|
||||
src={proxyVideos
|
||||
? proxyVideoUrl(videoPreview.formatStreams[0].url)
|
||||
: videoPreview.formatStreams[0].url}
|
||||
>
|
||||
</video>
|
||||
</div>
|
||||
{:else}
|
||||
<img class="responsive" src={img.src} alt="Thumbnail for video" />
|
||||
{/if}
|
||||
{:else}
|
||||
<p>{$_('thumbnail.failedToLoadImage')}</p>
|
||||
{/if}
|
||||
{#if progress}
|
||||
<progress
|
||||
class="absolute right bottom"
|
||||
style="z-index: 1;"
|
||||
value={progress}
|
||||
max={video.lengthSeconds}
|
||||
></progress>
|
||||
{/if}
|
||||
{#if !('liveVideo' in video) || !video.liveVideo}
|
||||
{#if video.lengthSeconds !== 0}
|
||||
<div
|
||||
class="absolute right bottom small-margin black white-text small-text thumbnail-corner"
|
||||
>
|
||||
{videoLength(video.lengthSeconds)}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="absolute right bottom small-margin red white-text small-text thumbnail-corner">
|
||||
{$_('thumbnail.live')}
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
{#if showVideoPreview && videoPreview}
|
||||
<button
|
||||
class="no-padding"
|
||||
style="position: absolute; bottom: 10px; left: 10px; width: 30px; height: 30px;"
|
||||
on:click={() => {
|
||||
videoPreviewMuted = !videoPreviewMuted;
|
||||
}}
|
||||
<a
|
||||
class="wave thumbnail"
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
on:click={syncChangeVideo}
|
||||
>
|
||||
<i>
|
||||
{#if videoPreviewMuted}
|
||||
volume_off
|
||||
{:else}
|
||||
volume_up
|
||||
{/if}
|
||||
</i>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="small-padding">
|
||||
<nav class="no-margin">
|
||||
<div class="max">
|
||||
<a
|
||||
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;">
|
||||
{letterCase(video.title)}
|
||||
</div>
|
||||
|
||||
<div class="tooltip bottom small">{letterCase(video.title)}</div>
|
||||
</a>
|
||||
<div style="margin-top: 1.4em;">
|
||||
<a href={`/channel/${video.authorId}`}>{video.author}</a
|
||||
>{#if !('publishedText' in video) && 'viewCountText' in video}
|
||||
• {video.viewCountText}{/if}
|
||||
</div>
|
||||
<nav class="no-margin">
|
||||
{#if 'publishedText' in video}
|
||||
<div class="max">
|
||||
{cleanNumber(video.viewCount)} • {video.publishedText}
|
||||
{#if loading}
|
||||
<progress class="circle"></progress>
|
||||
{:else if loaded}
|
||||
{#if showVideoPreview && videoPreview}
|
||||
<div style="max-width: 100%; max-height: {imgHeight}px;">
|
||||
<video
|
||||
style="max-width: 100%; height: {imgHeight}px;"
|
||||
autoplay
|
||||
poster={img.src}
|
||||
width="100%"
|
||||
height="100%"
|
||||
muted={videoPreviewMuted}
|
||||
controls={false}
|
||||
volume={videoPreviewVolume}
|
||||
src={proxyVideos
|
||||
? proxyVideoUrl(videoPreview.formatStreams[0].url)
|
||||
: videoPreview.formatStreams[0].url}
|
||||
>
|
||||
</video>
|
||||
</div>
|
||||
<button class="transparent circle">
|
||||
<i>more_vert</i>
|
||||
<menu class="left no-wrap">
|
||||
<a href="#hide" on:click={hideVideo}>
|
||||
{$_('hideVideo')}
|
||||
</a>
|
||||
<div class="divider"></div>
|
||||
<ShareVideo {video} />
|
||||
</menu>
|
||||
</button>
|
||||
{:else}
|
||||
<img class="responsive" src={img.src} alt="Thumbnail for video" />
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
{:else}
|
||||
<p>{$_('thumbnail.failedToLoadImage')}</p>
|
||||
{/if}
|
||||
{#if progress}
|
||||
<progress
|
||||
class="absolute right bottom"
|
||||
style="z-index: 1;"
|
||||
value={progress}
|
||||
max={video.lengthSeconds}
|
||||
></progress>
|
||||
{/if}
|
||||
{#if !('liveVideo' in video) || !video.liveVideo}
|
||||
{#if video.lengthSeconds !== 0}
|
||||
<div
|
||||
class="absolute right bottom small-margin black white-text small-text thumbnail-corner"
|
||||
>
|
||||
{videoLength(video.lengthSeconds)}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="absolute right bottom small-margin red white-text small-text thumbnail-corner"
|
||||
>
|
||||
{$_('thumbnail.live')}
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
{#if showVideoPreview && videoPreview}
|
||||
<button
|
||||
class="no-padding"
|
||||
style="position: absolute; bottom: 10px; left: 10px; width: 30px; height: 30px;"
|
||||
on:click={() => {
|
||||
videoPreviewMuted = !videoPreviewMuted;
|
||||
}}
|
||||
>
|
||||
<i>
|
||||
{#if videoPreviewMuted}
|
||||
volume_off
|
||||
{:else}
|
||||
volume_up
|
||||
{/if}
|
||||
</i>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="small-padding">
|
||||
<nav class="no-margin">
|
||||
<div class="max">
|
||||
<a
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
class:ellipsis-root={!sideways}
|
||||
><div class="bold" class:ellipsis={!sideways}>
|
||||
{truncate(letterCase(video.title.trim()), 40)}
|
||||
</div>
|
||||
|
||||
<div class="tooltip bottom small">{letterCase(video.title)}</div>
|
||||
</a>
|
||||
|
||||
<a class:author={!sideways} href={`/channel/${video.authorId}`}>{video.author}</a>
|
||||
<span>
|
||||
{#if !('publishedText' in video) && 'viewCountText' in video}
|
||||
• {video.viewCountText}{/if}
|
||||
</span>
|
||||
<nav class="no-margin">
|
||||
{#if 'publishedText' in video}
|
||||
<div class="max">
|
||||
{cleanNumber(video.viewCount)} • {video.publishedText}
|
||||
</div>
|
||||
<button class="transparent circle">
|
||||
<i>more_vert</i>
|
||||
<menu class="left no-wrap">
|
||||
<a href="#hide" on:click={hideVideo}>
|
||||
{$_('hideVideo')}
|
||||
</a>
|
||||
<div class="divider"></div>
|
||||
<ShareVideo {video} />
|
||||
</menu>
|
||||
</button>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -331,6 +359,31 @@
|
||||
<div id="video-container" style="display: none;"></div>
|
||||
|
||||
<style>
|
||||
.side-ways {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.side-ways img {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.ellipsis-root {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.author {
|
||||
margin-top: 1.4em;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
deArrowEnabledStore,
|
||||
deArrowInstanceStore,
|
||||
deArrowThumbnailInstanceStore,
|
||||
deArrowTitlesOnly,
|
||||
interfaceAutoExpandComments,
|
||||
interfaceAutoExpandDesc,
|
||||
interfaceForceCase,
|
||||
interfacePreviewVideoOnHoverStore,
|
||||
interfaceRegionStore,
|
||||
@@ -153,6 +156,21 @@ const persistedStores = [
|
||||
name: 'forceCase',
|
||||
store: interfaceForceCase,
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'autoExpandComments',
|
||||
store: interfaceAutoExpandComments,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'autoExpandDesc',
|
||||
store: interfaceAutoExpandDesc,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'deArrowTitlesOnly',
|
||||
store: deArrowTitlesOnly,
|
||||
type: 'boolean'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@
|
||||
},
|
||||
"searchSuggestions": "Search suggestions",
|
||||
"previewVideoOnHover": "Preview video on hover",
|
||||
"expandDescription": "Expand description by default",
|
||||
"expandComments": "Expand comments by default",
|
||||
"dataPreferences": {
|
||||
"content": "Looking to import/export subscriptions, change password or delete account? Click here and scroll to the bottom of the page.",
|
||||
"dataPreferences": "Data preferences"
|
||||
@@ -123,7 +125,8 @@
|
||||
},
|
||||
"deArrow": {
|
||||
"title": "DeArrow",
|
||||
"thumbnailInstanceUrl": "Thumbnail instance URL"
|
||||
"thumbnailInstanceUrl": "Thumbnail instance URL",
|
||||
"titleOnly": "Title only"
|
||||
}
|
||||
},
|
||||
"subscribe": "Subscribe"
|
||||
|
||||
@@ -47,6 +47,8 @@ export const interfaceRegionStore: Writable<string> = persisted('interfaceRegion
|
||||
export const interfaceSearchSuggestionsStore = persisted('searchSuggestions', true);
|
||||
export const interfacePreviewVideoOnHoverStore = persisted('previewVideoOnHover', true);
|
||||
export const interfaceForceCase: Writable<TitleCase> = persisted('forceCase', null);
|
||||
export const interfaceAutoExpandComments: Writable<boolean> = persisted('autoExpandComments', true);
|
||||
export const interfaceAutoExpandDesc: Writable<boolean> = persisted('autoExpandDesc', false);
|
||||
|
||||
export const sponsorBlockStore = persisted('sponsorBlock', true);
|
||||
export const sponsorBlockUrlStore: Writable<string | null | undefined> = persisted(
|
||||
@@ -63,6 +65,7 @@ export const deArrowInstanceStore = persisted(
|
||||
import.meta.env.VITE_DEFAULT_DEARROW_INSTANCE || 'https://sponsor.ajay.app'
|
||||
);
|
||||
export const deArrowEnabledStore = persisted('deArrowEnabled', false);
|
||||
export const deArrowTitlesOnly = persisted('deArrowTitlesOnly', true);
|
||||
export const deArrowThumbnailInstanceStore = persisted(
|
||||
'deArrowThumbnailInstance',
|
||||
import.meta.env.VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE || 'https://dearrow-thumb.ajay.app'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import ui from 'beercss';
|
||||
|
||||
export async function getDynamicTheme(mode?: string): Promise<Record<string, string>> {
|
||||
const givenSettings = (await window.ui('theme')) as IBeerCssTheme;
|
||||
const givenSettings = (await ui('theme'));
|
||||
|
||||
// @ts-ignore
|
||||
const themes: string = givenSettings[mode ? mode : (ui('mode') as string)];
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
import {
|
||||
activePageStore,
|
||||
authStore,
|
||||
interfaceAutoExpandComments,
|
||||
interfaceAutoExpandDesc,
|
||||
miniPlayerSrcStore,
|
||||
playerAutoplayNextByDefaultStore,
|
||||
playerListenByDefaultStore,
|
||||
@@ -555,7 +557,7 @@
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<details>
|
||||
<details open={$interfaceAutoExpandDesc}>
|
||||
<summary class="bold none">
|
||||
<nav>
|
||||
<div class="max">
|
||||
@@ -597,7 +599,7 @@
|
||||
|
||||
{#if comments && comments.comments.length > 0}
|
||||
<article>
|
||||
<details open>
|
||||
<details open={$interfaceAutoExpandComments}>
|
||||
<summary class="none bold">
|
||||
<nav>
|
||||
<div class="max">{numberWithCommas(comments.commentCount)} comments</div>
|
||||
@@ -631,7 +633,7 @@
|
||||
{#each data.video.recommendedVideos as recommendedVideo}
|
||||
<article class="no-padding">
|
||||
{#key recommendedVideo.videoId}
|
||||
<Thumbnail video={recommendedVideo} />
|
||||
<Thumbnail video={recommendedVideo} sideways={true} />
|
||||
{/key}
|
||||
</article>
|
||||
{/each}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 995 KiB After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 854 KiB |
+1
-1
@@ -5,7 +5,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
|
||||
LATEST_VERSION = "1.1.0"
|
||||
LATEST_VERSION = "1.1.1"
|
||||
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