Started new color theme system
This commit is contained in:
@@ -1,24 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { bookmarkletSaveToUrl } from '$lib/externalSettings/index';
|
||||
import { letterCase, titleCases, type TitleCase } from '$lib/letterCasing';
|
||||
import { setAmoledTheme } from '$lib/theme';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import ui from 'beercss';
|
||||
import { iso31661 } from 'iso-3166';
|
||||
import type { RgbaColor, HsvaColor, Colord } from 'colord';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { isUnrestrictedPlatform, timeout, shareURL } from '$lib/misc';
|
||||
import { getPages, type Pages } from '$lib/navPages';
|
||||
import { setInvidiousInstance, goToInvidiousLogin, invidiousLogout } from '$lib/auth';
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
import {
|
||||
invidiousAuthStore,
|
||||
backendInUseStore,
|
||||
darkModeStore,
|
||||
invidiousInstanceStore,
|
||||
interfaceAllowInsecureRequests,
|
||||
interfaceAmoledTheme,
|
||||
interfaceAndroidUseNativeShare,
|
||||
interfaceAutoExpandChapters,
|
||||
interfaceAutoExpandComments,
|
||||
@@ -30,46 +24,14 @@
|
||||
interfaceSearchHistoryEnabled,
|
||||
interfaceSearchSuggestionsStore,
|
||||
searchHistoryStore,
|
||||
themeColorStore,
|
||||
watchHistoryEnabledStore
|
||||
} from '../../store';
|
||||
import { tick } from 'svelte';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import ComboBox from '../ComboBox.svelte';
|
||||
|
||||
let invidiousInstance = $state(get(invidiousInstanceStore));
|
||||
|
||||
let invalidInstance = $state(false);
|
||||
let colorPickerOpen = $state(false);
|
||||
let colorPickerDebounce: ReturnType<typeof setTimeout>;
|
||||
|
||||
async function setColor(color: {
|
||||
hsv: HsvaColor | null;
|
||||
rgb: RgbaColor | null;
|
||||
hex: string | null;
|
||||
color: Colord | null;
|
||||
}) {
|
||||
if (!color.hex) return;
|
||||
if (colorPickerDebounce) clearTimeout(colorPickerDebounce);
|
||||
|
||||
colorPickerDebounce = setTimeout(async () => {
|
||||
themeColorStore.set(color.hex);
|
||||
await tick();
|
||||
setAmoledTheme();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
const isDark = get(darkModeStore);
|
||||
|
||||
if (isDark) {
|
||||
ui('mode', 'light');
|
||||
darkModeStore.set(false);
|
||||
} else {
|
||||
ui('mode', 'dark');
|
||||
darkModeStore.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
async function setInstance(event: Event) {
|
||||
event.preventDefault();
|
||||
@@ -188,57 +150,6 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<button onclick={toggleDarkMode} class="no-margin surface-container-highest">
|
||||
{#if !$darkModeStore}
|
||||
<i>dark_mode</i>
|
||||
<span>{$_('layout.theme.darkMode')}</span>
|
||||
{:else}
|
||||
<i>light_mode</i>
|
||||
<span>{$_('layout.theme.lightMode')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
<button class="surface-container-highest" onclick={() => (colorPickerOpen = !colorPickerOpen)}>
|
||||
<i>palette</i>
|
||||
<span>{$_('layout.theme.color')}</span>
|
||||
</button>
|
||||
|
||||
{#if colorPickerOpen}
|
||||
<div class="space"></div>
|
||||
|
||||
<div class="color-picker">
|
||||
<ColorPicker
|
||||
isTextInput={false}
|
||||
isDialog={false}
|
||||
onInput={setColor}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
hex={get(themeColorStore)}
|
||||
sliderDirection="horizontal"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space"></div>
|
||||
|
||||
{#if $darkModeStore}
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.theme.AmoledTheme')}</div>
|
||||
</div>
|
||||
<label class="switch" tabindex="0">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$interfaceAmoledTheme}
|
||||
onclick={() => interfaceAmoledTheme.set(!$interfaceAmoledTheme)}
|
||||
role="switch"
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
@@ -436,22 +347,3 @@
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.color-picker {
|
||||
--cp-bg-color: var(--surface-container);
|
||||
--cp-border-color: transparent;
|
||||
--cp-text-color: var(--on-surface);
|
||||
--cp-input-color: var(--surface);
|
||||
--cp-button-hover-color: var(--surface-variant);
|
||||
--slider-width: 50px;
|
||||
--picker-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.color-picker {
|
||||
--picker-width: 95vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { mergeAttrs } from 'melt';
|
||||
import Filters from './Filters.svelte';
|
||||
import ExportImport from './ExportImport.svelte';
|
||||
import Theme from './Theme.svelte';
|
||||
|
||||
type TabCategories =
|
||||
| 'interface'
|
||||
@@ -27,7 +28,8 @@
|
||||
| 'engine'
|
||||
| 'account'
|
||||
| 'filters'
|
||||
| 'export';
|
||||
| 'export'
|
||||
| 'theme';
|
||||
|
||||
const tabCategories: Tabs<TabCategories> = new Tabs({
|
||||
value: 'interface',
|
||||
@@ -42,6 +44,7 @@
|
||||
|
||||
let tabs: { id: TabCategories; label: string; icon: string; component: Component }[] = $state([
|
||||
{ id: 'interface', label: $_('layout.interface'), icon: 'grid_view', component: Interface },
|
||||
{ id: 'theme', label: $_('layout.theme.theme'), icon: 'colors', component: Theme },
|
||||
{ id: 'player', label: $_('layout.player.title'), icon: 'smart_display', component: Player },
|
||||
{ id: 'filters', label: $_('layout.filter.title'), icon: 'filter_alt', component: Filters },
|
||||
{ id: 'ryd', label: 'Return YT Dislike', icon: 'thumb_down', component: Ryd },
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
getDynamicTheme,
|
||||
setAmoledTheme,
|
||||
setThemeColor,
|
||||
type ThemeColors,
|
||||
type ThemeKey
|
||||
} from '$lib/theme';
|
||||
import ui from 'beercss';
|
||||
import type { RgbaColor, HsvaColor, Colord } from 'colord';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
import {
|
||||
darkModeStore,
|
||||
interfaceAdvancedThemingStore,
|
||||
interfaceAmoledTheme,
|
||||
themeColorStore
|
||||
} from '../../store';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { titleCase } from '$lib/letterCasing';
|
||||
|
||||
let colorPickerOpen = $state(false);
|
||||
let colorPickerDebounce: ReturnType<typeof setTimeout>;
|
||||
|
||||
let currentThemeColors: ThemeColors | undefined = $state();
|
||||
|
||||
onMount(async () => {
|
||||
currentThemeColors = await getDynamicTheme();
|
||||
});
|
||||
|
||||
async function colorOnInput(
|
||||
color: {
|
||||
hsv: HsvaColor | null;
|
||||
rgb: RgbaColor | null;
|
||||
hex: string | null;
|
||||
color: Colord | null;
|
||||
},
|
||||
propetyKey: ThemeKey
|
||||
) {
|
||||
if (colorPickerDebounce) clearTimeout(colorPickerDebounce);
|
||||
|
||||
colorPickerDebounce = setTimeout(async () => {
|
||||
if (!color.hex) return;
|
||||
setThemeColor(propetyKey, color.hex);
|
||||
await tick();
|
||||
currentThemeColors = await getDynamicTheme();
|
||||
|
||||
interfaceAdvancedThemingStore.set(currentThemeColors);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function themeColorOnInput(color: {
|
||||
hsv: HsvaColor | null;
|
||||
rgb: RgbaColor | null;
|
||||
hex: string | null;
|
||||
color: Colord | null;
|
||||
}) {
|
||||
if (!color.hex) return;
|
||||
if (colorPickerDebounce) clearTimeout(colorPickerDebounce);
|
||||
|
||||
colorPickerDebounce = setTimeout(async () => {
|
||||
themeColorStore.set(color.hex);
|
||||
await tick();
|
||||
setAmoledTheme();
|
||||
currentThemeColors = await getDynamicTheme();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function toggleDarkMode() {
|
||||
const isDark = get(darkModeStore);
|
||||
|
||||
if (isDark) {
|
||||
ui('mode', 'light');
|
||||
darkModeStore.set(false);
|
||||
} else {
|
||||
ui('mode', 'dark');
|
||||
darkModeStore.set(true);
|
||||
}
|
||||
|
||||
currentThemeColors = await getDynamicTheme();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={toggleDarkMode} class="no-margin surface-container-highest">
|
||||
{#if !$darkModeStore}
|
||||
<i>dark_mode</i>
|
||||
<span>{$_('layout.theme.darkMode')}</span>
|
||||
{:else}
|
||||
<i>light_mode</i>
|
||||
<span>{$_('layout.theme.lightMode')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
<button class="surface-container-highest" onclick={() => (colorPickerOpen = !colorPickerOpen)}>
|
||||
<i>palette</i>
|
||||
<span>{$_('layout.theme.color')}</span>
|
||||
</button>
|
||||
|
||||
{#if colorPickerOpen}
|
||||
<div class="space"></div>
|
||||
|
||||
<div class="color-picker global-color-picker">
|
||||
<ColorPicker
|
||||
isTextInput={false}
|
||||
isDialog={false}
|
||||
onInput={themeColorOnInput}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
hex={get(themeColorStore)}
|
||||
sliderDirection="horizontal"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space"></div>
|
||||
|
||||
{#if $darkModeStore}
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.theme.AmoledTheme')}</div>
|
||||
</div>
|
||||
<label class="switch" tabindex="0">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$interfaceAmoledTheme}
|
||||
onclick={async () => {
|
||||
interfaceAmoledTheme.set(!$interfaceAmoledTheme);
|
||||
currentThemeColors = await getDynamicTheme();
|
||||
}}
|
||||
role="switch"
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h5>{$_('layout.theme.advanced')}</h5>
|
||||
<div class="space"></div>
|
||||
|
||||
{#if currentThemeColors}
|
||||
{#each Object.entries(currentThemeColors) as [themeVar, themeColor] (themeVar)}
|
||||
<div class="color-picker">
|
||||
<ColorPicker
|
||||
hex={themeColor}
|
||||
label={titleCase(themeVar.replaceAll('--', '').replaceAll('-', ' '))}
|
||||
position="responsive"
|
||||
textInputModes={['hex']}
|
||||
sliderDirection="vertical"
|
||||
onInput={async (color) => await colorOnInput(color, themeVar as ThemeKey)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.color-picker {
|
||||
--cp-bg-color: var(--surface-container);
|
||||
--cp-border-color: transparent;
|
||||
--cp-text-color: var(--on-surface);
|
||||
--cp-input-color: var(--surface);
|
||||
--cp-button-hover-color: var(--surface-variant);
|
||||
--slider-width: 30px;
|
||||
--picker-width: 350px;
|
||||
width: 100%;
|
||||
--input-size: 40px;
|
||||
}
|
||||
|
||||
.global-color-picker {
|
||||
--slider-width: 50px;
|
||||
--picker-width: 500px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.global-color-picker {
|
||||
--picker-width: 95vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -103,7 +103,7 @@ menu {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.color-picker .picker {
|
||||
.global-color-picker .picker {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -199,7 +199,8 @@
|
||||
"darkMode": "Dark mode",
|
||||
"lightMode": "Light mode",
|
||||
"AmoledTheme": "Amoled theme",
|
||||
"color": "Color"
|
||||
"color": "Color",
|
||||
"advanced": "Advanced theming"
|
||||
},
|
||||
"searchSuggestions": "Search suggestions",
|
||||
"previewVideoOnHover": "Preview video on hover",
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ensureNoTrailingSlash, getPublicEnv } from './misc';
|
||||
import type { EngineFallback } from './api/misc';
|
||||
import type z from 'zod';
|
||||
import type { zFilterSchema } from './filtering/index';
|
||||
import type { ThemeColors } from './theme';
|
||||
|
||||
function createListenerFunctions(): {
|
||||
callListeners: (eventKey: string, newValue: any) => void;
|
||||
@@ -260,6 +261,11 @@ export const interfaceAndroidUseNativeShare = persist(
|
||||
createStorage(),
|
||||
'androidUseNativeShare'
|
||||
);
|
||||
export const interfaceAdvancedThemingStore: Writable<ThemeColors> = persist(
|
||||
writable({}),
|
||||
createStorage(),
|
||||
'advancedTheming'
|
||||
);
|
||||
|
||||
export const sponsorBlockStore = persist(writable(true), createStorage(), 'sponsorBlock');
|
||||
export const sponsorBlockUrlStore: Writable<string | null | undefined> = persist(
|
||||
|
||||
@@ -4,16 +4,61 @@ import { get } from 'svelte/store';
|
||||
import { SystemBars, SystemBarsStyle } from '@capacitor/core';
|
||||
import { darkModeStore, interfaceAmoledTheme } from './store';
|
||||
|
||||
export async function getDynamicTheme(mode?: string): Promise<Record<string, string>> {
|
||||
export type ThemeKey =
|
||||
| '--primary'
|
||||
| '--on-primary'
|
||||
| '--primary-container'
|
||||
| '--on-primary-container'
|
||||
| '--secondary'
|
||||
| '--on-secondary'
|
||||
| '--secondary-container'
|
||||
| '--on-secondary-container'
|
||||
| '--tertiary'
|
||||
| '--on-tertiary'
|
||||
| '--tertiary-container'
|
||||
| '--on-tertiary-container'
|
||||
| '--error'
|
||||
| '--on-error'
|
||||
| '--error-container'
|
||||
| '--on-error-container'
|
||||
| '--background'
|
||||
| '--on-background'
|
||||
| '--surface'
|
||||
| '--on-surface'
|
||||
| '--surface-variant'
|
||||
| '--on-surface-variant'
|
||||
| '--outline'
|
||||
| '--outline-variant'
|
||||
| '--shadow'
|
||||
| '--scrim'
|
||||
| '--inverse-surface'
|
||||
| '--inverse-on-surface'
|
||||
| '--inverse-primary'
|
||||
| '--surface-dim'
|
||||
| '--surface-bright'
|
||||
| '--surface-container-lowest'
|
||||
| '--surface-container-low'
|
||||
| '--surface-container'
|
||||
| '--surface-container-high'
|
||||
| '--surface-container-highest';
|
||||
|
||||
export type ThemeColors = Partial<Record<ThemeKey, string>>;
|
||||
|
||||
export async function getDynamicTheme(mode?: string): Promise<ThemeColors> {
|
||||
const givenSettings = await ui('theme');
|
||||
|
||||
if (typeof givenSettings !== 'object') return {};
|
||||
|
||||
// @ts-expect-error Works as expected
|
||||
const themes: string = givenSettings[mode ? mode : (ui('mode') as string)];
|
||||
const themeVars: Record<string, string> = {};
|
||||
themes.split(';').forEach((keyVar) => {
|
||||
for (const keyVar of themes.split(';')) {
|
||||
const [key, value] = keyVar.split(':');
|
||||
themeVars[key] = value;
|
||||
});
|
||||
themeVars[key] = window.getComputedStyle(document.body).getPropertyValue(key) ?? value;
|
||||
}
|
||||
|
||||
delete themeVars[''];
|
||||
|
||||
return themeVars;
|
||||
}
|
||||
|
||||
@@ -35,7 +80,7 @@ export function setAmoledTheme() {
|
||||
const isDark = get(darkModeStore);
|
||||
|
||||
if (isAmoled && isDark) {
|
||||
const rootVars = [
|
||||
const rootVars: ThemeKey[] = [
|
||||
'--surface-container',
|
||||
'--surface',
|
||||
'--surface-container-lowest',
|
||||
@@ -44,7 +89,7 @@ export function setAmoledTheme() {
|
||||
'--surface-container-highest'
|
||||
];
|
||||
rootVars.forEach((varName) => {
|
||||
document.body.style.setProperty(varName, '#000');
|
||||
setThemeColor(varName, '#000');
|
||||
});
|
||||
} else {
|
||||
setTheme();
|
||||
@@ -70,3 +115,53 @@ export function setTheme() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const SUPPORTED_THEME_KEYS: ThemeKey[] = [
|
||||
'--primary',
|
||||
'--on-primary',
|
||||
'--primary-container',
|
||||
'--on-primary-container',
|
||||
'--secondary',
|
||||
'--on-secondary',
|
||||
'--secondary-container',
|
||||
'--on-secondary-container',
|
||||
'--tertiary',
|
||||
'--on-tertiary',
|
||||
'--tertiary-container',
|
||||
'--on-tertiary-container',
|
||||
'--error',
|
||||
'--on-error',
|
||||
'--error-container',
|
||||
'--on-error-container',
|
||||
'--background',
|
||||
'--on-background',
|
||||
'--surface',
|
||||
'--on-surface',
|
||||
'--surface-variant',
|
||||
'--on-surface-variant',
|
||||
'--outline',
|
||||
'--outline-variant',
|
||||
'--shadow',
|
||||
'--scrim',
|
||||
'--inverse-surface',
|
||||
'--inverse-on-surface',
|
||||
'--inverse-primary',
|
||||
'--surface-dim',
|
||||
'--surface-bright',
|
||||
'--surface-container-lowest',
|
||||
'--surface-container-low',
|
||||
'--surface-container',
|
||||
'--surface-container-high',
|
||||
'--surface-container-highest'
|
||||
] as const;
|
||||
|
||||
export function setThemeColors(theme: ThemeColors) {
|
||||
for (const [themeKey, themeValue] of Object.entries(theme)) {
|
||||
setThemeColor(themeKey as ThemeKey, themeValue.trim());
|
||||
}
|
||||
}
|
||||
|
||||
export function setThemeColor(theme: ThemeKey, color: string) {
|
||||
document.documentElement.style.setProperty(theme, color.trim());
|
||||
document.body.style.setProperty(theme, color.trim());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user