Skip to content

Commit

Permalink
Merge pull request #5348 from WoltLab/dark-mode
Browse files Browse the repository at this point in the history
Implement a dark mode for styles and the admin panel
  • Loading branch information
dtdesign committed Mar 9, 2023
2 parents 883d2ed + 931bbdc commit 572f95e
Show file tree
Hide file tree
Showing 61 changed files with 1,657 additions and 538 deletions.
26 changes: 18 additions & 8 deletions com.woltlab.wcf/templates/codemirror.tpl
Expand Up @@ -29,17 +29,23 @@
DomTraverse,
DomUtil,
) => {
const isDarkMode = window.getComputedStyle(document.documentElement).colorScheme === "dark";
const codemirrorCss = document.head.querySelector('link[href$="codemirror.css"]');
if (codemirrorCss === null) {
let link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '{@$__wcf->getPath()}js/3rdParty/codemirror/codemirror.css';
document.head.appendChild(link);
function addStylesheet(name) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `{@$__wcf->getPath()}js/3rdParty/codemirror/${ name }.css`;
document.head.append(link);
}
addStylesheet("codemirror");
addStylesheet("addon/dialog/dialog");
link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '{@$__wcf->getPath()}js/3rdParty/codemirror/addon/dialog/dialog.css';
document.head.appendChild(link);
if (isDarkMode) {
addStylesheet("theme/material-darker");
}
}
var config = {
Expand All @@ -60,6 +66,10 @@
indentUnit: 4,
readOnly: {if !$editable|isset || $editable}false{else}true{/if}
};
if (isDarkMode) {
config.theme = "material-darker";
}
document.querySelectorAll('{@$codemirrorSelector|encodeJS}').forEach((element) => {
{event name='javascriptInit'}
Expand Down
6 changes: 5 additions & 1 deletion com.woltlab.wcf/templates/documentHeader.tpl
@@ -1,2 +1,6 @@
<!DOCTYPE html>
<html dir="{lang}wcf.global.pageDirection{/lang}" lang="{$__wcf->language->getBcp47()}">
<html
dir="{lang}wcf.global.pageDirection{/lang}"
lang="{$__wcf->language->getBcp47()}"
data-color-scheme="{$__wcf->getStyleHandler()->getColorScheme()}"
>
15 changes: 15 additions & 0 deletions com.woltlab.wcf/templates/headIncludeJavaScript.tpl
Expand Up @@ -21,6 +21,20 @@
{* This constant is a compiler option, it does not exist in production. *}
var COMPILER_TARGET_DEFAULT = {if !VISITOR_USE_TINY_BUILD || $__wcf->user->userID}true{else}false{/if};
{/if}
{if $__wcf->getStyleHandler()->getStyle()->hasDarkMode}
{
let colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
try {
const value = localStorage.getItem("wsc_colorScheme");
if (value === "light" || value === "dark") {
colorScheme = value;
}
} catch {}
document.documentElement.dataset.colorScheme = colorScheme;
}
{/if}
</script>

<script src="{$__wcf->getPath()}js/WoltLabSuite/WebComponent.js?v={@LAST_UPDATE_TIME}"></script>
Expand Down Expand Up @@ -68,6 +82,7 @@ window.addEventListener('pageshow', function(event) {
url: '{link controller="BackgroundQueuePerform"}{/link}',
force: {if $forceBackgroundQueuePerform|isset}true{else}false{/if}
},
colorScheme: '{@$__wcf->getStyleHandler()->getColorScheme()|encodeJS}',
enableUserPopover: {if $__wcf->getSession()->getPermission('user.profile.canViewUserProfile')}true{else}false{/if},
executeCronjobs: {if $executeCronjobs}'{link controller="CronjobPerform"}{/link}'{else}undefined{/if},
{if ENABLE_SHARE_BUTTONS}
Expand Down
32 changes: 23 additions & 9 deletions com.woltlab.wcf/templates/pageFooter.tpl
Expand Up @@ -18,20 +18,34 @@
{/if}

{assign var=__boxesFooter value=$__wcf->getBoxHandler()->getBoxes('footer')}
{if $__wcf->getStyleHandler()->showStyleChanger() && $__wcf->getStyleHandler()->countStyles() > 1}
{if $__wcf->getStyleHandler()->showStyleChanger()}
{assign var=__showStyleChanger value=true}
{else}
{assign var=__showStyleChanger value=false}
{/if}
{if $__boxesFooter|count || !$boxesFooter|empty || $__showStyleChanger}

{if $__boxesFooter|count || !$boxesFooter|empty || $__showStyleChanger || $__wcf->getStyleHandler()->showColorSchemeSelector()}
<div class="boxesFooter">
<div class="layoutBoundary{if $__showStyleChanger} clearfix{/if}">
{if $__showStyleChanger}
<span class="styleChanger jsOnly">
<a href="#" class="jsButtonStyleChanger">{lang}wcf.style.changeStyle{/lang}</a>
</span>
{/if}
<div class="layoutBoundary{if $__showStyleChanger || $__wcf->getStyleHandler()->showColorSchemeSelector()} clearfix{/if}">
{hascontent}
<div class="styleChanger jsOnly">
{content}
{if $__showStyleChanger}
<button type="button" class="jsButtonStyleChanger">{lang}wcf.style.changeStyle{/lang}</button>
{/if}
{if $__wcf->getStyleHandler()->showColorSchemeSelector()}
<button type="button" class="page__colorScheme jsButtonStyleColorScheme jsTooltip" title="{lang}wcf.style.setColorScheme{/lang}">
<span class="page__colorScheme--dark">
{icon name='moon' type='solid'}
</span>
<span class="page__colorScheme--light">
{icon name='sun' type='solid'}
</span>
</button>
{/if}
{/content}
</div>
{/hascontent}
{hascontent}
<div class="boxContainer">
{content}
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/update_com.woltlab.wcf_6.0.sql
@@ -0,0 +1 @@
INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('individualScssDarkMode', '');
1 change: 1 addition & 0 deletions ts/WoltLabSuite/Core/Acp/Bootstrap.ts
Expand Up @@ -24,6 +24,7 @@ export function setup(options: AcpBootstrapOptions): void {
options = Core.extend(
{
bootstrap: {
colorScheme: "system",
enableMobileMenu: true,
pageMenuMainProvider: new AcpUiPageMenuMainBackend(),
},
Expand Down
37 changes: 37 additions & 0 deletions ts/WoltLabSuite/Core/Acp/Ui/Style/DarkMode.ts
@@ -0,0 +1,37 @@
/**
* Allows the addition of a dark mode to an existing style.
*
* @author Alexander Ebert
* @copyright 2001-2022 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @module WoltLabSuite/Core/Acp/Ui/Style/DarkMode
* @since 6.0
*/

import { prepareRequest } from "../../../Ajax/Backend";
import { confirmationFactory } from "../../../Component/Confirmation";
import { getPhrase } from "../../../Language";
import { show as showNotification } from "../../../Ui/Notification";

async function promptConfirmation(endpoint: string, question: string): Promise<void> {
const ok = await confirmationFactory().custom(question).message(getPhrase("wcf.dialog.confirmation.cannotBeUndone"));
if (ok) {
const response = await prepareRequest(endpoint).post().fetchAsResponse();
if (response?.ok) {
showNotification(undefined, () => {
window.location.reload();
});
}
}
}

function setupAddDarkMode(): void {
const button = document.querySelector<HTMLButtonElement>(".jsButtonAddDarkMode");
button?.addEventListener("click", () => {
void promptConfirmation(button.dataset.endpoint!, button.dataset.question!);
});
}

export function setup(): void {
setupAddDarkMode();
}
2 changes: 2 additions & 0 deletions ts/WoltLabSuite/Core/Acp/Ui/Style/Editor.ts
Expand Up @@ -11,6 +11,7 @@ import * as Core from "../../../Core";
import DomUtil from "../../../Dom/Util";
import * as EventHandler from "../../../Event/Handler";
import * as UiScreen from "../../../Ui/Screen";
import { setup as setupDarkMode } from "./DarkMode";

const _stylePreviewRegions = new Map<string, HTMLElement>();
let _stylePreviewRegionMarker: HTMLElement;
Expand Down Expand Up @@ -285,6 +286,7 @@ function initVisualEditor(styleRuleMap: StyleRuleMap): void {
export function setup(options: StyleEditorOptions): void {
handleLayoutWidth();
handleScss(options.isTainted);
setupDarkMode();

if (!options.isTainted) {
handleProtection(options.styleId);
Expand Down
12 changes: 10 additions & 2 deletions ts/WoltLabSuite/Core/Bootstrap.ts
Expand Up @@ -30,11 +30,13 @@ import * as UiObjectActionDelete from "./Ui/Object/Action/Delete";
import * as UiObjectActionToggle from "./Ui/Object/Action/Toggle";
import { init as initSearch } from "./Ui/Search";
import { PageMenuMainProvider } from "./Ui/Page/Menu/Main/Provider";
import { whenFirstSeen } from "./LazyLoader";
import { adoptPageOverlayContainer } from "./Helper/PageOverlay";

import type { ColorScheme } from "./Controller/Style/ColorScheme";

// perfectScrollbar does not need to be bound anywhere, it just has to be loaded for WCF.js
import "perfect-scrollbar";
import { whenFirstSeen } from "./LazyLoader";
import { adoptPageOverlayContainer } from "./Helper/PageOverlay";

// non strict equals by intent
if (window.WCF == null) {
Expand All @@ -50,6 +52,7 @@ window.WCF.Language.addObject = Language.addObject;
window.__wcf_bc_eventHandler = EventHandler;

export interface BoostrapOptions {
colorScheme: ColorScheme;
enableMobileMenu: boolean;
pageMenuMainProvider: PageMenuMainProvider;
}
Expand All @@ -74,6 +77,7 @@ function initA11y() {
export function setup(options: BoostrapOptions): void {
options = Core.extend(
{
colorScheme: "light",
enableMobileMenu: true,
pageMenuMainProvider: undefined,
},
Expand Down Expand Up @@ -141,6 +145,10 @@ export function setup(options: BoostrapOptions): void {

DomChangeListener.add("WoltLabSuite/Core/Bootstrap", () => initA11y);

if (options.colorScheme === "system") {
void import("./Controller/Style/ColorScheme").then(({ setup }) => setup());
}

whenFirstSeen("[data-report-content]", () => {
void import("./Ui/Moderation/Report").then(({ setup }) => setup());
});
Expand Down
4 changes: 4 additions & 0 deletions ts/WoltLabSuite/Core/BootstrapFrontend.ts
Expand Up @@ -20,11 +20,14 @@ import UiPageMenuMainFrontend from "./Ui/Page/Menu/Main/Frontend";
import { whenFirstSeen } from "./LazyLoader";
import { prepareRequest } from "./Ajax/Backend";

import type { ColorScheme } from "./Controller/Style/ColorScheme";

interface BootstrapOptions {
backgroundQueue: {
url: string;
force: boolean;
};
colorScheme: ColorScheme;
enableUserPopover: boolean;
executeCronjobs: string | undefined;
shareButtonProviders?: ShareProvider[];
Expand Down Expand Up @@ -60,6 +63,7 @@ export function setup(options: BootstrapOptions): void {
options.backgroundQueue.url = window.WSC_API_URL + options.backgroundQueue.url.substr(window.WCF_PATH.length);

Bootstrap.setup({
colorScheme: options.colorScheme,
enableMobileMenu: true,
pageMenuMainProvider: new UiPageMenuMainFrontend(),
});
Expand Down
96 changes: 96 additions & 0 deletions ts/WoltLabSuite/Core/Controller/Style/ColorScheme.ts
@@ -0,0 +1,96 @@
/**
* Offer users the ability to enforce a specific color scheme.
*
* @author Alexander Ebert
* @copyright 2001-2023 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.0
*/

import { getPhrase } from "WoltLabSuite/Core/Language";
import { attach, create } from "../../Ui/Dropdown/Builder";
import { registerCallback } from "../../Ui/Dropdown/Simple";

export type ColorScheme = "dark" | "light" | "system";

let currentScheme: ColorScheme = "system";
let mediaQuery: MediaQueryList;

function setScheme(scheme: ColorScheme): void {
currentScheme = scheme;

if (currentScheme === "light" || currentScheme === "dark") {
document.documentElement.dataset.colorScheme = currentScheme;
} else {
applySystemScheme();
}

try {
localStorage.setItem("wsc_colorScheme", currentScheme);
} catch {
/* Ignore any errors when accessing the `localStorage`. */
}
}

function applySystemScheme(): void {
if (currentScheme === "system") {
document.documentElement.dataset.colorScheme = mediaQuery.matches ? "dark" : "light";
}
}

function initializeButton(button: HTMLElement): void {
const dropdownMenu = create([
{
identifier: "light",
label: getPhrase("wcf.style.setColorScheme.light"),
callback() {
setScheme("light");
},
},
{
identifier: "dark",
label: getPhrase("wcf.style.setColorScheme.dark"),
callback() {
setScheme("dark");
},
},
"divider",
{
identifier: "system",
label: getPhrase("wcf.style.setColorScheme.system"),
callback() {
setScheme("system");
},
},
]);

attach(dropdownMenu, button);

registerCallback(button.id, (_containerId, action) => {
if (action === "open") {
dropdownMenu.querySelectorAll(".active").forEach((element) => element.classList.remove("active"));
dropdownMenu.querySelector(`[data-identifier="${currentScheme}"]`)!.classList.add("active");
}
});
}

export function setup(): void {
const button = document.querySelector<HTMLElement>(".jsButtonStyleColorScheme");
if (button) {
initializeButton(button);
}

try {
const value = localStorage.getItem("wsc_colorScheme");
if (value === "light" || value === "dark") {
currentScheme = value;
}
} catch {
/* Ignore any errors when accessing the `localStorage`. */
}

mediaQuery = matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", () => {
applySystemScheme();
});
}
10 changes: 10 additions & 0 deletions wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.0.php
Expand Up @@ -100,6 +100,16 @@
->columns([
EnumDatabaseTableColumn::create('apiVersion')
->drop(),
TinyintDatabaseTableColumn::create('hasDarkMode')
->defaultValue(0),
]),
PartialDatabaseTable::create('wcf1_style_variable')
->columns([
MediumtextDatabaseTableColumn::create('defaultValueDarkMode'),
]),
PartialDatabaseTable::create('wcf1_style_variable_value')
->columns([
MediumtextDatabaseTableColumn::create('variableValueDarkMode'),
]),
PartialDatabaseTable::create('wcf1_user_group_option')
->columns([
Expand Down

0 comments on commit 572f95e

Please sign in to comment.