Make theme switching browser-based and instant.

This commit is contained in:
Buster Neece 2023-06-26 05:33:20 -05:00
parent 5a73a41024
commit de656fbaa2
No known key found for this signature in database
18 changed files with 144 additions and 176 deletions

View File

@ -39,9 +39,6 @@ return static function (RouteCollectorProxy $app) {
$group->get('/profile', Controller\Frontend\Profile\IndexAction::class)
->setName('profile:index');
$group->get('/profile/theme', Controller\Frontend\Profile\ThemeAction::class)
->setName('profile:theme');
}
)->add(Middleware\EnableView::class)
->add(Middleware\RequireLogin::class);

62
frontend/js/inc/theme.js Normal file
View File

@ -0,0 +1,62 @@
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
'use strict'
const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)
const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = theme => {
document.documentElement.setAttribute('data-theme', theme)
document.documentElement.dispatchEvent(new CustomEvent(
"theme-change",
{
detail: theme
}
));
}
const currentTheme = document.documentElement.getAttribute('data-theme');
if (currentTheme !== 'light' && currentTheme !== 'dark') {
setTheme(getPreferredTheme())
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
}
})
window.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.theme-switcher').forEach(
toggle => {
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const currentTheme = getPreferredTheme();
if (currentTheme === 'light') {
setStoredTheme('dark');
setTheme('dark');
} else {
setStoredTheme('light');
setTheme('light');
}
});
});
})
})()

View File

@ -1,10 +1,6 @@
@at-root {
$theme: 'light';
@import 'common';
}
@at-root {
$theme: 'light';
@import 'common-colors';
}
@ -12,10 +8,3 @@
$theme: 'dark';
@import 'common-colors';
}
@media screen and (prefers-color-scheme: dark) {
[data-theme="browser"] {
$theme: 'dark';
@import 'common-colors';
}
}

View File

@ -48,23 +48,6 @@
</b-wrapped-form-group>
</b-col>
<b-col md="6">
<b-wrapped-form-group
id="edit_form_theme"
:field="form.theme"
>
<template #label>
{{ $gettext('Site Theme') }}
</template>
<template #default="slotProps">
<b-form-radio-group
:id="slotProps.id"
v-model="slotProps.field.$model"
stacked
:options="themeOptions"
/>
</template>
</b-wrapped-form-group>
<b-wrapped-form-group
id="edit_form_show_24_hour_time"
:field="form.show_24_hour_time"
@ -115,23 +98,6 @@ const localeOptions = computed(() => {
return localeOptions;
});
const themeOptions = computed(() => {
return [
{
text: $gettext('Prefer System Default'),
value: 'browser'
},
{
text: $gettext('Light'),
value: 'light'
},
{
text: $gettext('Dark'),
value: 'dark'
}
];
});
const show24hourOptions = computed(() => {
return [
{

View File

@ -46,14 +46,12 @@ const {form, resetForm, v$, ifValid} = useVuelidateOnForm(
name: {},
email: {required, email},
locale: {required},
theme: {required},
show_24_hour_time: {}
},
{
name: '',
email: '',
locale: 'default',
theme: 'browser',
show_24_hour_time: null,
}
);

View File

@ -3,7 +3,7 @@
v-model="textValue"
basic
:lang="lang"
:dark="dark"
:dark="isDark"
/>
</template>
@ -13,7 +13,7 @@ import {useVModel} from "@vueuse/core";
import {computed} from "vue";
import {css} from "@codemirror/lang-css";
import {javascript} from "@codemirror/lang-javascript";
import {useAzuraCast} from "~/vendor/azuracast";
import useGetTheme from "~/functions/useGetTheme";
const props = defineProps({
modelValue: {
@ -39,11 +39,7 @@ const lang = computed(() => {
return null;
});
const {theme} = useAzuraCast();
const dark = computed(() => {
return theme === 'dark';
})
const {isDark} = useGetTheme();
</script>
<script>

View File

@ -11,9 +11,9 @@
</template>
<script setup>
import {onMounted, provide, ref, shallowRef} from "vue";
import {onMounted, provide, ref, shallowRef, watch} from "vue";
import L from "~/vendor/leaflet";
import {useAzuraCast} from "~/vendor/azuracast";
import useGetTheme from "~/functions/useGetTheme";
const props = defineProps({
attribution: {
@ -27,7 +27,7 @@ const $map = shallowRef();
provide('map', $map);
const {theme} = useAzuraCast();
const {theme} = useGetTheme();
onMounted(() => {
// Init map
@ -41,10 +41,17 @@ onMounted(() => {
const tileAttribution = 'Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL.';
L.tileLayer(tileUrl, {
theme: theme,
theme: theme.value,
attribution: tileAttribution,
}).addTo(map);
watch(theme, (newTheme) => {
L.tileLayer(tileUrl, {
theme: newTheme,
attribution: tileAttribution,
}).addTo(map);
});
/*
// Add fullscreen control
const fullscreenControl = new L.Control.Fullscreen();

View File

@ -0,0 +1,24 @@
import {computed, onMounted, ref} from "vue";
import {useEventListener} from "@vueuse/core";
export default function useGetTheme() {
const htmlElement = document.documentElement;
const theme = ref(null);
onMounted(() => {
theme.value = htmlElement.getAttribute('data-theme');
});
useEventListener(htmlElement, 'theme-change', (evt) => {
theme.value = evt.detail;
});
const isDark = computed(() => theme.value === 'dark');
const isLight = computed(() => theme.value === 'light');
return {
theme,
isDark,
isLight
};
}

View File

@ -10,7 +10,6 @@ export function useAzuraCast() {
localeShort: App.locale_short ?? 'en',
localeWithDashes: App.locale_with_dashes ?? 'en-US',
timeConfig: App.time_config ?? {},
apiCsrf: App.api_csrf ?? null,
theme: App.theme ?? 'light'
apiCsrf: App.api_csrf ?? null
}
}

View File

@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Frontend\Profile;
use App\Container\EntityManagerAwareTrait;
use App\Controller\SingleActionInterface;
use App\Enums\SupportedThemes;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
final class ThemeAction implements SingleActionInterface
{
use EntityManagerAwareTrait;
public function __invoke(
ServerRequest $request,
Response $response,
array $params
): ResponseInterface {
$user = $request->getUser();
$currentTheme = $user->getTheme();
$newTheme = match ($currentTheme) {
SupportedThemes::Dark => SupportedThemes::Light,
default => SupportedThemes::Dark
};
$user->setTheme($newTheme);
$this->em->persist($user);
$this->em->flush();
$referrer = $request->getHeaderLine('Referer');
return $response->withRedirect(
$referrer ?: $request->getRouter()->named('dashboard')
);
}
}

View File

@ -28,9 +28,7 @@ final class Customization
private SupportedLocales $locale;
private SupportedThemes $theme;
private SupportedThemes $publicTheme;
private ?SupportedThemes $publicTheme;
private string $instanceName;
@ -42,7 +40,6 @@ final class Customization
$this->instanceName = $this->settings->getInstanceName() ?? '';
$this->user = null;
$this->theme = SupportedThemes::default();
$this->publicTheme = $this->settings->getPublicTheme();
$this->locale = SupportedLocales::default();
@ -57,51 +54,24 @@ final class Customization
$this->user = $request->getAttribute(ServerRequest::ATTR_USER);
// Register current theme
$this->theme = $this->determineTheme($request);
$this->publicTheme = $this->determineTheme($request, true);
$queryParams = $request->getQueryParams();
if (!empty($queryParams['theme'])) {
$theme = SupportedThemes::tryFrom($queryParams['theme']);
if (null !== $theme && $theme !== SupportedThemes::Browser) {
$this->publicTheme = $theme;
}
}
// Register locale
$this->locale = SupportedLocales::createFromRequest($this->environment, $request);
}
}
private function determineTheme(
ServerRequestInterface $request,
bool $isPublicTheme = false
): SupportedThemes {
$queryParams = $request->getQueryParams();
if (!empty($queryParams['theme'])) {
$theme = SupportedThemes::tryFrom($queryParams['theme']);
if (null !== $theme) {
return $theme;
}
}
if (null !== $this->user) {
$userTheme = $this->user->getTheme();
if (null !== $userTheme) {
return $userTheme;
}
}
return ($isPublicTheme)
? $this->settings->getPublicTheme()
: SupportedThemes::default();
}
public function getLocale(): SupportedLocales
{
return $this->locale;
}
/**
* Returns the user-customized or system default theme.
*/
public function getTheme(): SupportedThemes
{
return $this->theme;
}
/**
* Get the instance name for this AzuraCast instance.
*/
@ -113,9 +83,11 @@ final class Customization
/**
* Get the theme name to be used in public (non-logged-in) pages.
*/
public function getPublicTheme(): SupportedThemes
public function getPublicTheme(): ?SupportedThemes
{
return $this->publicTheme;
return (SupportedThemes::Browser !== $this->publicTheme)
? $this->publicTheme
: null;
}
/**

View File

@ -6,7 +6,6 @@ namespace App\Entity\Fixture;
use App\Entity\Role;
use App\Entity\User;
use App\Enums\SupportedThemes;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
@ -36,7 +35,6 @@ final class UserFixture extends AbstractFixture implements DependentFixtureInter
$adminUser->setEmail($adminEmail);
$adminUser->setName('System Administrator');
$adminUser->setNewPassword($adminPassword);
$adminUser->setTheme(SupportedThemes::Browser);
/** @var Role $adminRole */
$adminRole = $this->getReference('admin_role');

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Entity\Migration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20230626102616 extends AbstractMigration
{
public function getDescription(): string
{
return 'Store theme at the browser level, not the user level.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE users DROP theme');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users ADD theme VARCHAR(25) DEFAULT NULL');
}
}

View File

@ -316,9 +316,9 @@ class Settings implements Stringable
]
protected ?SupportedThemes $public_theme = null;
public function getPublicTheme(): SupportedThemes
public function getPublicTheme(): ?SupportedThemes
{
return $this->public_theme ?? SupportedThemes::default();
return $this->public_theme;
}
public function setPublicTheme(?SupportedThemes $publicTheme): void

View File

@ -7,7 +7,6 @@ namespace App\Entity;
use App\Auth;
use App\Entity\Interfaces\EntityGroupsInterface;
use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Enums\SupportedThemes;
use App\Normalizer\Attributes\DeepNormalize;
use App\OpenApi;
use App\Utilities\Strings;
@ -73,14 +72,6 @@ class User implements Stringable, IdentifiableEntityInterface
]
protected ?string $locale = null;
#[
OA\Property(example: "dark"),
ORM\Column(type: 'string', length: 25, nullable: true, enumType: SupportedThemes::class),
Attributes\AuditIgnore,
Groups([EntityGroupsInterface::GROUP_GENERAL, EntityGroupsInterface::GROUP_ALL])
]
protected ?SupportedThemes $theme = null;
#[
OA\Property(example: true),
ORM\Column(nullable: true),
@ -225,16 +216,6 @@ class User implements Stringable, IdentifiableEntityInterface
$this->locale = $locale;
}
public function getTheme(): ?SupportedThemes
{
return $this->theme;
}
public function setTheme(?SupportedThemes $theme = null): void
{
$this->theme = $theme;
}
public function getShow24HourTime(): ?bool
{
return $this->show_24_hour_time;

View File

@ -18,7 +18,7 @@ $title ??= null;
$header ??= null;
?>
<!DOCTYPE html>
<html lang="<?= $customization->getLocale()->getHtmlLang() ?>" data-theme="<?= $customization->getTheme()->value ?>">
<html lang="<?= $customization->getLocale()->getHtmlLang() ?>">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@ -115,7 +115,7 @@ endif; ?>">
</a>
</li>
<li>
<a class="dropdown-item" href="<?= $router->named('profile:theme') ?>">
<a class="dropdown-item theme-switcher" href="javascript:">
<i class="material-icons" aria-hidden="true">invert_colors</i>
<?= __('Switch Theme') ?>
</a>

View File

@ -17,7 +17,7 @@ $hide_footer ??= false;
?>
<!DOCTYPE html>
<html lang="<?= $customization->getLocale()->getHtmlLang() ?>"
data-theme="<?= $customization->getPublicTheme()->value ?>">
data-theme="<?= $customization->getPublicTheme()?->value ?>">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -55,11 +55,4 @@ $app = [
<?php endif; ?>
var App = <?=json_encode($app, JSON_THROW_ON_ERROR) ?>;
let currentTheme = document.documentElement.getAttribute('data-theme');
if (currentTheme === 'browser') {
currentTheme = (window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
}
App.theme = currentTheme;
</script>