From 282460cb3f5f331303b05ceb8b1981477ebfd3e9 Mon Sep 17 00:00:00 2001
From: zzz <zzz@i2pmail.org>
Date: Thu, 30 Sep 2021 09:55:35 -0400
Subject: [PATCH] Console: Add js to /configui to preview themes

Save theme change before form processing so no refresh required
Enable/disable reset and apply buttons on config clicks
Prep for theme picker in wizard
---
 .../src/net/i2p/router/web/CSSHelper.java     | 10 +++
 .../router/web/helpers/ConfigUIHandler.java   | 35 +++++-----
 .../router/web/helpers/ConfigUIHelper.java    |  6 +-
 apps/routerconsole/jsp/configui.jsp           |  9 +--
 apps/routerconsole/jsp/css.jsi                | 12 ++--
 apps/routerconsole/jsp/js/configui.js         | 67 +++++++++++++++++++
 apps/routerconsole/jsp/summarynoframe.jsi     |  2 +-
 7 files changed, 110 insertions(+), 31 deletions(-)
 create mode 100644 apps/routerconsole/jsp/js/configui.js

diff --git a/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java
index 2926d5d0e1..308daa481d 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java
@@ -69,6 +69,16 @@ public class CSSHelper extends HelperBase {
         return url;
     }
 
+    /**
+     * So we don't have to refresh after saving. Called from css.jsi.
+     * @since 0.9.52
+     */
+    public void setTheme(String theme) {
+        if (theme != null && theme.length() > 0 &&
+            theme.replaceAll("[a-zA-Z0-9_-]", "").length() == 0)
+            _context.router().saveConfig(PROP_THEME_NAME, theme);
+    }
+
     /**
      * Returns whether app embedding is enabled or disabled
      * @since 0.9.32
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigUIHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigUIHandler.java
index c1d0505aa3..219bd38352 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigUIHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigUIHandler.java
@@ -28,7 +28,7 @@ public class ConfigUIHandler extends FormHandler {
             delUser();
         } else if (_action.equals(_t("Add user"))) {
             addUser();
-        }
+        } // else lang change, handled in CSSHelper
     }
 
     public void setShouldsave(String moo) { _shouldSave = true; }
@@ -43,22 +43,23 @@ public class ConfigUIHandler extends FormHandler {
         _config = val;
     }
 
-    /** note - lang change is handled in CSSHelper but we still need to save it here */
+    /**
+     *  This is for the theme options only.
+     *  Lang change is handled in CSSHelper.
+     */
     private void saveChanges() {
-        if (_config == null || _config.length() <= 0)
-            return;
-        if (_config.replaceAll("[a-zA-Z0-9_-]", "").length() != 0) {
-            addFormError("Bad theme name");
-            return;
-        }
         Map<String, String> changes = new HashMap<String, String>();
         List<String> removes = new ArrayList<String>();
         String oldTheme = _context.getProperty(CSSHelper.PROP_THEME_NAME, CSSHelper.DEFAULT_THEME);
         boolean oldForceMobileConsole = _context.getBooleanProperty(CSSHelper.PROP_FORCE_MOBILE_CONSOLE);
-        if (_config.equals("default")) // obsolete
-            removes.add(CSSHelper.PROP_THEME_NAME);
-        else
-            changes.put(CSSHelper.PROP_THEME_NAME, _config);
+        boolean validTheme = _config != null && _config.length() > 0 &&
+                             _config.replaceAll("[a-zA-Z0-9_-]", "").length() == 0;
+        if (validTheme) {
+            if (_config.equals("default")) // obsolete
+                removes.add(CSSHelper.PROP_THEME_NAME);
+            else
+                changes.put(CSSHelper.PROP_THEME_NAME, _config);
+        }
         if (_universalTheming)
             changes.put(CSSHelper.PROP_UNIVERSAL_THEMING, "true");
         else
@@ -73,11 +74,11 @@ public class ConfigUIHandler extends FormHandler {
             removes.add(CSSHelper.PROP_EMBED_APPS);
         boolean ok = _context.router().saveConfig(changes, removes);
         if (ok) {
-            if (!oldTheme.equals(_config))
-                addFormNoticeNoEscape(_t("Theme change saved.") +
-                              " <a href=\"configui\">" +
-                              _t("Refresh the page to view.") +
-                              "</a>");
+            // Theme saving happens in CSSHelper
+            // so output this even if it didn't change
+            //if (!oldTheme.equals(_config))
+            if (validTheme)
+                addFormNotice(_t("Theme change saved."));
             if (oldForceMobileConsole != _forceMobileConsole)
                 addFormNoticeNoEscape(_t("Mobile console option saved.") +
                               " <a href=\"configui\">" +
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigUIHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigUIHelper.java
index 49b5df0cc5..cae5214718 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigUIHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigUIHelper.java
@@ -41,7 +41,7 @@ public class ConfigUIHelper extends HelperBase {
         }
         boolean universalTheming = _context.getBooleanProperty(CSSHelper.PROP_UNIVERSAL_THEMING);
         buf.append("</div><div id=\"themeoptions\">" +
-                   "<label><input type=\"checkbox\" name=\"universalTheming\" ");
+                   "<label><input id=\"themebox1\" type=\"checkbox\" name=\"universalTheming\" ");
         if (universalTheming)
             buf.append(CHECKED);
         buf.append("value=\"1\">")
@@ -53,7 +53,7 @@ public class ConfigUIHelper extends HelperBase {
     public String getForceMobileConsole() {
         StringBuilder buf = new StringBuilder(256);
         boolean forceMobileConsole = _context.getBooleanProperty(CSSHelper.PROP_FORCE_MOBILE_CONSOLE);
-        buf.append("<label><input type=\"checkbox\" name=\"forceMobileConsole\" ");
+        buf.append("<label><input id=\"themebox2\" type=\"checkbox\" name=\"forceMobileConsole\" ");
         if (forceMobileConsole)
             buf.append(CHECKED);
         buf.append("value=\"1\">")
@@ -62,7 +62,7 @@ public class ConfigUIHelper extends HelperBase {
         boolean embedApps = _context.getBooleanProperty(CSSHelper.PROP_EMBED_APPS);
         buf.append("<label title=\"")
            .append(_t("Enabling the Universal Theming option is recommended when embedding these applications"))
-           .append("\"><input type=\"checkbox\" name=\"embedApps\" ");
+           .append("\"><input id=\"themebox3\" type=\"checkbox\" name=\"embedApps\" ");
         if (embedApps)
             buf.append(CHECKED);
         buf.append("value=\"1\">")
diff --git a/apps/routerconsole/jsp/configui.jsp b/apps/routerconsole/jsp/configui.jsp
index 06fafe3df1..58bb1b233d 100644
--- a/apps/routerconsole/jsp/configui.jsp
+++ b/apps/routerconsole/jsp/configui.jsp
@@ -13,6 +13,7 @@ input.default {
 }
 </style>
 <%@include file="summaryajax.jsi" %>
+<script src="/js/configui.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 </head><body>
 <%@include file="summary.jsi" %>
 <jsp:useBean class="net.i2p.router.web.helpers.ConfigUIHelper" id="uihelper" scope="request" />
@@ -26,7 +27,7 @@ input.default {
  <jsp:useBean class="net.i2p.router.web.helpers.ConfigUIHandler" id="formhandler" scope="request" />
 <%@include file="formhandler.jsi" %>
 <h3 id="themeheading"><%=uihelper._t("Router Console Theme")%></h3>
- <form action="" method="POST">
+ <form id="themeForm" action="" method="POST">
  <input type="hidden" name="consoleNonce" value="<%=net.i2p.router.web.CSSHelper.getNonce()%>" >
  <input type="hidden" name="nonce" value="<%=pageNonce%>" >
  <input type="hidden" name="action" value="blah" >
@@ -43,8 +44,8 @@ input.default {
 <% } %>
  <jsp:getProperty name="uihelper" property="forceMobileConsole" />
 <hr><div class="formaction" id="themeui">
-<input type="reset" class="cancel" value="<%=intl._t("Cancel")%>" >
-<input type="submit" name="shouldsave" class="accept" value="<%=intl._t("Apply")%>" >
+<input id="themeCancel" type="reset" class="cancel" value="<%=intl._t("Cancel")%>" >
+<input id="themeApply" type="submit" name="shouldsave" class="accept" value="<%=intl._t("Apply")%>" >
 </div></div></form>
 <h3 id="langheading"><%=uihelper._t("Router Console Language")%></h3>
  <form action="" method="POST">
@@ -56,7 +57,7 @@ input.default {
 <p id="helptranslate"><%=uihelper._t("Please contribute to the router console translation project! Contact the developers in #i2p-dev on IRC to help.")%>
 </p><hr><div class="formaction" id="langui">
 <input type="reset" class="cancel" value="<%=intl._t("Cancel")%>" >
-<input type="submit" name="shouldsave" class="accept" value="<%=intl._t("Apply")%>" >
+<input type="submit" name="foo" class="accept" value="<%=intl._t("Apply")%>" >
 </div></div></form>
 
 <h3 id="passwordheading"><%=uihelper._t("Router Console Password")%></h3>
diff --git a/apps/routerconsole/jsp/css.jsi b/apps/routerconsole/jsp/css.jsi
index aad30f9b97..e3ab1970b7 100644
--- a/apps/routerconsole/jsp/css.jsi
+++ b/apps/routerconsole/jsp/css.jsi
@@ -33,11 +33,6 @@
 <jsp:useBean class="net.i2p.router.web.CSSHelper" id="intl" scope="request" />
 <jsp:setProperty name="intl" property="contextId" value="<%=i2pcontextId%>" /><%
 
-   // used several times below
-   String theUserAgent = request.getHeader("User-Agent");
-   String theThemePath = intl.getTheme(theUserAgent);
-
-%><link rel="icon" href="<%=theThemePath%>images/favicon.ico"><%
    response.setHeader("Accept-Ranges", "none");
 
    String cspNonce = Integer.toHexString(net.i2p.util.RandomSource.getInstance().nextInt());
@@ -61,8 +56,13 @@
    if (net.i2p.router.web.CSSHelper.getNonce().equals(conNonceParam)) {
        intl.setLang(request.getParameter("lang"));
        intl.setNews(request.getParameter("news"));
+       intl.setTheme(request.getParameter("theme"));
    }
-%><link href="<%=theThemePath%>console.css?<%=net.i2p.CoreVersion.VERSION%>" rel="stylesheet" type="text/css">
+   // used several times below
+   String theUserAgent = request.getHeader("User-Agent");
+   String theThemePath = intl.getTheme(theUserAgent);
+%><link rel="icon" href="<%=theThemePath%>images/favicon.ico">
+<link id="pagestyle" href="<%=theThemePath%>console.css?<%=net.i2p.CoreVersion.VERSION%>" rel="stylesheet" type="text/css">
 <%
    if (intl.getLang().equals("zh")) {
        // make the fonts bigger for chinese
diff --git a/apps/routerconsole/jsp/js/configui.js b/apps/routerconsole/jsp/js/configui.js
new file mode 100644
index 0000000000..c1baa6c45c
--- /dev/null
+++ b/apps/routerconsole/jsp/js/configui.js
@@ -0,0 +1,67 @@
+/* @license http://creativecommons.org/publicdomain/zero/1.0/legalcode CC0-1.0 */
+
+// This component is dedicated to the public domain. It uses the CC0
+// as a formal dedication to the public domain and in circumstances where
+// a public domain is not usable.
+
+var prefersDarkTheme = false;
+var oldTheme = "light";
+
+function detectDark() {
+    // https://stackoverflow.com/questions/56393880/how-do-i-detect-dark-mode-using-javascript
+    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+        // dark mode
+        prefersDarkTheme = true;
+    }
+}
+
+function swapStyleSheet(theme) {
+    // https://stackoverflow.com/questions/14292997/changing-style-sheet-javascript
+    document.getElementById("pagestyle").setAttribute("href", "/themes/console/" + theme + "/console.css");
+    document.getElementById("i2plogo").setAttribute("src", "/themes/console/" + theme + "/images/i2plogo.png");
+}
+
+function disableButtons(disabled) {
+    document.getElementById("themeApply").disabled = disabled;
+    document.getElementById("themeCancel").disabled = disabled;
+}
+
+function resetStyleSheet() {
+    swapStyleSheet(oldTheme);
+    document.getElementById("themeForm").reset();
+    disableButtons(true);
+}
+
+function initThemeSwitcher() {
+    var dark = document.getElementById("dark");
+    dark.onclick = function() {
+        swapStyleSheet("dark");
+        disableButtons(false);
+    }
+    if (dark.checked) {
+        oldTheme = "dark";
+    }
+    var light = document.getElementById("light");
+    light.onclick = function() {
+        swapStyleSheet("light");
+        disableButtons(false);
+    }
+    var apply = document.getElementById("themeApply");
+    apply.setAttribute("disabled", true);
+    var cancel = document.getElementById("themeCancel");
+    cancel.setAttribute("disabled", true);
+    cancel.onclick = function() {
+        resetStyleSheet();
+    }
+    document.getElementById("themebox1").onclick = function() { disableButtons(false); }
+    document.getElementById("themebox2").onclick = function() { disableButtons(false); }
+    document.getElementById("themebox3").onclick = function() { disableButtons(false); }
+    // unused for now
+    detectDark();
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+    initThemeSwitcher();
+}, true);
+
+/* @license-end */
diff --git a/apps/routerconsole/jsp/summarynoframe.jsi b/apps/routerconsole/jsp/summarynoframe.jsi
index 22c85834e9..8bdf2cc64a 100644
--- a/apps/routerconsole/jsp/summarynoframe.jsi
+++ b/apps/routerconsole/jsp/summarynoframe.jsi
@@ -8,7 +8,7 @@
 %>
   <div>
    <a href="/" target="_top">
-    <img src="<%=intl.getTheme(request.getHeader("User-Agent"))%>images/i2plogo.png" alt="<%=intl._t("I2P Router Console")%>" title="<%=intl._t("I2P Router Console")%>">
+    <img id="i2plogo" src="<%=intl.getTheme(request.getHeader("User-Agent"))%>images/i2plogo.png" alt="<%=intl._t("I2P Router Console")%>" title="<%=intl._t("I2P Router Console")%>">
    </a>
   </div>
   <div id="xhr">
-- 
GitLab