Bläddra i källkod

feat: option for ui theme auto/dark/light

tophf 4 år sedan
förälder
incheckning
07233c9b61

+ 5 - 0
.postcssrc.js

@@ -0,0 +1,5 @@
+const cfg = require('@gera2ld/plaid/postcss/precss')({});
+cfg.plugins[1] = require('precss')({
+  features: { 'prefers-color-scheme-query': false },
+});
+module.exports = cfg;

+ 3 - 1
package.json

@@ -40,11 +40,13 @@
     "fancy-log": "^1.3.2",
     "fancy-log": "^1.3.2",
     "gulp": "^4.0.2",
     "gulp": "^4.0.2",
     "gulp-plumber": "^1.1.0",
     "gulp-plumber": "^1.1.0",
+    "html-inline-css-webpack-plugin": "^1.11.1",
     "husky": "^4.2.3",
     "husky": "^4.2.3",
     "js-yaml": "^3.13.1",
     "js-yaml": "^3.13.1",
     "jsdom": "^16.2.1",
     "jsdom": "^16.2.1",
     "npm-run-all": "^4.1.5",
     "npm-run-all": "^4.1.5",
     "plugin-error": "^1.0.0",
     "plugin-error": "^1.0.0",
+    "postcss-combine-media-query": "^1.0.1",
     "sharp": "^0.26.2",
     "sharp": "^0.26.2",
     "sign-addon": "^3.1.0",
     "sign-addon": "^3.1.0",
     "tape": "^4.13.2",
     "tape": "^4.13.2",
@@ -81,4 +83,4 @@
     }
     }
   },
   },
   "beta": 8
   "beta": 8
-}
+}

+ 43 - 1
scripts/plaid.conf.js

@@ -1,4 +1,6 @@
 const { isProd } = require('@gera2ld/plaid/util');
 const { isProd } = require('@gera2ld/plaid/util');
+const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+const TerserPlugin = isProd && require('terser-webpack-plugin');
 
 
 /**
 /**
  * For each entry, `key` is the chunk name, `value` has following properties:
  * For each entry, `key` is the chunk name, `value` has following properties:
@@ -7,7 +9,6 @@ const { isProd } = require('@gera2ld/plaid/util');
  * - value.html.inlineSource: if true, JS and CSS files will be inlined in HTML.
  * - value.html.inlineSource: if true, JS and CSS files will be inlined in HTML.
  */
  */
 exports.pages = [
 exports.pages = [
-  'background',
   'confirm',
   'confirm',
   'options',
   'options',
   'popup',
   'popup',
@@ -22,6 +23,17 @@ exports.pages = [
   },
   },
 }), {});
 }), {});
 
 
+const minimizerOptions = {
+  cache: true,
+  parallel: true,
+  sourceMap: true,
+  terserOptions: {
+    output: {
+      ascii_only: true,
+    },
+  },
+};
+
 const splitVendor = prefix => ({
 const splitVendor = prefix => ({
   [prefix]: {
   [prefix]: {
     test: new RegExp(`node_modules[/\\\\]${prefix}.*?\\.js`),
     test: new RegExp(`node_modules[/\\\\]${prefix}.*?\\.js`),
@@ -48,4 +60,34 @@ exports.optimization = {
       ...splitVendor('vue'),
       ...splitVendor('vue'),
     },
     },
   },
   },
+  minimizer: isProd && [
+    // apply `postcss-combine-media-query`
+    new OptimizeCssAssetsPlugin({
+      cssProcessor: require('postcss')([
+        require('postcss-combine-media-query'),
+        require('cssnano'),
+      ]),
+    }),
+    // Minifying Violentmonkey code
+    new TerserPlugin({
+      chunkFilter: ({ name }) => !name.startsWith('public/'),
+      ...minimizerOptions,
+      terserOptions: {
+        ...minimizerOptions.terserOptions,
+        compress: {
+          ecma: 6,
+          // 'safe' since we don't rely on function prototypes
+          unsafe_arrows: true,
+        },
+      },
+    }),
+    // Minifying non-Violentmonkey code
+    new TerserPlugin({
+      chunkFilter: ({ name }) => name.startsWith('public/'),
+      ...minimizerOptions,
+    }),
+  ],
+};
+exports.styleOptions = {
+  extract: true, // Will be embedded as <style> to ensure uiTheme option doesn't cause FOUC
 };
 };

+ 35 - 39
scripts/webpack.conf.js

@@ -1,8 +1,9 @@
 const { modifyWebpackConfig, shallowMerge, defaultOptions } = require('@gera2ld/plaid');
 const { modifyWebpackConfig, shallowMerge, defaultOptions } = require('@gera2ld/plaid');
 const { isProd } = require('@gera2ld/plaid/util');
 const { isProd } = require('@gera2ld/plaid/util');
+const fs = require('fs');
 const webpack = require('webpack');
 const webpack = require('webpack');
 const WrapperWebpackPlugin = require('wrapper-webpack-plugin');
 const WrapperWebpackPlugin = require('wrapper-webpack-plugin');
-const TerserPlugin = require('terser-webpack-plugin');
+const HTMLInlineCSSWebpackPlugin = require('html-inline-css-webpack-plugin').default;
 const projectConfig = require('./plaid.conf');
 const projectConfig = require('./plaid.conf');
 const mergedConfig = shallowMerge(defaultOptions, projectConfig);
 const mergedConfig = shallowMerge(defaultOptions, projectConfig);
 
 
@@ -30,34 +31,6 @@ const definitions = new webpack.DefinePlugin({
   ]),
   ]),
   'process.env.INIT_FUNC_NAME': JSON.stringify(INIT_FUNC_NAME),
   'process.env.INIT_FUNC_NAME': JSON.stringify(INIT_FUNC_NAME),
 });
 });
-const minimizerOptions = {
-  cache: true,
-  parallel: true,
-  sourceMap: true,
-  terserOptions: {
-    output: {
-      ascii_only: true,
-    },
-  },
-};
-const minimizer = isProd && [
-  new TerserPlugin({
-    chunkFilter: ({ name }) => !name.startsWith('public/'),
-    ...minimizerOptions,
-    terserOptions: {
-      ...minimizerOptions.terserOptions,
-      compress: {
-        ecma: 6,
-        // 'safe' since we don't rely on function prototypes
-        unsafe_arrows: true,
-      },
-    },
-  }),
-  new TerserPlugin({
-    chunkFilter: ({ name }) => name.startsWith('public/'),
-    ...minimizerOptions,
-  }),
-];
 
 
 const modify = (page, entry, init) => modifyWebpackConfig(
 const modify = (page, entry, init) => modifyWebpackConfig(
   (config) => {
   (config) => {
@@ -69,10 +42,6 @@ const modify = (page, entry, init) => modifyWebpackConfig(
     projectConfig: {
     projectConfig: {
       ...mergedConfig,
       ...mergedConfig,
       ...entry && { pages: { [page]: { entry }} },
       ...entry && { pages: { [page]: { entry }} },
-      optimization: {
-        ...mergedConfig.optimization,
-        minimizer,
-      },
     },
     },
   },
   },
 );
 );
@@ -85,16 +54,43 @@ const [globalsCommonHeader, globalsInjectedHeader] = [
   './src/injected/safe-injected-globals.js',
   './src/injected/safe-injected-globals.js',
 ].map(path =>
 ].map(path =>
   require('fs').readFileSync(path, {encoding: 'utf8'}).replace(/export const/g, 'const'));
   require('fs').readFileSync(path, {encoding: 'utf8'}).replace(/export const/g, 'const'));
+const globalWrapper = new WrapperWebpackPlugin({
+  header: `{ ${globalsCommonHeader}`,
+  footer: `}`,
+  test: /^(?!injected|public).*\.js$/,
+});
 
 
 module.exports = Promise.all([
 module.exports = Promise.all([
   modify((config) => {
   modify((config) => {
     config.output.publicPath = '/';
     config.output.publicPath = '/';
-    config.plugins.push(
-      new WrapperWebpackPlugin({
-        header: `{ ${globalsCommonHeader}`,
-        footer: `}`,
-        test: /^(?!injected|public).*\.js$/,
-      }));
+    config.plugins.push(globalWrapper);
+    /* Embedding as <style> to ensure uiTheme option doesn't cause FOUC.
+     * Note that in production build there's no <head> in html but document.head is still
+     * auto-created per the specification so our styles will be placed correctly anyway. */
+    config.plugins.push(new HTMLInlineCSSWebpackPlugin({
+      replace: {
+        target: '<body>',
+        position: 'before',
+      },
+    }));
+  }),
+  modify('background', './src/background', (config) => {
+    config.plugins.push(globalWrapper);
+    config.plugins.push(new class ListBackgroundScripts {
+      apply(compiler) {
+        compiler.hooks.afterEmit.tap(this.constructor.name, compilation => {
+          const path = `${compilation.outputOptions.path}/manifest.json`;
+          const manifest = JSON.parse(fs.readFileSync(path, {encoding: 'utf8'}));
+          const scripts = [...compilation.entrypoints.values()][0].chunks.map(c => c.files[0]);
+          if (`${manifest.background.scripts}` !== `${scripts}`) {
+            manifest.background.scripts = scripts;
+            fs.writeFileSync(path,
+              JSON.stringify(manifest, null, isProd ? 0 : 2),
+              {encoding: 'utf8'});
+          }
+        });
+      }
+    });
   }),
   }),
   modify('injected', './src/injected', (config) => {
   modify('injected', './src/injected', (config) => {
     config.plugins.push(
     config.plugins.push(

+ 15 - 0
src/_locales/en/messages.yml

@@ -690,6 +690,9 @@ optionEditorWindowHint:
 optionEditorWindowSimple:
 optionEditorWindowSimple:
   description: Label for the editor window type
   description: Label for the editor window type
   message: Hide omnibox
   message: Hide omnibox
+optionPopup:
+  description: Label of the popup menu section in settings.
+  message: Popup menu and icon
 optionPopupEnabledFirst:
 optionPopupEnabledFirst:
   description: Option to show enabled scripts first in popup.
   description: Option to show enabled scripts first in popup.
   message: Enabled first
   message: Enabled first
@@ -705,6 +708,18 @@ optionPopupShowDisabled:
 optionShowEnabledFirst:
 optionShowEnabledFirst:
   description: Option to show enabled scripts first in alphabetical order.
   description: Option to show enabled scripts first in alphabetical order.
   message: Show enabled scripts first
   message: Show enabled scripts first
+optionUiTheme:
+  description: Label for the theme selector in Advanced settings.
+  message: 'UI theme: '
+optionUiThemeAuto:
+  description: Name of a theme selector value in settings.
+  message: automatic
+optionUiThemeDark:
+  description: Name of a theme selector value in settings.
+  message: dark
+optionUiThemeLight:
+  description: Name of a theme selector value in settings.
+  message: light
 searchCaseSensitive:
 searchCaseSensitive:
   description: Option to perform a case-sensitive search
   description: Option to perform a case-sensitive search
   message: Case sensitive
   message: Case sensitive

+ 3 - 0
src/common/options-defaults.js

@@ -77,4 +77,7 @@ export default {
   // Enables automatic updates to the default template with new versions of VM
   // Enables automatic updates to the default template with new versions of VM
   /** @type {?Boolean} this must be |null| for template-hook.js upgrade routine */
   /** @type {?Boolean} this must be |null| for template-hook.js upgrade routine */
   scriptTemplateEdited: null,
   scriptTemplateEdited: null,
+  /** @typedef {'' | 'dark' | 'light'} VMUiTheme */
+  /** @type VMUiTheme */
+  uiTheme: '',
 };
 };

+ 37 - 15
src/common/ui/style/index.js

@@ -3,7 +3,15 @@ import './style.css';
 
 
 let style;
 let style;
 let styleTheme;
 let styleTheme;
+/** @type {CSSMediaRule[]} */
+let darkMediaRules;
+let localStorage = {};
+/* Accessing `localStorage` throws in Private Browsing mode and when DOM storage is disabled.
+ * Since it allows object-like access, we'll map it to a variable or use a dummy on exception. */
+try { ({ localStorage } = global); } catch (e) { /* keep the dummy object */ }
+
 const THEME_KEY = 'editorTheme';
 const THEME_KEY = 'editorTheme';
+const UI_THEME_KEY = 'uiTheme';
 const CACHE_KEY = 'cacheCustomCSS';
 const CACHE_KEY = 'cacheCustomCSS';
 
 
 const setStyle = (css, elem) => {
 const setStyle = (css, elem) => {
@@ -13,35 +21,49 @@ const setStyle = (css, elem) => {
   }
   }
   if (css || elem) {
   if (css || elem) {
     css = css || '';
     css = css || '';
-    elem.textContent = css;
-    try {
-      localStorage.setItem(CACHE_KEY, css);
-    } catch {
-      // ignore
+    if (elem.textContent !== css) {
+      elem.textContent = css;
+    }
+    if (localStorage[CACHE_KEY] !== css) {
+      localStorage[CACHE_KEY] = css;
     }
     }
   }
   }
   return elem;
   return elem;
 };
 };
 
 
-const setTheme = (css) => {
+const setCmTheme = (css) => {
   if (!global.location.pathname.startsWith('/popup')) {
   if (!global.location.pathname.startsWith('/popup')) {
     styleTheme = setStyle(css ?? options.get(THEME_KEY), styleTheme);
     styleTheme = setStyle(css ?? options.get(THEME_KEY), styleTheme);
   }
   }
 };
 };
 
 
-// In some versions of Firefox, `localStorage` is not allowed to be accessed
-// in Private Browsing mode.
-try {
-  setStyle(localStorage.getItem(CACHE_KEY));
-} catch {
-  // ignore
-}
+const setUiTheme = theme => {
+  const darkThemeCondition = '(prefers-color-scheme: dark)';
+  const mediaText = theme === 'dark' && 'screen'
+    || theme === 'light' && 'not all'
+    || darkThemeCondition;
+  if (!darkMediaRules) {
+    darkMediaRules = [];
+    for (const sheet of document.styleSheets) {
+      for (const rule of sheet.cssRules) {
+        if (rule.conditionText?.includes(darkThemeCondition)) {
+          darkMediaRules.push(rule);
+        }
+      }
+    }
+  }
+  darkMediaRules.forEach(rule => { rule.media.mediaText = mediaText; });
+};
+
+setStyle(localStorage[CACHE_KEY]);
 
 
-options.ready.then(setTheme);
 options.hook((changes) => {
 options.hook((changes) => {
   let v;
   let v;
   if ((v = changes[THEME_KEY]) != null) {
   if ((v = changes[THEME_KEY]) != null) {
-    setTheme(v);
+    setCmTheme(v);
+  }
+  if ((v = changes[UI_THEME_KEY]) != null) {
+    setUiTheme(v);
   }
   }
   if ((v = changes.customCSS) != null) {
   if ((v = changes.customCSS) != null) {
     style = setStyle(v, style);
     style = setStyle(v, style);

+ 1 - 1
src/manifest.yml

@@ -19,7 +19,7 @@ browser_action:
   default_title: __MSG_extName__
   default_title: __MSG_extName__
   default_popup: popup/index.html
   default_popup: popup/index.html
 background:
 background:
-  page: background/index.html
+  scripts: []
 options_page: options/index.html
 options_page: options/index.html
 options_ui:
 options_ui:
   page: options/index.html
   page: options/index.html

+ 18 - 1
src/options/views/tab-settings/index.vue

@@ -2,7 +2,7 @@
   <div class="tab-settings mb-1c">
   <div class="tab-settings mb-1c">
     <h1 class="mt-0" v-text="i18n('labelSettings')"></h1>
     <h1 class="mt-0" v-text="i18n('labelSettings')"></h1>
     <section class="mb-1c">
     <section class="mb-1c">
-      <h3 v-text="i18n('labelGeneral')"></h3>
+      <h3 v-text="i18n('optionPopup')"/>
       <div>
       <div>
         <setting-check name="autoReload" :label="i18n('labelAutoReloadCurrentTab')" />
         <setting-check name="autoReload" :label="i18n('labelAutoReloadCurrentTab')" />
       </div>
       </div>
@@ -82,6 +82,16 @@
     <div v-show="showAdvanced">
     <div v-show="showAdvanced">
       <section class="mb-1c">
       <section class="mb-1c">
         <h3 v-text="i18n('labelGeneral')"></h3>
         <h3 v-text="i18n('labelGeneral')"></h3>
+        <div>
+          <label>
+            <locale-group i18n-key="optionUiTheme">
+              <select v-for="opt in ['uiTheme']" v-model="settings[opt]" :key="opt">
+                <option v-for="(title, value) in items[opt].enum" :key="`${opt}:${value}`"
+                        :value="value" v-text="title" />
+              </select>
+            </locale-group>
+          </label>
+        </div>
         <div>
         <div>
           <label>
           <label>
             <span v-text="i18n('labelInjectionMode')"></span>
             <span v-text="i18n('labelInjectionMode')"></span>
@@ -172,6 +182,13 @@ const items = {
       alpha: i18n('filterAlphabeticalOrder'),
       alpha: i18n('filterAlphabeticalOrder'),
     },
     },
   },
   },
+  uiTheme: {
+    enum: {
+      '': i18n('optionUiThemeAuto'),
+      dark: i18n('optionUiThemeDark'),
+      light: i18n('optionUiThemeLight'),
+    },
+  },
   ...badgeColorEnum::mapEntry(() => badgeColorItem),
   ...badgeColorEnum::mapEntry(() => badgeColorItem),
 };
 };
 const normalizeEnum = (value, name) => (
 const normalizeEnum = (value, name) => (

+ 20 - 0
yarn.lock

@@ -4995,6 +4995,14 @@ html-entities@^1.2.1:
   resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
   resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
   integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
   integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
 
 
+html-inline-css-webpack-plugin@^1.11.1:
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/html-inline-css-webpack-plugin/-/html-inline-css-webpack-plugin-1.11.1.tgz#e2aa6329687a108e90df6a30b65ad72d65731c03"
+  integrity sha512-P4GjZ4fxGn8Rm53TrYBUvjPMoOQAOzVGdVb8OKfrGkHyeTBl+M0+H0q6rjhiVZuF0I1MFUErc4N9hidcLlC1kg==
+  dependencies:
+    lodash "^4.17.15"
+    tslib "^1.9.3"
+
 html-minifier-terser@^5.0.1:
 html-minifier-terser@^5.0.1:
   version "5.0.4"
   version "5.0.4"
   resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.0.4.tgz#e8cc02748acb983bd7912ea9660bd31c0702ec32"
   resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.0.4.tgz#e8cc02748acb983bd7912ea9660bd31c0702ec32"
@@ -7583,6 +7591,13 @@ postcss-colormin@^4.0.3:
     postcss "^7.0.0"
     postcss "^7.0.0"
     postcss-value-parser "^3.0.0"
     postcss-value-parser "^3.0.0"
 
 
+postcss-combine-media-query@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-combine-media-query/-/postcss-combine-media-query-1.0.1.tgz#99a8c4477c31b5e20061c6509bb18165c0f8066e"
+  integrity sha512-DFSXuYy3ltDkC2esIF0ORoS9DCjlyfWhtoQkG9brZMuJY1ABOER95sm3dvccR6IEgSrYX4RgqiHD4Lq3JGrxyw==
+  dependencies:
+    postcss "^7.0.21"
+
 postcss-convert-values@^4.0.1:
 postcss-convert-values@^4.0.1:
   version "4.0.1"
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f"
   resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f"
@@ -10144,6 +10159,11 @@ tslib@^1.10.0, tslib@^1.9.0:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
   integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
   integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
 
 
+tslib@^1.9.3:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
 [email protected]:
 [email protected]:
   version "0.0.0"
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
   resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"