@@ -59,6 +59,9 @@ Due to Google's policies on custom YouTube frontends, Materialious isn't availab
|
||||
## Player
|
||||

|
||||
|
||||
## Transcript
|
||||

|
||||
|
||||
## Settings
|
||||

|
||||
|
||||
|
||||
@@ -159,7 +159,13 @@ Materialious allows you to overwrite the default values using `VITE_DEFAULT_SETT
|
||||
{
|
||||
// Set to true to enable dark mode, or false to disable it.
|
||||
"darkMode": true,
|
||||
|
||||
|
||||
// Force case for Videos/Playlists etc.
|
||||
"forceCase": "uppercase | lowercase | sentence case | title case",
|
||||
|
||||
// Any ISO 3166 country code
|
||||
"region": "US",
|
||||
|
||||
// Specifies the theme color in hexadecimal format (e.g., #ff0000 for red).
|
||||
"themeColor": "#ff0000",
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 11
|
||||
versionName "1.0.11"
|
||||
versionCode 12
|
||||
versionName "1.1.0"
|
||||
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.11",
|
||||
"version": "1.1.0",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.0.11",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "materialious",
|
||||
"version": "1.0.11",
|
||||
"version": "1.1.0",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor/android": "^6.0.0",
|
||||
@@ -16,9 +16,11 @@
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"beercss": "^3.5.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"he": "^1.2.0",
|
||||
"human-number": "^2.0.4",
|
||||
"iso-3166": "^4.3.0",
|
||||
"material-dynamic-colors": "^1.1.0",
|
||||
"media-captions": "^1.0.3",
|
||||
"media-icons": "^0.10.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"peerjs": "^1.5.3",
|
||||
@@ -36,6 +38,7 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||
"@types/eslint": "^8.56.7",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/he": "^1.2.3",
|
||||
"@types/human-number": "^1.0.2",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
||||
@@ -3837,6 +3840,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/he": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz",
|
||||
"integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/human-number": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/human-number/-/human-number-1.0.2.tgz",
|
||||
@@ -7258,7 +7267,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.0.11",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -19,6 +19,7 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||
"@types/eslint": "^8.56.7",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/he": "^1.2.3",
|
||||
"@types/human-number": "^1.0.2",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
||||
@@ -45,9 +46,11 @@
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"beercss": "^3.5.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
"he": "^1.2.0",
|
||||
"human-number": "^2.0.4",
|
||||
"iso-3166": "^4.3.0",
|
||||
"material-dynamic-colors": "^1.1.0",
|
||||
"media-captions": "^1.0.3",
|
||||
"media-icons": "^0.10.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"peerjs": "^1.5.3",
|
||||
|
||||
@@ -53,12 +53,6 @@
|
||||
|
||||
page.subscribe((pageUpdate) => loadTimeFromUrl(pageUpdate));
|
||||
|
||||
export function seekTo(time: number) {
|
||||
if (typeof player !== 'undefined') {
|
||||
player.currentTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
const proxyVideos = get(playerProxyVideosStore);
|
||||
|
||||
onMount(async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import type { Playlist } from './Api/model';
|
||||
import { getBestThumbnail, getLetterCaseClass, truncate } from './misc';
|
||||
import { getBestThumbnail, letterCase, 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 ${getLetterCaseClass()}`}>{truncate(playlist.title)}</div></a
|
||||
><div class="bold">{letterCase(truncate(playlist.title))}</div></a
|
||||
>
|
||||
<div>
|
||||
<a class:link-disabled={playlist.authorId === null} href={`/channel/${playlist.authorId}`}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { iso31661 } from 'iso-3166';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { ensureNoTrailingSlash, titleCases, truncate } from './misc';
|
||||
import { ensureNoTrailingSlash, titleCase, titleCases, truncate } from './misc';
|
||||
import {
|
||||
authStore,
|
||||
darkModeStore,
|
||||
@@ -190,7 +190,9 @@
|
||||
>
|
||||
<option selected={$interfaceForceCase === null} value={null}>Default</option>
|
||||
{#each titleCases as caseType}
|
||||
<option selected={$interfaceForceCase === caseType} value={caseType}>{caseType}</option>
|
||||
<option selected={$interfaceForceCase === caseType} value={caseType}
|
||||
>{titleCase(`${caseType}`)}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
<label for="case">{$_('letterCase')}</label>
|
||||
|
||||
@@ -5,13 +5,7 @@
|
||||
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,
|
||||
getLetterCaseClass,
|
||||
proxyVideoUrl,
|
||||
videoLength
|
||||
} from './misc';
|
||||
import { cleanNumber, getBestThumbnail, letterCase, proxyVideoUrl, videoLength } from './misc';
|
||||
import type { PlayerEvents } from './player';
|
||||
import {
|
||||
authStore,
|
||||
@@ -298,14 +292,11 @@
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
style="display: flex; justify-content:flex-start; position: absolute; width: 100%;"
|
||||
><div
|
||||
class={`${getLetterCaseClass()} bold`}
|
||||
style="white-space: nowrap; overflow: hidden;text-overflow: ellipsis;"
|
||||
>
|
||||
{video.title}
|
||||
><div class="bold" style="white-space: nowrap; overflow: hidden;text-overflow: ellipsis;">
|
||||
{letterCase(video.title)}
|
||||
</div>
|
||||
|
||||
<div class={`tooltip bottom small ${getLetterCaseClass()}`}>{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
|
||||
@@ -341,7 +332,6 @@
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
max-height: 160px;
|
||||
}
|
||||
|
||||
.thumbnail img {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<script lang="ts">
|
||||
import Fuse from 'fuse.js';
|
||||
import { VTTCue, parseText, type ParsedCaptionsResult } from 'media-captions';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { MediaTimeUpdateEvent } from 'vidstack';
|
||||
import type { MediaPlayerElement } from 'vidstack/elements';
|
||||
import type { VideoPlay } from './Api/model';
|
||||
import { decodeHtmlCharCodes, videoLength } from './misc';
|
||||
import { instanceStore } from './store';
|
||||
|
||||
export let video: VideoPlay;
|
||||
export let player: MediaPlayerElement;
|
||||
|
||||
let url: string | null = null;
|
||||
|
||||
let transcript: ParsedCaptionsResult | null = null;
|
||||
let transcriptCues: VTTCue[] = [];
|
||||
let isLoading = false;
|
||||
let currentTime = 0;
|
||||
let search: string = '';
|
||||
|
||||
player.addEventListener('time-update', (event: MediaTimeUpdateEvent) => {
|
||||
currentTime = event.detail.currentTime;
|
||||
|
||||
const currentTranscriptLine = document.querySelector(
|
||||
'.transcript-line.secondary-container'
|
||||
) as HTMLElement;
|
||||
const transcriptScrollable = document.getElementById('transcript');
|
||||
|
||||
if (currentTranscriptLine && transcriptScrollable) {
|
||||
transcriptScrollable.scrollTop =
|
||||
currentTranscriptLine.offsetTop - transcriptScrollable.offsetTop - 300;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadTranscript() {
|
||||
if (!url) {
|
||||
transcript = null;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
transcript = null;
|
||||
const resp = await fetch(`${get(instanceStore)}${url}`);
|
||||
transcript = await parseText(await resp.text(), { strict: false });
|
||||
transcriptCues = transcript.cues;
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function searchTranscript() {
|
||||
if (!transcript) return;
|
||||
|
||||
if (search.trim() === '') {
|
||||
transcriptCues = transcript.cues;
|
||||
return;
|
||||
}
|
||||
|
||||
const fuse = new Fuse(transcript.cues, {
|
||||
keys: ['text']
|
||||
});
|
||||
|
||||
transcriptCues = fuse.search(search).map((item) => item.item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="scroll no-padding" style="height: 75vh;" id="transcript">
|
||||
<article class="no-elevate" style="position: sticky; top: 0; z-index: 3;">
|
||||
<h6>{$_('transcript')}</h6>
|
||||
<div class="field label suffix border">
|
||||
<select bind:value={url} on:change={loadTranscript} name="captions">
|
||||
<option selected={true} value={null}>{$_('selectLang')}</option>
|
||||
{#each video.captions as caption}
|
||||
<option value={caption.url}>{caption.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<label for="captions">{$_('language')}</label>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
<div class="max field round suffix prefix small no-margin surface-variant">
|
||||
<i class="front">search</i><input
|
||||
bind:value={search}
|
||||
on:input={searchTranscript}
|
||||
type="text"
|
||||
placeholder={$_('searchPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div class="space"></div>
|
||||
<div class="divider"></div>
|
||||
</article>
|
||||
|
||||
{#if isLoading}
|
||||
<nav class="center-align">
|
||||
<progress class="circle"></progress>
|
||||
</nav>
|
||||
{:else if transcript}
|
||||
{#if transcript.cues.length > 0}
|
||||
{#if transcriptCues.length > 0}
|
||||
{#each transcriptCues as cue}
|
||||
<div
|
||||
class="transcript-line"
|
||||
id={`transcript-line-${cue.startTime}`}
|
||||
on:click={() => (player.currentTime = cue.startTime)}
|
||||
class:secondary-container={currentTime >= cue.startTime && currentTime <= cue.endTime}
|
||||
>
|
||||
<p class="chip no-margin">{videoLength(cue.startTime)}</p>
|
||||
<p class="transcript-text">{decodeHtmlCharCodes(cue.text)}</p>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<nav class="center-align">
|
||||
<h6>{$_('noResult')}</h6>
|
||||
</nav>
|
||||
{/if}
|
||||
{:else}
|
||||
<nav class="center-align">
|
||||
<h6>{$_('noCaptionData')}</h6>
|
||||
</nav>
|
||||
{/if}
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.transcript-line {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
padding: 0.5em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.transcript-text {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.transcript-line .chip {
|
||||
padding: 0 1.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,11 @@
|
||||
"skipping": "Skipping",
|
||||
"replies": "replies",
|
||||
"region": "Region",
|
||||
"noResult": "No results",
|
||||
"language": "Language",
|
||||
"transcript": "Transcript",
|
||||
"noCaptionData": "Captions don't have any data.",
|
||||
"selectLang": "Select language",
|
||||
"letterCase": "Letter case for titles",
|
||||
"hideReplies": "Hide replies",
|
||||
"hideVideo": "Hide video",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { pushState } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import he from 'he';
|
||||
import humanNumber from 'human-number';
|
||||
import type Peer from 'peerjs';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -15,13 +16,48 @@ export const titleCases: TitleCase[] = [
|
||||
'sentence case'
|
||||
];
|
||||
|
||||
export function getLetterCaseClass(): string {
|
||||
export function letterCase(text: string): string {
|
||||
const casing = get(interfaceForceCase);
|
||||
if (!casing) return '';
|
||||
if (!casing) return text;
|
||||
|
||||
return casing.toLowerCase().replaceAll(' ', '-');
|
||||
switch (casing) {
|
||||
case 'lowercase':
|
||||
return text.toLowerCase();
|
||||
case 'uppercase':
|
||||
text.toUpperCase();
|
||||
case 'sentence case':
|
||||
return sentenceCase(text);
|
||||
default:
|
||||
return titleCase(text);
|
||||
}
|
||||
}
|
||||
|
||||
export function sentenceCase(text: string): string {
|
||||
let sentences: string[] = text.match(/[^.!?]*[.!?]*/g) || [];
|
||||
|
||||
let casedSentences: string[] = sentences.map(sentence => {
|
||||
sentence = sentence.trim();
|
||||
if (sentence.length === 0) return '';
|
||||
return sentence.charAt(0).toUpperCase() + sentence.slice(1).toLowerCase();
|
||||
});
|
||||
|
||||
return casedSentences.join(' ');
|
||||
}
|
||||
|
||||
export function titleCase(text: string): string {
|
||||
if (!text) return '';
|
||||
|
||||
let words: string[] = text.split(/\s+/);
|
||||
|
||||
let titleCasedWords: string[] = words.map(word => {
|
||||
if (word.length === 0) return '';
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||
});
|
||||
|
||||
return titleCasedWords.join(' ');
|
||||
}
|
||||
|
||||
|
||||
export function truncate(value: string, maxLength: number = 50): string {
|
||||
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value;
|
||||
}
|
||||
@@ -40,7 +76,7 @@ export function cleanNumber(number: number): string {
|
||||
export function videoLength(lengthSeconds: number): string {
|
||||
const hours = Math.floor(lengthSeconds / 3600);
|
||||
let minutes: number | string = Math.floor((lengthSeconds % 3600) / 60);
|
||||
let seconds: number | string = lengthSeconds % 60;
|
||||
let seconds: number | string = Math.round(lengthSeconds % 60);
|
||||
|
||||
if (minutes < 10) {
|
||||
minutes = `0${minutes}`;
|
||||
@@ -63,9 +99,8 @@ export interface PhasedDescription {
|
||||
}
|
||||
|
||||
export function decodeHtmlCharCodes(str: string): string {
|
||||
return str.replace(/(&#(\d+);)/g, function (match, capture, charCode) {
|
||||
return String.fromCharCode(charCode);
|
||||
});
|
||||
const { decode } = he;
|
||||
return decode(str);
|
||||
}
|
||||
|
||||
export function phaseDescription(content: string): PhasedDescription {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<div class="space"></div>
|
||||
|
||||
<div class="max field round suffix prefix small no-margin white black-text">
|
||||
<div class="max field round suffix prefix small no-margin surface-variant">
|
||||
<i class="front">search</i><input
|
||||
bind:value={search}
|
||||
on:input={searchSubs}
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
import Player from '$lib/Player.svelte';
|
||||
import ShareVideo from '$lib/ShareVideo.svelte';
|
||||
import Thumbnail from '$lib/Thumbnail.svelte';
|
||||
import Transcript from '$lib/Transcript.svelte';
|
||||
import {
|
||||
cleanNumber,
|
||||
getBestThumbnail,
|
||||
getLetterCaseClass,
|
||||
letterCase,
|
||||
numberWithCommas,
|
||||
unsafeRandomItem
|
||||
} from '$lib/misc';
|
||||
@@ -66,9 +67,10 @@
|
||||
let theatreMode = get(playerTheatreModeByDefaultStore);
|
||||
|
||||
let audioMode = get(playerListenByDefaultStore);
|
||||
let seekTo: (time: number) => void;
|
||||
let player: MediaPlayerElement;
|
||||
|
||||
let showTranscript = false;
|
||||
|
||||
playlistSettingsStore.subscribe((playlistSetting) => {
|
||||
if (!data.playlistId) return;
|
||||
if (data.playlistId in playlistSetting) {
|
||||
@@ -398,23 +400,17 @@
|
||||
<div class="space"></div>
|
||||
|
||||
<div class="grid">
|
||||
<div class={`s12 m12 l${theatreMode ? '12' : '10'}`}>
|
||||
<div class={`s12 m12 l${theatreMode ? '12' : '9'}`}>
|
||||
<div style="display: flex;justify-content: center;">
|
||||
{#key data.video.videoId}
|
||||
<div
|
||||
style="max-height: 80vh;max-width: calc(80vh * 16 / 9);overflow: hidden;position: relative;flex: 1;"
|
||||
>
|
||||
<Player
|
||||
{data}
|
||||
{audioMode}
|
||||
isSyncing={$syncPartyPeerStore !== null}
|
||||
bind:seekTo
|
||||
bind:player
|
||||
/>
|
||||
<Player {data} {audioMode} isSyncing={$syncPartyPeerStore !== null} bind:player />
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
<h5 class={getLetterCaseClass()}>{data.video.title}</h5>
|
||||
<h5>{letterCase(data.video.title)}</h5>
|
||||
|
||||
<div class="grid no-padding">
|
||||
<div class="s12 m12 l5">
|
||||
@@ -484,6 +480,15 @@
|
||||
<i>width_wide</i>
|
||||
<div class="tooltip">{$_('player.theatreMode')}</div>
|
||||
</button>
|
||||
<button
|
||||
on:click={() => ((showTranscript = !showTranscript), (theatreMode = false))}
|
||||
class:border={!showTranscript}
|
||||
>
|
||||
<i>description</i>
|
||||
<div class="tooltip">
|
||||
{$_('transcript')}
|
||||
</div>
|
||||
</button>
|
||||
<button class="border"
|
||||
><i>share</i>
|
||||
<div class="tooltip">
|
||||
@@ -568,7 +573,7 @@
|
||||
{#if data.content.timestamps.length > 0}
|
||||
<h6 style="margin-bottom: .3em;">Chapters</h6>
|
||||
{#each data.content.timestamps as timestamp}
|
||||
<button on:click={() => seekTo(timestamp.time)} class="timestamps"
|
||||
<button on:click={() => (player.currentTime = timestamp.time)} class="timestamps"
|
||||
>{timestamp.timePretty}
|
||||
{#if !timestamp.title.startsWith('-')}
|
||||
-
|
||||
@@ -617,7 +622,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if !theatreMode}
|
||||
<div class="s12 m12 l2">
|
||||
<div class="s12 m12 l3">
|
||||
{#if player && showTranscript}
|
||||
<Transcript bind:player video={data.video} />
|
||||
{/if}
|
||||
{#if !playlist}
|
||||
{#if data.video.recommendedVideos}
|
||||
{#each data.video.recommendedVideos as recommendedVideo}
|
||||
@@ -629,7 +637,7 @@
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
<article style="height: 75vh; position: relative;" id="playlist" class="scroll no-padding">
|
||||
<article style="height: 85vh; position: relative;" id="playlist" class="scroll no-padding">
|
||||
<article class="no-elevate" style="position: sticky; top: 0; z-index: 3;">
|
||||
<h6>{playlist.title}</h6>
|
||||
<p>
|
||||
@@ -700,10 +708,6 @@
|
||||
--plyr-color-main: var(--primary);
|
||||
}
|
||||
|
||||
.grid {
|
||||
padding: 1em 10em;
|
||||
}
|
||||
|
||||
.timestamps {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0.4em;
|
||||
|
||||
@@ -39,26 +39,6 @@ main.root {
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
|
||||
|
Before Width: | Height: | Size: 684 KiB After Width: | Height: | Size: 995 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 706 KiB After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -5,7 +5,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
|
||||
LATEST_VERSION = "1.0.11"
|
||||
LATEST_VERSION = "1.1.0"
|
||||
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")
|
||||
|
||||
ROOT_PACKAGE = os.path.join(WORKING_DIR, "package.json")
|
||||
|
||||