From ebd911201df0e6fce95b408935fb35580851170e Mon Sep 17 00:00:00 2001 From: Kuuuube <61125188+Kuuuube@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:00:52 -0400 Subject: Add full dark mode support (#1079) * Add full dark mode support * Fix welcome, permissions, and quick start scrollbars * Fix action popup dark mode on mobile * Add theme to info page * Reduce flashbang * Move position of settingsDisplayController to not break things * Fix dictionary import drag drop theming * Make dark shadow color less bad * Prepare themeController to avoid not being able to set browser theme --- ext/css/action-popup.css | 63 +++++++++++++++------- ext/css/material.css | 2 +- ext/css/popup-preview.css | 11 ++-- ext/css/settings.css | 47 ++++++++++++++-- ext/js/pages/action-popup-main.js | 9 ++++ ext/js/pages/info-main.js | 12 +++++ ext/js/pages/permissions-main.js | 6 +-- ext/js/pages/quick-start-guide-main.js | 35 ++++++++++++ ext/js/pages/settings/popup-preview-frame.js | 28 +++------- .../pages/settings/settings-display-controller.js | 19 +++++++ ext/js/pages/settings/settings-main.js | 6 +-- ext/js/pages/welcome-main.js | 6 +-- ext/permissions.html | 2 +- ext/popup-preview.html | 3 -- ext/quick-start-guide.html | 3 +- ext/settings.html | 6 +-- ext/welcome.html | 4 +- 17 files changed, 197 insertions(+), 65 deletions(-) create mode 100644 ext/js/pages/quick-start-guide-main.js diff --git a/ext/css/action-popup.css b/ext/css/action-popup.css index e88aa3ee..5b68396f 100644 --- a/ext/css/action-popup.css +++ b/ext/css/action-popup.css @@ -27,6 +27,30 @@ --warning-color: #96751c; --warning-color-light: #edc75e; + + --nav-button-background-image: linear-gradient(#f8f8f8, #e0e0e0); + --nav-button-border-color: #cccccc; + --nav-button-background-image-focus: linear-gradient(#e8e8e8, #d0d0d0); + --nav-button-border-color-focus: #aaaaaa; + --nav-button-background-image-active-focus: linear-gradient(#c8c8c8, #e0e0e0); + --nav-button-border-color-active-focus: #808080; + --nav-button-icon-bg: #333333; + --background-color: #f8f9fa; + --text-color: #333333; + --svg-filter: 0; +} + +:root[data-theme=dark] { + --nav-button-background-image: linear-gradient(#464646, #444444); + --nav-button-border-color: #333333; + --nav-button-background-image-focus: linear-gradient(#171717, #2f2f2f); + --nav-button-border-color-focus: #555555; + --nav-button-background-image-active-focus: linear-gradient(#373737, #1f1f1f); + --nav-button-border-color-active-focus: #7f7f7f; + --nav-button-icon-bg: #fbfbfb; + --background-color: #1e1e1e; + --text-color: #cccccc; + --svg-filter: invert(100%); } body { @@ -36,6 +60,7 @@ body { font-size: var(--font-size); width: max-content; height: max-content; + background-color: var(--background-color); } h3 { @@ -44,7 +69,7 @@ h3 { font-weight: 500; line-height: 1.1; font-size: 24px; - color: inherit; + color: var(--text-color); } label { font-weight: normal; @@ -98,7 +123,7 @@ label { margin: 0 -10px; padding: 0.5em 10px; cursor: pointer; - color: #333; + color: var(--text-color); text-decoration: none; background-color: transparent; transition: background-color 0.125s linear 0s; @@ -109,7 +134,7 @@ label { } .link-group:hover, .link-group:active { - color: #333; + color: var(--text-color); text-decoration: none; } .link-group:hover>.link-group-label, @@ -136,11 +161,11 @@ label { margin: 0; padding: 0; } -.link-group-icon[data-icon=chevron] { background-image: url(/images/right-chevron.svg); } -.link-group-icon[data-icon=cog] { background-image: url(/images/cog.svg); } -.link-group-icon[data-icon=key] { background-image: url(/images/key.svg); } -.link-group-icon[data-icon=magnifying-glass] { background-image: url(/images/magnifying-glass.svg); } -.link-group-icon[data-icon=question-mark-circle] { background-image: url(/images/question-mark-circle.svg); } +.link-group-icon[data-icon=chevron] { background-image: url(/images/right-chevron.svg); filter: var(--svg-filter); } +.link-group-icon[data-icon=cog] { background-image: url(/images/cog.svg); filter: var(--svg-filter); } +.link-group-icon[data-icon=key] { background-image: url(/images/key.svg); filter: var(--svg-filter); } +.link-group-icon[data-icon=magnifying-glass] { background-image: url(/images/magnifying-glass.svg); filter: var(--svg-filter); } +.link-group-icon[data-icon=question-mark-circle] { background-image: url(/images/question-mark-circle.svg); filter: var(--svg-filter); } .link-group-label { vertical-align: middle; @@ -304,8 +329,8 @@ input[type=checkbox]~.toggle-group>.toggle-handle:active:focus { white-space: nowrap; } .nav-button { - background-image: linear-gradient(#f8f8f8, #e0e0e0); - border: 1px solid #cccccc; + background-image: var(--nav-button-background-image); + border: 1px solid var(--nav-button-border-color); margin: 0; padding: 2px 3px; cursor: pointer; @@ -317,27 +342,27 @@ input[type=checkbox]~.toggle-group>.toggle-handle:active:focus { .nav-button:hover, .nav-button:focus { z-index: 1; - border-color: #aaaaaa; - background-image: linear-gradient(#e8e8e8, #d0d0d0); + border-color: var(--nav-button-border-color-focus); + background-image: var(--nav-button-background-image-focus); box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15); outline: none; } .nav-button:focus:not(:focus-visible) { - background-image: linear-gradient(#f8f8f8, #e0e0e0); - border-color: #cccccc; + background-image: var(--nav-button-background-image); + border-color: var(--nav-button-border-color); box-shadow: none; } .nav-button:focus-visible { z-index: 1; - border-color: #aaaaaa; - background-image: linear-gradient(#e8e8e8, #d0d0d0); + border-color: var(--nav-button-border-color-focus); + background-image: var(--nav-button-background-image-focus); box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15); } .nav-button:active, .nav-button:active:focus { z-index: 1; - border-color: #808080; - background-image: linear-gradient(#c8c8c8, #e0e0e0); + border-color: var(--nav-button-border-color-active-focus); + background-image: var(--nav-button-background-image-active-focus); box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15); } .nav-button>.icon { @@ -346,7 +371,7 @@ input[type=checkbox]~.toggle-group>.toggle-handle:active:focus { width: 16px; height: 16px; box-sizing: content-box; - background-color: #333333; + background-color: var(--nav-button-icon-bg); } .nav-button:first-child, .nav-button:first-child[hidden]+.nav-button { diff --git a/ext/css/material.css b/ext/css/material.css index 18c72621..00265586 100644 --- a/ext/css/material.css +++ b/ext/css/material.css @@ -136,7 +136,7 @@ --background-color-light: #0a0a0a; --background-color-dark1: #333333; - --shadow-color: rgba(255, 255, 255, 0.185); + --shadow-color: rgba(105, 105, 105, 0.185); --shadow-color-off: rgba(255, 255, 255, 0); --shadow-color-light: rgba(255, 255, 255, 0.085); diff --git a/ext/css/popup-preview.css b/ext/css/popup-preview.css index 2f5a03a8..a72edc65 100644 --- a/ext/css/popup-preview.css +++ b/ext/css/popup-preview.css @@ -24,18 +24,23 @@ --line-height: calc(var(--line-height-no-units) / var(--font-size-no-units)); --animation-duration: 0s; + + --example-text-color: #333333; } :root[data-loaded=true] { --animation-duration: 0.25s; } +:root[data-theme=dark] { + --example-text-color: #d4d4d4; +} + + html { transition: background-color var(--animation-duration) linear 0s, color var(--animation-duration) linear 0s; background-color: rgba(255, 255, 255, 0); - color: #333333; } html.dark { - color: #d4d4d4; background-color: #1e1e1e; } html, @@ -92,7 +97,7 @@ body { margin: 0; padding: 0; outline: none; - color: inherit; + color: var(--example-text-color); background-color: transparent; white-space: pre; transition: background-color var(--animation-duration) linear 0s, border-color var(--animation-duration) linear 0s; diff --git a/ext/css/settings.css b/ext/css/settings.css index d23062b1..3c05906e 100644 --- a/ext/css/settings.css +++ b/ext/css/settings.css @@ -76,6 +76,15 @@ --modal-padding-vertical: 0.625em; --modal-padding-vertical-half: calc(var(--modal-padding-vertical) * 0.5); --modal-button-spacing: 0.625em; + + --scrollbar-thumb-color: #c1c1c1; + --scrollbar-track-color: #f1f1f1; + --scrollbar-inverse-thumb-color: #444444; + --scrollbar-inverse-track-color: #2f2f2f; + + --dictionary-import-border-color: #cccccc; + --dictionary-import-border-color-hover: #bfd1ff; + --dictionary-import-hover-background-color: rgba(28, 116, 233, 0.05); } :root:not([data-loaded=true]) { --animation-duration: 0s; @@ -83,6 +92,38 @@ :root[data-theme=dark] { --separator-color1: #333333; --separator-color2: #222222; + + --scrollbar-thumb-color: #444444; + --scrollbar-track-color: #2f2f2f; + --scrollbar-inverse-thumb-color: #c1c1c1; + --scrollbar-inverse-track-color: #f1f1f1; + + --dictionary-import-border-color: #333333; + --dictionary-import-border-color-hover: #1c74e9; + --dictionary-import-hover-background-color: rgba(191, 209, 255, 0.05); +} + +/* Scrollbars */ +:root:not([data-theme=light]) .scrollbar { + scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color); +} +:root:not([data-theme=light]) .scrollbar::-webkit-scrollbar { + width: auto; +} +:root:not([data-theme=light]) .scrollbar::-webkit-scrollbar-button { + height: 0; +} +:root:not([data-theme=light]) .scrollbar::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb-color); +} +:root:not([data-theme=light]) .scrollbar::-webkit-scrollbar-track { + background-color: var(--scrollbar-thumb-color); +} +:root:not([data-theme=light]) .scrollbar::-webkit-scrollbar-track-piece { + background-color: var(--scrollbar-track-color); +} +:root:not([data-theme=light]) .scrollbar::-webkit-scrollbar-corner { + background-color: var(--scrollbar-track-color); } @@ -2377,7 +2418,7 @@ input[type=number].dictionary-priority { #dictionary-drop-file-zone { transition: background-color var(--animation-duration) ease-in-out, border var(--animation-duration) ease-in-out; - border: 2px dashed rgb(204, 204, 204); + border: 2px dashed var(--dictionary-import-border-color); border-radius: 5px; flex: auto; min-height: 20em; @@ -2390,13 +2431,13 @@ input[type=number].dictionary-priority { } #dictionary-drop-file-zone:hover { - background-color: rgba(28, 116, 233, 0.05); + background-color: var(--dictionary-import-hover-background-color); border: 2px dashed var(--accent-color); } #dictionary-drop-file-zone.drag-over { border: 2px solid var(--accent-color); - background-color: rgb(191, 209, 255); + background-color: var(--dictionary-import-border-color-hover); } #dictionary-drag-drop-text { diff --git a/ext/js/pages/action-popup-main.js b/ext/js/pages/action-popup-main.js index cdf8f328..ce29bcf1 100644 --- a/ext/js/pages/action-popup-main.js +++ b/ext/js/pages/action-popup-main.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {ThemeController} from '../app/theme-controller.js'; import {Application} from '../application.js'; import {getAllPermissions, hasRequiredPermissionsForOptions} from '../data/permissions-util.js'; import {querySelectorNotNull} from '../dom/query-selector.js'; @@ -30,10 +31,15 @@ class DisplayController { this._api = api; /** @type {?import('settings').Options} */ this._optionsFull = null; + /** @type {ThemeController} */ + this._themeController = new ThemeController(document.documentElement); } /** */ async prepare() { + this._themeController.siteTheme = 'light'; + this._themeController.prepare(); + const manifest = chrome.runtime.getManifest(); this._showExtensionInfo(manifest); @@ -201,6 +207,9 @@ class DisplayController { } void this._updateDictionariesEnabledWarnings(options); void this._updatePermissionsWarnings(options); + + this._themeController.theme = options.general.popupTheme; + this._themeController.updateTheme(); } /** */ diff --git a/ext/js/pages/info-main.js b/ext/js/pages/info-main.js index c0e37261..f431239f 100644 --- a/ext/js/pages/info-main.js +++ b/ext/js/pages/info-main.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {ThemeController} from '../app/theme-controller.js'; import {Application} from '../application.js'; import {promiseTimeout} from '../core/utilities.js'; import {DocumentFocusController} from '../dom/document-focus-controller.js'; @@ -154,6 +155,17 @@ await Application.main(true, async (application) => { const settingsController = new SettingsController(application); await settingsController.prepare(); + /** @type {ThemeController} */ + const themeController = new ThemeController(document.documentElement); + themeController.prepare(); + const optionsFull = await application.api.optionsGetFull(); + const {profiles, profileCurrent} = optionsFull; + const primaryProfile = (profileCurrent >= 0 && profileCurrent < profiles.length) ? profiles[profileCurrent] : null; + if (primaryProfile !== null) { + themeController.theme = primaryProfile.options.general.popupTheme; + themeController.updateTheme(); + } + const backupController = new BackupController(settingsController, null); await backupController.prepare(); diff --git a/ext/js/pages/permissions-main.js b/ext/js/pages/permissions-main.js index fd92987d..d27f2d79 100644 --- a/ext/js/pages/permissions-main.js +++ b/ext/js/pages/permissions-main.js @@ -130,10 +130,10 @@ await Application.main(true, async (application) => { const persistentStorageController = new PersistentStorageController(application); void persistentStorageController.prepare(); + const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); + settingsDisplayController.prepare(); + await promiseTimeout(100); document.documentElement.dataset.loaded = 'true'; - - const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); - settingsDisplayController.prepare(); }); diff --git a/ext/js/pages/quick-start-guide-main.js b/ext/js/pages/quick-start-guide-main.js new file mode 100644 index 00000000..41644f51 --- /dev/null +++ b/ext/js/pages/quick-start-guide-main.js @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import {ThemeController} from '../app/theme-controller.js'; +import {Application} from '../application.js'; +import {SettingsController} from './settings/settings-controller.js'; + +await Application.main(true, async (application) => { + const settingsController = new SettingsController(application); + await settingsController.prepare(); + /** @type {ThemeController} */ + const themeController = new ThemeController(document.documentElement); + themeController.prepare(); + const optionsFull = await application.api.optionsGetFull(); + const {profiles, profileCurrent} = optionsFull; + const primaryProfile = (profileCurrent >= 0 && profileCurrent < profiles.length) ? profiles[profileCurrent] : null; + if (primaryProfile !== null) { + themeController.theme = primaryProfile.options.general.popupTheme; + themeController.updateTheme(); + } +}); diff --git a/ext/js/pages/settings/popup-preview-frame.js b/ext/js/pages/settings/popup-preview-frame.js index 37aca898..7abcfe2a 100644 --- a/ext/js/pages/settings/popup-preview-frame.js +++ b/ext/js/pages/settings/popup-preview-frame.js @@ -18,6 +18,7 @@ import * as wanakana from '../../../lib/wanakana.js'; import {Frontend} from '../../app/frontend.js'; +import {ThemeController} from '../../app/theme-controller.js'; import {createApiMap, invokeApiMapHandler} from '../../core/api-map.js'; import {querySelectorNotNull} from '../../dom/query-selector.js'; import {TextSourceRange} from '../../dom/text-source-range.js'; @@ -57,6 +58,8 @@ export class PopupPreviewFrame { this._languageSummaries = []; /** @type {boolean} */ this._wanakanaBound = false; + /** @type {ThemeController} */ + this._themeController = new ThemeController(document.documentElement); /* eslint-disable @stylistic/no-multi-spaces */ /** @type {import('popup-preview-frame').ApiMap} */ @@ -74,10 +77,10 @@ export class PopupPreviewFrame { async prepare() { window.addEventListener('message', this._onMessage.bind(this), false); + this._themeController.siteTheme = 'light'; + this._themeController.prepare(); + // Setup events - /** @type {HTMLInputElement} */ - const darkThemeCheckbox = querySelectorNotNull(document, '#theme-dark-checkbox'); - darkThemeCheckbox.addEventListener('change', this._onThemeDarkCheckboxChanged.bind(this), false); this._exampleText.addEventListener('click', this._onExampleTextClick.bind(this), false); this._exampleTextInput.addEventListener('blur', this._onExampleTextInputBlur.bind(this), false); this._exampleTextInput.addEventListener('input', this._onExampleTextInputInput.bind(this), false); @@ -137,6 +140,8 @@ export class PopupPreviewFrame { options.general.popupHorizontalTextPosition = 'below'; options.general.popupVerticalTextPosition = 'before'; options.scanning.selectText = false; + this._themeController.theme = options.general.popupTheme; + this._themeController.updateTheme(); return options; } @@ -165,23 +170,6 @@ export class PopupPreviewFrame { invokeApiMapHandler(this._windowMessageHandlers, action, params, [], callback); } - /** - * @param {Event} e - */ - _onThemeDarkCheckboxChanged(e) { - const element = /** @type {HTMLInputElement} */ (e.currentTarget); - document.documentElement.classList.toggle('dark', element.checked); - if (this._themeChangeTimeout !== null) { - clearTimeout(this._themeChangeTimeout); - } - this._themeChangeTimeout = setTimeout(() => { - this._themeChangeTimeout = null; - const popup = /** @type {Frontend} */ (this._frontend).popup; - if (popup === null) { return; } - void popup.updateTheme(); - }, 300); - } - /** */ _onExampleTextClick() { if (this._exampleTextInput === null) { return; } diff --git a/ext/js/pages/settings/settings-display-controller.js b/ext/js/pages/settings/settings-display-controller.js index f732f1ef..49f7192c 100644 --- a/ext/js/pages/settings/settings-display-controller.js +++ b/ext/js/pages/settings/settings-display-controller.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {ThemeController} from '../../app/theme-controller.js'; import {isInputElementFocused} from '../../dom/document-util.js'; import {PopupMenu} from '../../dom/popup-menu.js'; import {querySelectorNotNull} from '../../dom/query-selector.js'; @@ -39,10 +40,16 @@ export class SettingsDisplayController { this._onMoreToggleClickBind = this._onMoreToggleClick.bind(this); /** @type {(event: MouseEvent) => void} */ this._onMenuButtonClickBind = this._onMenuButtonClick.bind(this); + /** @type {ThemeController} */ + this._themeController = new ThemeController(document.documentElement); } /** */ prepare() { + this._themeController.siteTheme = 'light'; + this._themeController.prepare(); + void this._updateTheme(); + const onFabButtonClick = this._onFabButtonClick.bind(this); for (const fabButton of /** @type {NodeListOf} */ (document.querySelectorAll('.fab-button'))) { fabButton.addEventListener('click', onFabButtonClick, false); @@ -84,6 +91,18 @@ export class SettingsDisplayController { window.addEventListener('keydown', this._onKeyDown.bind(this), false); window.addEventListener('popstate', this._onPopState.bind(this), false); this._updateScrollTarget(); + + const themeDropdown = document.querySelector('[data-setting="general.popupTheme"]'); + if (themeDropdown) { + themeDropdown.addEventListener('change', this._updateTheme.bind(this), false); + } + } + + /** */ + async _updateTheme() { + const options = await this._settingsController.getOptions(); + this._themeController.theme = options.general.popupTheme; + this._themeController.updateTheme(); } // Private diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js index 3ff818ac..a315eac4 100644 --- a/ext/js/pages/settings/settings-main.js +++ b/ext/js/pages/settings/settings-main.js @@ -169,10 +169,10 @@ await Application.main(true, async (application) => { const sortFrequencyDictionaryController = new SortFrequencyDictionaryController(settingsController); preparePromises.push(sortFrequencyDictionaryController.prepare()); + const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); + settingsDisplayController.prepare(); + await Promise.all(preparePromises); document.documentElement.dataset.loaded = 'true'; - - const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); - settingsDisplayController.prepare(); }); diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js index 10a84a59..56a7e7d8 100644 --- a/ext/js/pages/welcome-main.js +++ b/ext/js/pages/welcome-main.js @@ -92,10 +92,10 @@ await Application.main(true, async (application) => { const languagesController = new LanguagesController(settingsController); preparePromises.push(languagesController.prepare()); + const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); + settingsDisplayController.prepare(); + await Promise.all(preparePromises); document.documentElement.dataset.loaded = 'true'; - - const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); - settingsDisplayController.prepare(); }); diff --git a/ext/permissions.html b/ext/permissions.html index de5fb4ad..31f30186 100644 --- a/ext/permissions.html +++ b/ext/permissions.html @@ -19,7 +19,7 @@ -
+
diff --git a/ext/popup-preview.html b/ext/popup-preview.html index 8cbb01c9..3da26eda 100644 --- a/ext/popup-preview.html +++ b/ext/popup-preview.html @@ -25,9 +25,6 @@
-
- -