Add support for experiments

This change refactors the options logic and adds support for
experiments: a new type of options which are not shown in the options
page (their usage will be similar to Chrome flags).

Experiments can be set from the
chrome-extension://{extension_id}/options/experiments.html page.

This code refactoring simplifies the options definition. Each option now
has a default value, and a context: the place where the option is set
(options, experiments, internal, deprecated).

Change-Id: I358ae07c832acae6b4536788c4dbe12a0e4730bf
diff --git a/docs/developers/add_feature.md b/docs/developers/add_feature.md
index 1f3fe0d..15053e4 100644
--- a/docs/developers/add_feature.md
+++ b/docs/developers/add_feature.md
@@ -9,8 +9,8 @@
 
 ### How to add the feature switch option
 1. First of all, think of a short codename for the feature.
-2. Modify the `//src/common/common.js` file by adding the default value for the
-option in the `defaultOptions` object.
+2. Modify the `//src/common/common.js` file by adding an entry in the
+`optionsPrototype` object.
     - All features should have the `false` value set as a default, so existing
     users have to explicitly enable the option after they receive the extension
     update. Otherwise, it might cause confusion, because users wouldn't know if
@@ -59,7 +59,7 @@
 codename appended by an underscore and a suffix
 (`{{feature_codename}}_{{suffix}}`).
 2. Modify the `//src/common/common.js` file by doing the following things:
-    1. Add a default value for the option in the `defaultOptions` object.
+    1. Add an entry for the option in the `optionsPrototype` object.
     2. Append the option's codename to the `specialOptions` object. This is so
     the option can be handled in a specific way when showing/saving it in the
     options page, or so it is handled outside of the options page.
diff --git a/src/_locales/ca/messages.json b/src/_locales/ca/messages.json
index c4c74cc..2c3ed92 100644
--- a/src/_locales/ca/messages.json
+++ b/src/_locales/ca/messages.json
@@ -127,6 +127,14 @@
     "message": "Desat",
     "description": "Message which appears in the options page when the settings are saved"
   },
+  "options_experiments_title": {
+    "message": "Experiments",
+    "description": "Title of the experiments page: a page where highly experimental options can be set."
+  },
+  "options_experiments_description": {
+    "message": "<i>Welchrome!</i> Aquí a sota trobaràs una llista d'experiments: funcions que estan en desenvolupament i que encara no estan llestes del tot per ser llençades. Són altament experimentals i podrien trencar-se i/o causar problemes, però si ets valent/a, sisplau activa les que més t'interessin i <a href='https://github.com/avm99963/infinitegforums/discussions/categories/feedback'>envia feedback</a>!",
+    "description": "Description shown in the beginning of the experiments page, below the title."
+  },
   "inject_links": {
     "message": "Enllaços",
     "description": "Heading which we use before the 'previous post' link in a user profile in TW"
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index f3b71d3..7c8b45b 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -127,6 +127,14 @@
     "message": "Saved",
     "description": "Message which appears in the options page when the settings are saved"
   },
+  "options_experiments_title": {
+    "message": "Experiments",
+    "description": "Title of the experiments page: a page where highly experimental options can be set."
+  },
+  "options_experiments_description": {
+    "message": "Welchrome! Below you'll a find a list of experiments: features which are in development, and are not quite ready for launch. They are highly experimental and so might break and/or cause issues, but if you're brave, please do enable the ones you're most interested in, and <a href='https://github.com/avm99963/infinitegforums/discussions/categories/feedback'>give feedback</a>!",
+    "description": "Description shown in the beginning of the experiments page, below the title."
+  },
   "inject_links": {
     "message": "Links",
     "description": "Heading which we use before the 'previous post' link in a user profile in TW"
diff --git a/src/_locales/es/messages.json b/src/_locales/es/messages.json
index 1ac1f11..ae9fbdd 100644
--- a/src/_locales/es/messages.json
+++ b/src/_locales/es/messages.json
@@ -127,6 +127,14 @@
     "message": "Guardado",
     "description": "Message which appears in the options page when the settings are saved"
   },
+  "options_experiments_title": {
+    "message": "Experimentos",
+    "description": "Title of the experiments page: a page where highly experimental options can be set."
+  },
+  "options_experiments_description": {
+    "message": "<i>Welchrome!</i> Aquí abajo encontrarás una lista de experimentos: funciones que están en desarrollo y que todavía no están listas del todo para lanzarse. Son altamente experimentales y podrían romperse y/o causar problemas, pero si eres valiente, por favor activa las que más te interesen, ¡y <a href='https://github.com/avm99963/infinitegforums/discussions/categories/feedback'>envia feedback</a>!",
+    "description": "Description shown in the beginning of the experiments page, below the title."
+  },
   "inject_links": {
     "message": "Enlaces",
     "description": "Heading which we use before the 'previous post' link in a user profile in TW"
diff --git a/src/background.js b/src/background.js
index d7aae23..8d0f411 100644
--- a/src/background.js
+++ b/src/background.js
@@ -4,7 +4,7 @@
 chrome.runtime.onInstalled.addListener(function(details) {
   if (details.reason == 'install' || details.reason == 'update') {
     chrome.storage.sync.get(null, function(options) {
-      cleanUpOptions(options);
+      cleanUpOptions(options, false);
     });
   }
 });
diff --git a/src/common/common.js b/src/common/common.js
index e4af064..205cdc9 100644
--- a/src/common/common.js
+++ b/src/common/common.js
@@ -1,28 +1,108 @@
-const defaultOptions = {
-  'list': true,
-  'thread': true,
-  'threadall': false,
-  'fixedtoolbar': false,
-  'redirect': false,
-  'history': false,
-  'loaddrafts': false,
-  'batchduplicate': false,
-  'escalatethreads': false,
-  'movethreads': false,
-  'increasecontrast': false,
-  'stickysidebarheaders': false,
-  'profileindicator': false,
-  'profileindicatoralt': false,
-  'profileindicatoralt_months': 12,
-  'ccdarktheme': false,
-  'ccdarktheme_mode': 'switch',
-  'ccdarktheme_switch_enabled': true,
-  'ccforcehidedrawer': false,
-  'ccdragndropfix': false,
-  'batchlock': false,
-  'smei_sortdirection': false,
-  'enhancedannouncementsdot': false,
-  'repositionexpandthread': false,
+const optionsPrototype = {
+  // Available options:
+  'list': {
+    defaultValue: true,
+    context: 'options',
+  },
+  'thread': {
+    defaultValue: true,
+    context: 'options',
+  },
+  'threadall': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'fixedtoolbar': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'redirect': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'history': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'loaddrafts': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'increasecontrast': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'stickysidebarheaders': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'profileindicator': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'profileindicatoralt': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'profileindicatoralt_months': {
+    defaultValue: 12,
+    context: 'options',
+  },
+  'ccdarktheme': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'ccdarktheme_mode': {
+    defaultValue: 'switch',
+    context: 'options',
+  },
+  'ccforcehidedrawer': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'ccdragndropfix': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'batchlock': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'smei_sortdirection': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'enhancedannouncementsdot': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'repositionexpandthread': {
+    defaultValue: false,
+    context: 'options',
+  },
+
+  // Experiments:
+
+
+  // Internal options:
+  'ccdarktheme_switch_enabled': {
+    defaultValue: true,
+    context: 'internal',
+  },
+
+  // Deprecated options:
+  'escalatethreads': {
+    defaultValue: false,
+    context: 'deprecated',
+  },
+  'movethreads': {
+    defaultValue: false,
+    context: 'deprecated',
+  },
+  'batchduplicate': {
+    defaultValue: false,
+    context: 'deprecated',
+  },
 };
 
 const specialOptions = [
@@ -32,34 +112,28 @@
   'ccdragndropfix',
 ];
 
-const deprecatedOptions = [
-  'escalatethreads',
-  'movethreads',
-  'batchduplicate',
-];
-
 function isEmpty(obj) {
   return Object.keys(obj).length === 0;
 }
 
-function cleanUpOptions(options) {
-  console.log('[cleanUpOptions] Previous options', options);
+// Adds missing options with their default value. If |dryRun| is set to false,
+// they are also saved to the sync storage area.
+function cleanUpOptions(options, dryRun = false) {
+  console.log('[cleanUpOptions] Previous options', JSON.stringify(options));
 
-  if (typeof options !== 'object' || options === null) {
-    options = defaultOptions;
-  } else {
-    var ok = true;
-    for (const [opt, value] of Object.entries(defaultOptions)) {
-      if (!(opt in options)) {
-        ok = false;
-        options[opt] = value;
-      }
+  if (typeof options !== 'object' || options === null) options = {};
+
+  var ok = true;
+  for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
+    if (!(opt in options)) {
+      ok = false;
+      options[opt] = optMeta['defaultValue'];
     }
   }
 
-  console.log('[cleanUpOptions] New options', options);
+  console.log('[cleanUpOptions] New options', JSON.stringify(options));
 
-  if (!ok) {
+  if (!ok && !dryRun) {
     chrome.storage.sync.set(options);
   }
 
diff --git a/src/options/experiments.html b/src/options/experiments.html
new file mode 100644
index 0000000..a49e6c9
--- /dev/null
+++ b/src/options/experiments.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Experiments</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet" href="options.css">
+    <link rel="stylesheet" href="chrome_style/chrome_style.css">
+  </head>
+  <body>
+    <main>
+      <h1 data-i18n="experiments_title"></h1>
+      <p data-i18n="experiments_description"></p>
+      <form>
+        <div class="actions"><button id="save" data-i18n="save"></button></div>
+      </form>
+      <div id="save-indicator"></div>
+    </main>
+    <script src="../common/common.js"></script>
+    <script src="experiments_bit.js"></script>
+    <script src="options_common.js"></script>
+  </body>
+</html>
diff --git a/src/options/experiments_bit.js b/src/options/experiments_bit.js
new file mode 100644
index 0000000..af8e086
--- /dev/null
+++ b/src/options/experiments_bit.js
@@ -0,0 +1 @@
+window.CONTEXT = 'experiments';
diff --git a/src/options/options.css b/src/options/options.css
index 06b8c40..7ebe04f 100644
--- a/src/options/options.css
+++ b/src/options/options.css
@@ -2,6 +2,11 @@
   padding-top: 16px;
 }
 
+main {
+  margin: auto;
+  max-width: 600px;
+}
+
 .features-link {
   position: absolute;
   top: 8px;
diff --git a/src/options/options.html b/src/options/options.html
index 69cddd8..8dc8129 100644
--- a/src/options/options.html
+++ b/src/options/options.html
@@ -8,41 +8,44 @@
     <link rel="stylesheet" href="chrome_style/chrome_style.css">
   </head>
   <body>
-    <a href="https://gerrit.avm99963.com/plugins/gitiles/infinitegforums/+/master/docs/features.md" target="_blank" class="features-link">
-      <!--
-        Material Design Icon - action/help_outline
-         - LICENSE: Apache License Version 2.0
-         - Source: https://github.com/google/material-design-icons/
-         - Author: Google LLC
-      -->
-      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-4h2v2h-2zm1.61-9.96c-2.06-.3-3.88.97-4.43 2.79-.18.58.26 1.17.87 1.17h.2c.41 0 .74-.29.88-.67.32-.89 1.27-1.5 2.3-1.28.95.2 1.65 1.13 1.57 2.1-.1 1.34-1.62 1.63-2.45 2.88 0 .01-.01.01-.01.02-.01.02-.02.03-.03.05-.09.15-.18.32-.25.5-.01.03-.03.05-.04.08-.01.02-.01.04-.02.07-.12.34-.2.75-.2 1.25h2c0-.42.11-.77.28-1.07.02-.03.03-.06.05-.09.08-.14.18-.27.28-.39.01-.01.02-.03.03-.04.1-.12.21-.23.33-.34.96-.91 2.26-1.65 1.99-3.56-.24-1.74-1.61-3.21-3.35-3.47z"/></svg>
-    </a>
-    <form>
-      <div class="option"><input type="checkbox" id="list"> <label for="list" data-i18n="list"></label></div>
-      <div class="option"><input type="checkbox" id="thread"> <label for="thread" data-i18n="thread"></label></div>
-      <div class="option"><input type="checkbox" id="threadall"> <label for="threadall" data-i18n="threadall"></label></div>
-      <h4 data-i18n="enhancements"></h4>
-      <div class="option"><input type="checkbox" id="fixedtoolbar"> <label for="fixedtoolbar" data-i18n="fixedtoolbar"></label></div>
-      <div class="option"><input type="checkbox" id="redirect"> <label for="redirect" data-i18n="redirect"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-      <div class="option"><input type="checkbox" id="history"> <label for="history" data-i18n="history"></label></div>
-      <div class="option"><input type="checkbox" id="loaddrafts"> <label for="loaddrafts" data-i18n="loaddrafts"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-      <div class="option"><input type="checkbox" id="increasecontrast"> <label for="increasecontrast" data-i18n="increasecontrast"></label></div>
-      <div class="option"><input type="checkbox" id="stickysidebarheaders"> <label for="stickysidebarheaders" data-i18n="stickysidebarheaders"></label></div>
-      <div class="option"><input type="checkbox" id="ccdarktheme"> <label for="ccdarktheme" data-i18n="ccdarktheme"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-      <div class="option"><input type="checkbox" id="ccforcehidedrawer"> <label for="ccforcehidedrawer" data-i18n="ccforcehidedrawer"></label></div>
-      <div id="dragndrop-wrapper" class="option" hidden><input type="checkbox" id="ccdragndropfix"> <label for="ccdragndropfix" data-i18n="ccdragndropfix"></label></div>
-      <div class="option"><input type="checkbox" id="batchlock"> <label for="batchlock" data-i18n="batchlock"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-      <div class="option"><input type="checkbox" id="smei_sortdirection"> <label for="smei_sortdirection" data-i18n="smei_sortdirection"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-      <div class="option"><input type="checkbox" id="enhancedannouncementsdot"> <label for="enhancedannouncementsdot" data-i18n="enhancedannouncementsdot"></label></div>
-      <div class="option"><input type="checkbox" id="repositionexpandthread"> <label for="repositionexpandthread" data-i18n="repositionexpandthread"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-      <h4 data-i18n="profileindicator_header"></h4>
-      <div class="option"><input type="checkbox" id="profileindicator"> <label for="profileindicator" data-i18n="profileindicator"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-      <div class="option"><input type="checkbox" id="profileindicatoralt"> <label for="profileindicatoralt" data-i18n="profileindicatoralt"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-      <div class="option"><a href="https://gerrit.avm99963.com/plugins/gitiles/infinitegforums/+/refs/heads/master/docs/op_indicator.md" target="_blank" rel="noreferrer noopener" data-i18n="profileindicator_moreinfo"></a></div>
-      <div class="actions"><button id="save" data-i18n="save"></button></div>
-    </form>
-    <div id="save-indicator"></div>
+    <main>
+      <a href="https://gerrit.avm99963.com/plugins/gitiles/infinitegforums/+/master/docs/features.md" target="_blank" class="features-link">
+        <!--
+          Material Design Icon - action/help_outline
+           - LICENSE: Apache License Version 2.0
+           - Source: https://github.com/google/material-design-icons/
+           - Author: Google LLC
+        -->
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-4h2v2h-2zm1.61-9.96c-2.06-.3-3.88.97-4.43 2.79-.18.58.26 1.17.87 1.17h.2c.41 0 .74-.29.88-.67.32-.89 1.27-1.5 2.3-1.28.95.2 1.65 1.13 1.57 2.1-.1 1.34-1.62 1.63-2.45 2.88 0 .01-.01.01-.01.02-.01.02-.02.03-.03.05-.09.15-.18.32-.25.5-.01.03-.03.05-.04.08-.01.02-.01.04-.02.07-.12.34-.2.75-.2 1.25h2c0-.42.11-.77.28-1.07.02-.03.03-.06.05-.09.08-.14.18-.27.28-.39.01-.01.02-.03.03-.04.1-.12.21-.23.33-.34.96-.91 2.26-1.65 1.99-3.56-.24-1.74-1.61-3.21-3.35-3.47z"/></svg>
+      </a>
+      <form>
+        <div class="option"><input type="checkbox" id="list"> <label for="list" data-i18n="list"></label></div>
+        <div class="option"><input type="checkbox" id="thread"> <label for="thread" data-i18n="thread"></label></div>
+        <div class="option"><input type="checkbox" id="threadall"> <label for="threadall" data-i18n="threadall"></label></div>
+        <h4 data-i18n="enhancements"></h4>
+        <div class="option"><input type="checkbox" id="fixedtoolbar"> <label for="fixedtoolbar" data-i18n="fixedtoolbar"></label></div>
+        <div class="option"><input type="checkbox" id="redirect"> <label for="redirect" data-i18n="redirect"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="history"> <label for="history" data-i18n="history"></label></div>
+        <div class="option"><input type="checkbox" id="loaddrafts"> <label for="loaddrafts" data-i18n="loaddrafts"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="increasecontrast"> <label for="increasecontrast" data-i18n="increasecontrast"></label></div>
+        <div class="option"><input type="checkbox" id="stickysidebarheaders"> <label for="stickysidebarheaders" data-i18n="stickysidebarheaders"></label></div>
+        <div class="option"><input type="checkbox" id="ccdarktheme"> <label for="ccdarktheme" data-i18n="ccdarktheme"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="ccforcehidedrawer"> <label for="ccforcehidedrawer" data-i18n="ccforcehidedrawer"></label></div>
+        <div id="dragndrop-wrapper" class="option" hidden><input type="checkbox" id="ccdragndropfix"> <label for="ccdragndropfix" data-i18n="ccdragndropfix"></label></div>
+        <div class="option"><input type="checkbox" id="batchlock"> <label for="batchlock" data-i18n="batchlock"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="smei_sortdirection"> <label for="smei_sortdirection" data-i18n="smei_sortdirection"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="enhancedannouncementsdot"> <label for="enhancedannouncementsdot" data-i18n="enhancedannouncementsdot"></label></div>
+        <div class="option"><input type="checkbox" id="repositionexpandthread"> <label for="repositionexpandthread" data-i18n="repositionexpandthread"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <h4 data-i18n="profileindicator_header"></h4>
+        <div class="option"><input type="checkbox" id="profileindicator"> <label for="profileindicator" data-i18n="profileindicator"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><input type="checkbox" id="profileindicatoralt"> <label for="profileindicatoralt" data-i18n="profileindicatoralt"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
+        <div class="option"><a href="https://gerrit.avm99963.com/plugins/gitiles/infinitegforums/+/refs/heads/master/docs/op_indicator.md" target="_blank" rel="noreferrer noopener" data-i18n="profileindicator_moreinfo"></a></div>
+        <div class="actions"><button id="save" data-i18n="save"></button></div>
+      </form>
+      <div id="save-indicator"></div>
+    </main>
     <script src="../common/common.js"></script>
-    <script src="options.js"></script>
+    <script src="options_bit.js"></script>
+    <script src="options_common.js"></script>
   </body>
 </html>
diff --git a/src/options/options.js b/src/options/options.js
deleted file mode 100644
index 46913d4..0000000
--- a/src/options/options.js
+++ /dev/null
@@ -1,153 +0,0 @@
-var savedSuccessfullyTimeout = null;
-
-const exclusiveOptions = [['thread', 'threadall']];
-
-function save(e) {
-  var options = defaultOptions;
-
-  // Validation checks before saving
-  var months = document.getElementById('profileindicatoralt_months');
-  if (!months.checkValidity()) {
-    console.warn(months.validationMessage);
-    return;
-  }
-
-  e.preventDefault();
-
-  // Save
-  Object.keys(options).forEach(function(opt) {
-    if (deprecatedOptions.includes(opt)) return;
-
-    if (specialOptions.includes(opt)) {
-      switch (opt) {
-        case 'profileindicatoralt_months':
-          options[opt] = document.getElementById(opt).value || 12;
-          break;
-
-        case 'ccdarktheme_mode':
-          options[opt] = document.getElementById(opt).value || 'switch';
-          break;
-
-        // This option is controlled directly in the Community Console.
-        case 'ccdarktheme_switch_enabled':
-          break;
-
-        case 'ccdragndropfix':
-          options[opt] = document.getElementById(opt).checked || false;
-          break;
-
-        default:
-          console.warn('Unrecognized option: ' + opt);
-          break;
-      }
-      return;
-    }
-
-    options[opt] = document.getElementById(opt).checked || false;
-  });
-
-  chrome.storage.sync.set(options, function() {
-    window.close();
-
-    // In browsers like Firefox window.close is not supported:
-    if (savedSuccessfullyTimeout !== null)
-      window.clearTimeout(savedSuccessfullyTimeout);
-
-    document.getElementById('save-indicator').innerText =
-        '✓ ' + chrome.i18n.getMessage('options_saved');
-    savedSuccessfullyTimeout = window.setTimeout(_ => {
-      document.getElementById('save-indicator').innerText = '';
-    }, 3699);
-  });
-}
-
-function i18n() {
-  document.querySelectorAll('[data-i18n]')
-      .forEach(
-          el => el.innerHTML = chrome.i18n.getMessage(
-              'options_' + el.getAttribute('data-i18n')));
-}
-
-window.addEventListener('load', function() {
-  i18n();
-
-  chrome.storage.sync.get(null, function(items) {
-    items = cleanUpOptions(items);
-
-    Object.keys(defaultOptions).forEach(function(opt) {
-      if (deprecatedOptions.includes(opt)) return;
-
-      if (specialOptions.includes(opt)) {
-        switch (opt) {
-          case 'profileindicatoralt_months':
-            var input = document.createElement('input');
-            input.type = 'number';
-            input.id = 'profileindicatoralt_months';
-            input.max = '12';
-            input.min = '1';
-            input.value = items[opt];
-            input.required = true;
-            document.getElementById('profileindicatoralt_months--container')
-                .appendChild(input);
-            break;
-
-          case 'ccdarktheme_mode':
-            var select = document.createElement('select');
-            select.id = 'ccdarktheme_mode';
-
-            const modes = ['switch', 'system'];
-            for (const mode of modes) {
-              var modeOption = document.createElement('option');
-              modeOption.value = mode;
-              modeOption.textContent =
-                  chrome.i18n.getMessage('options_ccdarktheme_mode_' + mode);
-              if (items.ccdarktheme_mode == mode) modeOption.selected = true;
-              select.appendChild(modeOption);
-            }
-
-            document.getElementById('ccdarktheme_mode--container')
-                .appendChild(select);
-            break;
-
-          // This option is controlled directly in the Community Console.
-          case 'ccdarktheme_switch_enabled':
-            break;
-
-          // Firefox doesn't support drag and dropping bookmarks into the text
-          // editor while preserving the bookmark title.
-          case 'ccdragndropfix':
-            var showOption = !isFirefox();
-            if (showOption) {
-              document.getElementById('dragndrop-wrapper')
-                  .removeAttribute('hidden');
-
-              if (items[opt] === true)
-                document.getElementById(opt).checked = true;
-            }
-            break;
-
-          default:
-            console.warn('Unrecognized option: ' + opt);
-            break;
-        }
-        return;
-      }
-
-      if (items[opt] === true) document.getElementById(opt).checked = true;
-    });
-
-    exclusiveOptions.forEach(exclusive => {
-      exclusive.forEach(
-          el => document.getElementById(el).addEventListener('change', e => {
-            if (document.getElementById(exclusive[0]).checked &&
-                document.getElementById(exclusive[1]).checked) {
-              document
-                  .getElementById(
-                      exclusive[(e.currentTarget.id == exclusive[0] ? 1 : 0)])
-                  .checked = false;
-            }
-          }));
-    });
-    document.querySelector('#save').addEventListener('click', save);
-  });
-});
diff --git a/src/options/options_bit.js b/src/options/options_bit.js
new file mode 100644
index 0000000..a6186d3
--- /dev/null
+++ b/src/options/options_bit.js
@@ -0,0 +1 @@
+window.CONTEXT = 'options';
diff --git a/src/options/options_common.js b/src/options/options_common.js
new file mode 100644
index 0000000..10fee88
--- /dev/null
+++ b/src/options/options_common.js
@@ -0,0 +1,157 @@
+var savedSuccessfullyTimeout = null;
+
+const exclusiveOptions = [['thread', 'threadall']];
+
+// Get the value of the option set in the options/experiments page
+function getOptionValue(opt) {
+  if (specialOptions.includes(opt)) {
+    switch (opt) {
+      case 'profileindicatoralt_months':
+        return document.getElementById(opt).value || 12;
+
+      case 'ccdarktheme_mode':
+        return document.getElementById(opt).value || 'switch';
+
+      case 'ccdragndropfix':
+        return document.getElementById(opt).checked || false;
+
+      default:
+        console.warn('Unrecognized option: ' + opt);
+        return undefined;
+    }
+  }
+
+  return document.getElementById(opt).checked || false;
+}
+
+// Returns whether the option is included in the current context
+function isOptionShown(opt) {
+  if (!optionsPrototype.hasOwnProperty(opt)) return false;
+  return optionsPrototype[opt].context == window.CONTEXT;
+}
+
+function save(e) {
+  // Validation checks before saving
+  if (isOptionShown('profileindicatoralt_months')) {
+    var months = document.getElementById('profileindicatoralt_months');
+    if (!months.checkValidity()) {
+      console.warn(months.validationMessage);
+      return;
+    }
+  }
+
+  e.preventDefault();
+
+  chrome.storage.sync.get(null, function(items) {
+    var options = cleanUpOptions(items, true);
+
+    // Save
+    Object.keys(options).forEach(function(opt) {
+      if (!isOptionShown(opt)) return;
+      options[opt] = getOptionValue(opt);
+    });
+
+    chrome.storage.sync.set(options, function() {
+      window.close();
+
+      // In browsers like Firefox window.close is not supported:
+      if (savedSuccessfullyTimeout !== null)
+        window.clearTimeout(savedSuccessfullyTimeout);
+
+      document.getElementById('save-indicator').innerText =
+          '✓ ' + chrome.i18n.getMessage('options_saved');
+      savedSuccessfullyTimeout = window.setTimeout(_ => {
+        document.getElementById('save-indicator').innerText = '';
+      }, 3699);
+    });
+  });
+}
+
+function i18n() {
+  document.querySelectorAll('[data-i18n]')
+      .forEach(
+          el => el.innerHTML = chrome.i18n.getMessage(
+              'options_' + el.getAttribute('data-i18n')));
+}
+
+window.addEventListener('load', function() {
+  i18n();
+
+  chrome.storage.sync.get(null, function(items) {
+    items = cleanUpOptions(items, false);
+
+    for ([opt, optMeta] of Object.entries(optionsPrototype)) {
+      if (!isOptionShown(opt)) continue;
+
+      if (specialOptions.includes(opt)) {
+        switch (opt) {
+          case 'profileindicatoralt_months':
+            var input = document.createElement('input');
+            input.type = 'number';
+            input.id = 'profileindicatoralt_months';
+            input.max = '12';
+            input.min = '1';
+            input.value = items[opt];
+            input.required = true;
+            document.getElementById('profileindicatoralt_months--container')
+                .appendChild(input);
+            break;
+
+          case 'ccdarktheme_mode':
+            var select = document.createElement('select');
+            select.id = 'ccdarktheme_mode';
+
+            const modes = ['switch', 'system'];
+            for (const mode of modes) {
+              var modeOption = document.createElement('option');
+              modeOption.value = mode;
+              modeOption.textContent =
+                  chrome.i18n.getMessage('options_ccdarktheme_mode_' + mode);
+              if (items.ccdarktheme_mode == mode) modeOption.selected = true;
+              select.appendChild(modeOption);
+            }
+
+            document.getElementById('ccdarktheme_mode--container')
+                .appendChild(select);
+            break;
+
+          // Firefox doesn't support drag and dropping bookmarks into the text
+          // editor while preserving the bookmark title.
+          case 'ccdragndropfix':
+            var showOption = !isFirefox();
+            if (showOption) {
+              document.getElementById('dragndrop-wrapper')
+                  .removeAttribute('hidden');
+
+              if (items[opt] === true)
+                document.getElementById(opt).checked = true;
+            }
+            break;
+
+          default:
+            console.warn('Unrecognized option: ' + opt);
+            break;
+        }
+        continue;
+      }
+
+      if (items[opt] === true) document.getElementById(opt).checked = true;
+    }
+
+    exclusiveOptions.forEach(exclusive => {
+      if (!isOptionShown(exclusive[0]) || !isOptionShown(exclusive[1])) return;
+
+      exclusive.forEach(
+          el => document.getElementById(el).addEventListener('change', e => {
+            if (document.getElementById(exclusive[0]).checked &&
+                document.getElementById(exclusive[1]).checked) {
+              document
+                  .getElementById(
+                      exclusive[(e.currentTarget.id == exclusive[0] ? 1 : 0)])
+                  .checked = false;
+            }
+          }));
+    });
+    document.querySelector('#save').addEventListener('click', save);
+  });
+});
diff --git a/src/sw.js b/src/sw.js
index c214068..2f181cb 100644
--- a/src/sw.js
+++ b/src/sw.js
@@ -6,7 +6,7 @@
 chrome.runtime.onInstalled.addListener(details => {
   if (details.reason == 'install' || details.reason == 'update') {
     chrome.storage.sync.get(null, options => {
-      cleanUpOptions(options);
+      cleanUpOptions(options, false);
     });
   }
 });