diff --git a/CMakeLists.txt b/CMakeLists.txt
index ee1d441..6131385 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -36,7 +36,6 @@ set(SOURCE_FILES
libesphttpd/include/espmissingincludes.h
libesphttpd/include/espfs.h
libesphttpd/include/esp8266.h
- libesphttpd/include/cgiwifi.h
libesphttpd/include/cgiwebsocket.h
libesphttpd/include/cgiflash.h
libesphttpd/include/captdns.h
@@ -54,7 +53,6 @@ set(SOURCE_FILES
libesphttpd/lib/heatshrink/heatshrink_decoder.c
libesphttpd/lib/heatshrink/heatshrink.c
libesphttpd/mkupgimg/mkupgimg.c
- libesphttpd/util/cgiwifi.c
libesphttpd/util/cgiwebsocket.c
libesphttpd/util/cgiflash.c
libesphttpd/util/captdns.c
@@ -93,6 +91,14 @@ set(SOURCE_FILES
include/ets_sys_extra.h
user/io.c
user/io.h
+ user/cgi_wifi.c
+ user/cgi_wifi.h
+ user/cgi_persist.c
+ user/cgi_persist.h
+ user/cgi_network.c
+ user/cgi_network.h
+ user/cgi_appcfg.c
+ user/cgi_appcfg.h
user/cgi_ping.c
user/cgi_reset.c
user/uart_driver.c
@@ -113,7 +119,11 @@ set(SOURCE_FILES
user/cgi_sockets.h
user/ansi_parser_callbacks.c
user/ansi_parser_callbacks.h
- user/user_main.h)
+ user/user_main.h
+ user/wifimgr.c
+ user/wifimgr.h
+ user/persist.c
+ user/persist.h include/helpers.h)
include_directories(include)
include_directories(user)
@@ -132,6 +142,7 @@ add_definitions(
-DICACHE_FLASH_ATTR=
-DICACHE_RODATA_ATTR=
-DFLAG_GZIP=2
+ -DADMIN_PASSWORD="asdf"
-DESPFS_HEATSHRINK)
add_executable(esp_vt100_firmware ${SOURCE_FILES})
diff --git a/Makefile b/Makefile
index 5310111..edde199 100644
--- a/Makefile
+++ b/Makefile
@@ -65,7 +65,7 @@ LIBS += esphttpd
# compiler flags using during compilation of source files
CFLAGS = -Os -ggdb -std=gnu99 -Werror -Wpointer-arith -Wundef -Wall -Wl,-EL -fno-inline-functions \
-nostdlib -mlongcalls -mtext-section-literals -D__ets__ -DICACHE_FLASH \
- -Wno-address -Wno-unused -DHTTPD_MAX_BACKLOG_SIZE=8192
+ -Wno-address -Wno-unused -DHTTPD_MAX_BACKLOG_SIZE=8192 -DADMIN_PASSWORD=$(ADMIN_PASSWORD)
# linker flags used to generate the main object file
LDFLAGS = -nostdlib -Wl,--no-check-sections -u call_user_start -Wl,-static
diff --git a/build_web.sh b/build_web.sh
index 1495c74..b794ccd 100755
--- a/build_web.sh
+++ b/build_web.sh
@@ -7,24 +7,15 @@ mkdir -p html/img
mkdir -p html/js
mkdir -p html/css
-# Join scripts
-DD=html_orig/jssrc
-cat $DD/chibi.js \
- $DD/utils.js \
- $DD/modal.js \
- $DD/appcommon.js \
- $DD/term.js \
- $DD/wifi.js > html/js/app.js
+cd html_orig
+sh ./packjs.sh
+php ./build_html.php
+cd ..
-sass --sourcemap=none html_orig/sass/app.scss html_orig/css/app.css
+cp html_orig/js/app.js html/js/
-# No need to compress CSS and JS now, we run YUI on it later
-cp html_orig/css/app.css html/css/app.css
-cp html_orig/term.html html/term.tpl
-cp html_orig/wifi.html html/wifi.tpl
-cp html_orig/about.html html/about.tpl
-cp html_orig/help.html html/help.tpl
-cp html_orig/wifi_conn.html html/wifi_conn.tpl
-cp html_orig/img/loader.gif html/img/loader.gif
-cp html_orig/img/cvut.svg html/img/cvut.svg
-cp html_orig/favicon.ico html/favicon.ico
+sass html_orig/sass/app.scss html/css/app.css
+rm html/css/app.css.map
+
+cp html_orig/img/* html/img/
+cp html_orig/favicon.ico html/favicon.ico
diff --git a/esphttpdconfig.mk b/esphttpdconfig.mk
index 6f294de..edc6980 100644
--- a/esphttpdconfig.mk
+++ b/esphttpdconfig.mk
@@ -37,3 +37,6 @@ OUTPUT_TYPE = combined
# SPI flash size, in K
ESP_SPI_FLASH_SIZE_K = 1024
+
+# Admin password, used to store settings to flash as defaults
+ADMIN_PASSWORD = "19738426"
diff --git a/html_orig/.gitignore b/html_orig/.gitignore
index dec88d1..0a0f73a 100644
--- a/html_orig/.gitignore
+++ b/html_orig/.gitignore
@@ -1 +1 @@
-_test_env.php
+pages/_test_env.php
diff --git a/html_orig/_debug_replacements.php b/html_orig/_debug_replacements.php
new file mode 100644
index 0000000..e693ea1
--- /dev/null
+++ b/html_orig/_debug_replacements.php
@@ -0,0 +1,62 @@
+ 'ESP8266 Wireless Terminal',
+
+ '%btn1%' => '1',
+ '%btn2%' => '2',
+ '%btn3%' => '3',
+ '%btn4%' => '4',
+ '%btn5%' => '5',
+
+ '%screenData%' => '{
+ "w": 26, "h": 10,
+ "x": 0, "y": 0,
+ "cv": 1,
+ "screen": "70 t259"
+ }',
+
+ '%ap_enable%' => '1',
+ '%tpw%' => '60',
+ '%ap_channel%' => '7',
+ '%ap_ssid%' => 'ESP-123456',
+ '%ap_password%' => 'Passw0rd!',
+ '%ap_hidden%' => '0',
+ '%sta_ssid%' => 'Chlivek',
+ '%sta_password%' => 'windows XP is The Best',
+ '%sta_active_ip%' => '1.2.3.4',
+ '%sta_active_ssid%' => 'Chlivek',
+
+ '%sta_enable%' => '1',
+ '%opmode%' => '3',
+ '%vers_fw%' => '1.2.3',
+ '%date%' => date('Y-m-d'),
+ '%time%' => date('G:i'),
+ '%vers_httpd%' => '4.5.6',
+ '%vers_sdk%' => '1.52',
+ '%githubrepo%' => 'https://github.com/MightyPork/esp-vt100-firmware',
+
+ '%ap_dhcp_time%' => '120',
+ '%ap_dhcp_start%' => '192.168.4.100',
+ '%ap_dhcp_end%' => '192.168.4.200',
+ '%ap_addr_ip%' => '192.168.4.1',
+ '%ap_addr_mask%' => '255.255.255.0',
+
+ '%sta_dhcp_enable%' => '1',
+ '%sta_addr_ip%' => '192.168.0.33',
+ '%sta_addr_mask%' => '255.255.255.0',
+ '%sta_addr_gw%' => '192.168.0.1',
+
+ '%sta_mac%' => 'ab:cd:ef:01:23:45',
+ '%ap_mac%' => '01:23:45:ab:cd:ef',
+
+ '%term_width%' => '26',
+ '%term_height%' => '10',
+ '%default_bg%' => '0',
+ '%default_fg%' => '7',
+];
diff --git a/html_orig/_test_env.php.example b/html_orig/_env.php
similarity index 100%
rename from html_orig/_test_env.php.example
rename to html_orig/_env.php
diff --git a/html_orig/_env.php.example b/html_orig/_env.php.example
new file mode 100644
index 0000000..4d38f09
--- /dev/null
+++ b/html_orig/_env.php.example
@@ -0,0 +1,3 @@
+ $key,
+ 'bodyclass' => $bc,
+ 'path' => $path,
+ 'icon' => $icon ? "icn-$icon" : '',
+ 'label' => tr("menu.$key"),
+ 'title' => $titleKey ? tr($titleKey) : tr("menu.$key"),
+ ];
+ }
+}
+
+pg('cfg_wifi', 'cfg', 'wifi', '/cfg/wifi');
+pg('cfg_wifi_conn', '', '', '/cfg/wifi/connecting');
+pg('wifi_connstatus', 'api', '', '/cfg/wifi/connstatus');
+pg('wifi_set', 'api', '', '/cfg/wifi/set');
+pg('wifi_scan', 'api', '', '/cfg/wifi/scan');
+
+pg('cfg_network', 'cfg', 'network', '/cfg/network');
+pg('network_set', 'api', '', '/cfg/network/set');
+
+pg('cfg_app', 'cfg', 'terminal', '/cfg/app');
+pg('app_set', 'api', '', '/cfg/app/set');
+
+pg('cfg_admin', 'cfg', 'persist', '/cfg/admin');
+pg('write_defaults', 'api', '', '/cfg/admin/write_defaults');
+pg('restore_defaults', 'api', '', '/cfg/admin/restore_defaults');
+pg('restore_hard', 'api', '', '/cfg/admin/restore_hard');
+
+pg('help', 'cfg page-help', 'help', '/help');
+pg('about', 'cfg page-about', 'about', '/about');
+pg('term', 'term', '', '/', 'title.term');
+
+// ajax API
+
+return $pages;
diff --git a/html_orig/about.html b/html_orig/about.html
deleted file mode 100644
index d83f202..0000000
--- a/html_orig/about.html
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
- About - ESP8266 Remote Terminal
-
-
-
-
-
-
-
-About
-
-
-
-
-
Firmware
-
-
- Firmware
- v%vers_fw%, build %date% at %time%
-
-
- libesphttpd
- v%vers_httpd%
-
-
- ESP IoT SDK
- v%vers_sdk%
-
-
-
-
-
-
Issues
-
- Please report any issues to the bugtracker or send them by e-mail.
-
-
- Firmware updates can be downloaded from the releases page .
- Flash the images using esptool .
-
-
-
-
-
Contributing
-
- Submit your improvements and ideas to the project on GitHub .
-
-
-
-
-
Acknowledgements
-
- The webserver is based on a fork of the
- esphttpd library by Jeroen Domburg (Sprite_tm).
-
-
- Using (modified) JS library chibi.js by Kyle Barrow as a lightweight jQuery alternative.
-
-
-
-
- Terminal Help WiFi config
-
-
-
-
diff --git a/html_orig/base.php b/html_orig/base.php
new file mode 100644
index 0000000..925d057
--- /dev/null
+++ b/html_orig/base.php
@@ -0,0 +1,71 @@
+_env.php.example to _env.php and check the settings inside!");
+}
+
+require_once __DIR__ . '/_env.php';
+
+$prod = defined('STDIN');
+define('DEBUG', !$prod);
+$root = DEBUG ? json_encode(ESP_IP) : 'location.host';
+define('JS_WEB_ROOT', $root);
+
+define('LOCALE', isset($_GET['locale']) ? $_GET['locale'] : 'en');
+
+$_messages = require(__DIR__ . '/lang/' . LOCALE . '.php');
+$_pages = require(__DIR__ . '/_pages.php');
+
+define('APP_NAME', 'ESPTerm');
+
+/** URL (dev or production) */
+function url($name, $relative = false)
+{
+ global $_pages;
+ if ($relative) return $_pages[$name]->path;
+
+ if (DEBUG) return "/index.php?page=$name";
+ else return $_pages[$name]->path;
+}
+
+/** URL label for a button */
+function label($name)
+{
+ global $_pages;
+ return $_pages[$name]->label;
+}
+
+function e($s)
+{
+ return htmlspecialchars($s, ENT_HTML5 | ENT_QUOTES);
+}
+
+function tr($key)
+{
+ global $_messages;
+ return isset($_messages[$key]) ? $_messages[$key] : ('??' . $key . '??');
+}
+
+/** Like eval, but allows */
+function include_str($code)
+{
+ $tmp = tmpfile();
+ $tmpf = stream_get_meta_data($tmp);
+ $tmpf = $tmpf ['uri'];
+ fwrite($tmp, $code);
+ $ret = include($tmpf);
+ fclose($tmp);
+ return $ret;
+}
diff --git a/html_orig/build_html.php b/html_orig/build_html.php
new file mode 100755
index 0000000..d123e1e
--- /dev/null
+++ b/html_orig/build_html.php
@@ -0,0 +1,37 @@
+/Uis';
+ $s = preg_replace($pattern, '', $s);
+
+ $pattern = '/(?:(?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:(? $p) {
+ if ($p->bodyclass == 'api') continue;
+ echo "Generating: $_k ($p->title)\n";
+ $_GET['page'] = $_k;
+ ob_flush(); // print the message
+ ob_clean(); // clean up
+ include(__DIR__ . '/index.php');
+ $s = ob_get_contents(); // grab the output
+
+ // remove newlines and comments
+ // as tests have shown, it saves just a couple kilobytes,
+ // making it not a very big improvement at the expense of ugly html.
+// $s = process_html($s);
+
+ ob_clean(); // clean up
+ $of = __DIR__ . '/../html/' . $_k . '.tpl';
+ file_put_contents($of, $s); // write to a file
+}
+
+ob_flush();
diff --git a/html_orig/css/app.css b/html_orig/css/app.css
index d058ead..7063fea 100644
--- a/html_orig/css/app.css
+++ b/html_orig/css/app.css
@@ -1,3 +1,4 @@
+@charset "UTF-8";
/* normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Set default font family to sans-serif.
@@ -307,90 +308,93 @@ td,
th {
padding: 0; }
+@font-face {
+ font-family: 'fontello';
+ src: url("data:application/octet-stream;base64,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") format("woff"), url("data:application/octet-stream;base64,AAEAAAAPAIAAAwBwR1NVQiCLJXoAAAD8AAAAVE9TLzI+IFOEAAABUAAAAFZjbWFwRp90gwAAAagAAAI8Y3Z0IAc5/2gAABiwAAAAIGZwZ22KkZBZAAAY0AAAC3BnYXNwAAAAEAAAGKgAAAAIZ2x5ZhgKlUQAAAPkAAAQqGhlYWQOsBL8AAAUjAAAADZoaGVhB+MEGQAAFMQAAAAkaG10eCvN//kAABToAAAAMGxvY2EYlBJOAAAVGAAAABptYXhwAVINngAAFTQAAAAgbmFtZcydHR8AABVUAAACzXBvc3QcABrmAAAYJAAAAIRwcmVw5UErvAAAJEAAAACGAAEAAAAKADAAPgACREZMVAAObGF0bgAaAAQAAAAAAAAAAQAAAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAgAAQAGAAAAAQAAAAEDpgGQAAUAAAJ6ArwAAACMAnoCvAAAAeAAMQECAAACAAUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBmRWQAQOgA8esDhP+cAFoDhABkAAAAAQAAAAAAAAAAAAUAAAADAAAALAAAAAQAAAGoAAEAAAAAAKIAAwABAAAALAADAAoAAAGoAAQAdgAAABQAEAADAAToAugL6B/oJugu6DbxCPET8ev//wAA6ADoC+gf6CboLug28QjxE/Hr//8AAAAAAAAAAAAAAAAAAAAAAAAAAQAUABgAGAAYABgAGAAYABgAGAAAAAEAAgADAAQABQAGAAcACAAJAAoACwAAAQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAlAAAAAAAAAALAADoAAAA6AAAAAABAADoAQAA6AEAAAACAADoAgAA6AIAAAADAADoCwAA6AsAAAAEAADoHwAA6B8AAAAFAADoJgAA6CYAAAAGAADoLgAA6C4AAAAHAADoNgAA6DYAAAAIAADxCAAA8QgAAAAJAADxEwAA8RMAAAAKAADx6wAA8esAAAALAAQAAP/jA1kDPQADACEAMQBFAFFATisqIyIECAQBRw0BBAYBCAJGAAoHAQQICgRgAAgAAwYIA2AABgABAAYBXgUCAgAJCQBSBQICAAAJWAAJAAlMQD04NRcmMxETOxEREAsFHSs3ITUhBTMRNCYvAS4BBxUUBiMhIiYnNSMRMzU0NjMhMhYHAzU0JisBIgYXFRQWNzMyNgURFAYjISImJxE0NjMhMhYfAR4B1gGt/lMB9EgMBZ0FHAgeF/6+Fh4BSEggFQHRFiAB1goIawcMAQoIawcMAWQeF/0SFx4BIBYCBRc2D5wQFivW1gH0CBoHnAYMAegWICAW6P026BYgIBYBHrIICgoIsgcMAQoK/foWICAWAu4WIBgOnQ82AAAAAAEAAAAAA6UCygAVAB1AGg8BAAEBRwACAQJvAAEAAW8AAABmFBcUAwUXKwEUBwEGIicBJjQ/ATYyHwEBNjIfARYDpRD+IBAsEP7qDw9MECwQpAFuECwQTBACSBYQ/iAPDwEWECwQTBAQpQFvEBBMDwABAAD/4wPoAz4AHAAhQB4RAQABAUcCAQEAAW8DAQAAZgEAFxUNCwAcARwEBRQrBSInAScuAzU0NjcyHgIXPgMXMhYUBwEGAfQOC/6kDwoqIhqOfSJIPi4TFCxARiN9joD+pQodCgFQDwo2NlAle4oBGCoiFRQkKBoBjPWA/rEKAAEAAP/yApgDdgAUAC21AQEAAQFHS7AkUFhACwAAAQBwAAEBDAFJG0AJAAEAAW8AAABmWbQXFwIFFisJAhYUDwEGIicBJjQ3ATYyHwEWFAKO/tcBKQoKXQscC/5iCwsBngoeCl0KAtz+2P7XCh4KXQoKAZ8KHgoBngsLXQoeAAAAAQAA//wDoQNyAB8ANUAKEg8KBAMFAAIBR0uwHFBYQAwBAQACAHAAAgIMAkkbQAoAAgACbwEBAABmWbUdFBcDBRcrARQPARMVFA4BLwEHBiImNTQ3EycmNTQ3JTc2Mh8BBRYDoQ/KMAwVDPv6DBYMATDLDh8BGH4LIAx9ARggAhsMD8X+6QwLEAEHhIQHEgoECAEXxQ8MFQUo/hcX/igFAAP//f/jA18DPQAPADcARABIQEUpAQUDCQECAQACRwAEAgMCBANtAAMFAgMFawAHAAIEBwJgAAUAAAEFAGAAAQYGAVQAAQEGWAAGAQZMFR4rExYmJiMIBRwrJTU0JisBIgYdARQWOwEyNhM0LgEjIgcGHwEWMzI3PgEyFhUUBgcOARcVFBY7ATI2NDY/AT4DFxQOASIuAj4BMh4BAfQKCGsICgoIawgKjz5cMYhHCQ1KBAYJBR4lOCoWGyM8AQoIawgKGBIcCh4UDNdyxujIbgZ6vPS6foRrCAoKCGsICgoBfzFULncNCzcEByYbHhIVGgwPQiUUCAoKEiILEAYaHChSdcR0dMTqxHR0xAAD//3/4wNZAz0ADAG9AfcCd0uwCVBYQTwAvQC7ALgAnwCWAIgABgADAAAAjwABAAIAAwDaANMAbQBZAFEAQgA+ADMAIAAZAAoABwACAZ4BmAGWAYwBiwF6AXUBZQFjAQMA4QDgAAwABgAHAVMBTQEoAAMACAAGAfQB2wHRAcsBwAG+ATgBMwAIAAEACAAGAEcbS7AKUFhBQwC7ALgAnwCIAAQABQAAAL0AAQADAAUAjwABAAIAAwDaANMAbQBZAFEAQgA+ADMAIAAZAAoABwACAZ4BmAGWAYwBiwF6AXUBZQFjAQMA4QDgAAwABgAHAVMBTQEoAAMACAAGAfQB2wHRAcsBwAG+ATgBMwAIAAEACAAHAEcAlgABAAUAAQBGG0E8AL0AuwC4AJ8AlgCIAAYAAwAAAI8AAQACAAMA2gDTAG0AWQBRAEIAPgAzACAAGQAKAAcAAgGeAZgBlgGMAYsBegF1AWUBYwEDAOEA4AAMAAYABwFTAU0BKAADAAgABgH0AdsB0QHLAcABvgE4ATMACAABAAgABgBHWVlLsAlQWEA1AAIDBwMCB20ABwYDBwZrAAYIAwYIawAIAQMIAWsAAQFuCQEAAwMAVAkBAAADWAUEAgMAA0wbS7AKUFhAOgQBAwUCBQNlAAIHBQIHawAHBgUHBmsABggFBghrAAgBBQgBawABAW4JAQAFBQBUCQEAAAVWAAUABUobQDUAAgMHAwIHbQAHBgMHBmsABggDBghrAAgBAwgBawABAW4JAQADAwBUCQEAAANYBQQCAwADTFlZQRkAAQAAAdgB1gG5AbcBVwFWAMcAxQC1ALQAsQCuAHkAdgAHAAYAAAAMAAEADAAKAAUAFCsBMh4BFA4BIi4CPgEBDgEHMj4BNT4BNzYXJjY/ATY/AQYmNRQHNCYGNS4ELwEmNC8BBwYUKgEUIgYiBzYnJiM2JiczLgInLgEHBhQfARYGHgEHBg8BBhYXFhQGIg8BBiYnJicmByYnJgcyJgc+ASM2PwE2JxY/ATY3NjIWMxY0JzInJicmBwYXIg8BBi8BJiciBzYmIzYnJiIPAQYeATIXFgciBiIGFgcuAScWJyMiBiInJjc0FycGBzI2PwE2FzcXJgcGBxYHJy4BJyIHBgceAhQ3FgcyFxYXFgcnJgYWMyIPAQYfAQYWNwYfAx4CFwYWByIGNR4CFBY3NicuAjUzMh8BBh4CMx4BBzIeBB8DFjI/ATYWFxY3Ih8BHgEVHgEXNjUGFjM2NQYvASY0JjYXMjYuAicGJicUBhUjNjQ/ATYvASYHIgcOAyYnLgE0PwE2JzY/ATY7ATI0NiYjFjYXFjcnJjcWNx4CHwEWNjcWFx4BPgEmNSc1LgE2NzQ2PwE2JzI3JyYiNzYnPgEzFjYnPgE3FjYmPgEVNzYjFjc2JzYmJzMyNTYnJgM2NyYiLwE2Ji8BJi8BJg8BIg8BFSYnIi4BDgEPASY2JgYPAQY2BhUOARUuATceARcWBwYHBhcUBhYBrXTGcnLG6MhuBnq8ARMCCAMBAgQDERUTCgEMAggGAwEHBgQECgUGBAEIAQIBAwMEBAQEBgEGAggJBQQGAgQDAQgMAQUcBAMCAgEIAQ4BAgcJAwQEAQQCAwEHCgIEBQ0DAxQOEwQIBgECAQIFCQIBEwkGBAIFBgoDCAQHBQIDBgkEBgEFCQQFAwMCBQQBDgcLDwQQAwMBCAQIAQgDAQgEAwICAwQCBBIFAwwMAQMDAgwZGwMGBQUTBQMLBA0LAQQCBgQIBAkEUTIEBQIGBQMBGAoBAgcFBAMEBAQBAgEBAQIKBwcSBAcJBAMIBAIOAQECAg4CBAICDwgDBAMCAwUBBAoKAQQIBAUMBwIDCAMJBxYGBgUICBAEFAoBAgQCBgMOAwQBCgUIEQoCAgICAQUCBAEKAgMMAwIIAQIIAwEDAgcLBAECAggUAwgKAQIBBAIDBQIBAwIBAwEEGAMJAwEBAQMNAg4EAgMBBAMFAgYIBAICAQgEBAcIBQcMBAQCAgIGAQUEAwIDBQwEAhIBBAICBQ4JAgIKCAUJAgYGBwUJDAppc1ABDAENAQQDFQEDBQIDAgIBBQwIAwYGBgYBAQQIBAoBBwYCCgIEAQwBAQICBAsPAQIJCgEDPXTE6sR0dMTqxHT+3QEIAgYGAQQIAwULAQwBAwICDAEKBwIDBAIEAQIGDAUGAwMCBAEBAwMEAgQBAwMCAggEAgYEAQMEAQQEBgcDCAcKBwQFBgUMAwECBAIBAwwJDgMEBQcIBQMRAgMOCAUMAwEDCQkGBAMGAQ4ECgQBAgUCAgYKBAcHBwEJBQgHCAMCBwMCBAIGAgQFCgMDDgIFAgIFBAcCAQoIDwIDAwcDAg4DAgMEBgQGBAQBAS1PBAEIBAMEBg8KAgYEBQQFDgkUCwIBBhoCARcFBAYDBRQDAxAFAgEECAUIBAELGA0FDAICBAQMCA4EDgEKCxQHCAEFAw0CAQIBEgMKBAQJBQYCAwoDAgMFDAIQCBIDAwQEBgIECgcOAQUCBAEEAgIQBQ8FAgUDAgsCCAQEAgIEGA4JDgUJAQQGAQIDAgEEAwYHBgUCDwoBBAECAwECAwgFFwQCCAgDBQ4CCgoFAQIDBAsJBQICAgIGAgoGCgQEBAMBBAoEBgEHAgEHBgUEAgMBBQQC/g0VVQICBQQGAg8BAQIBAgEBAwIKAwYCAgUGBwMOBgIBBQQCCAECCAICAgIFHAgRCQ4JDAIEEAcAAQAA/+MDWQM9ADEAPkA7KgEDBSUdAgQDAkcABAMBAwQBbQABAgMBAmsABQADBAUDYAACAAACVAACAgBYAAACAEwpNRcjFyQGBRorARQOAgciJicmND8BNhYXHgEzMj4DLgIiBgcXFgYrASImJzU0Nh8BPgEzMh4CA1lEcqBWYK48BAVMBhEEKXZDOmhQKgIuTGxvZChNERMX+g8UASwRSDyaUleedEIBkFeedEICUkkGDgRNBQEGNTouTGp0akwuKCVNEC0WDvoYExJIOT5EdJ4AAAAC////4wQvA4QADwAvADBALQkBAgEAIAEDAgJHAAMCA3AAAQQBAgMBAmAAAAAFWAAFBQwASTUmNiYmFAYFGisBETQmJyEiBgcRFBYzITI2ExEUBgchFB4BFxQGIyEiJic0PgE1ISImNxE0NjMhMhYD6AoI/IMHCgEMBgN9BwxGNCX+0RIQARQP/uIPFAESEv7QJDYBNCUDfSU0AVoB0QcKAQwG/i8HCgoB2P2hJTQBFC4iBw4WFg4IIiwVNiQCXyU0NAAABAAA/+MDoQL1AAwAGQAzAFoAS0BIWVJORwQCCA0AAgADAkcJAQcIB28ACAIIbwQBAgMCbwADAANvAQEABQBvAAUGBgVUAAUFBlgABgUGTFVUIx1LNyISKxwTCgUdKyUUDgEuAz4CHgEFFA4BLgM+Ah4BFzQmIyIHBiInJiMiBgcUHgM3MzI+AzcUBw4EByIuBCcmNTQ3JjU0NzIWFzYzMhc+ATcWFRQHFgFlDiIuJAwCECAyHhIBYw4iLiQMAhAgMh4SWE5BF1YoYCdVGEJMASQ2UkouXi5KUjgifiIWSlRqVjIrSFxOTDoTI0wPHD1aPVJaU0o6XDsdD0zdFi4oAiQyKDQiBCosGBYuKAIkMig0IgQqLBhDXgwGBgxeQzFILBYMAggaKEySdEUrPiIUBAEEChgiOCRFdIRZLTJAOSwvFBIuKgE5QDEtWQAEAAAAAARfAz0ACgAgADoAUgCLQIhHAQsILwEEBhUBAgcDAQABBEcRDQILCAYICwZtEAkCBwQCBAcCbQ8FAgMCAQIDAW0ADAAKCAwKYAAIAAYECAZgAAQAAgMEAmAAAQAAAVQAAQEAWA4BAAEATDs7ISELCwEAO1I7UkxLRUNAPyE6ITo0My0rJyULIAsgGhkTEg8OBgUACgEKEgUUKyUiJic0PgEWBxQGNyIuASIGDwEiJjU0Nz4CFhcWFRQGNyInLgEHIg4DIyImNTQ3PgEeARcWFRQGNyInLgIGBwYjIiYnNDc2JCAEFxYVFAYCOwtQAUYsSAFSjAEqSEhGFhYKVAUsgoKEKwVUjgYGTIJVL2BGOCACCVQGStDY0kkGVI4GB2PY/tZkBwYJVAEGaAEgASwBImcFVDJSCxIYAhwQC1KXHBwcDg5UCgcGKzACNCkGBwpUmAU6OAEYIiQYVAoHBUpSAk5MBQcKVJcFWFgCXFYFVAoHBmhycmgGBwpUAAABAAAAAQAAyfbumV8PPPUACwPoAAAAANWaZ1gAAAAA1ZpnWP/9/+MEXwOEAAAACAACAAAAAAAAAAEAAAOE/5wAAAR2//3/+gRfAAEAAAAAAAAAAAAAAAAAAAAMA+gAAANZAAAD6AAAA+gAAALKAAADoAAAA1n//QNZ//0DWQAABC///wOgAAAEdgAAAAAAAACQAMgBCAFIAZgCHgYiBowG7geUCFQAAAABAAAADAH4AAQAAAAAAAIAJAA0AHMAAACqC3AAAAAAAAAAEgDeAAEAAAAAAAAANQAAAAEAAAAAAAEACAA1AAEAAAAAAAIABwA9AAEAAAAAAAMACABEAAEAAAAAAAQACABMAAEAAAAAAAUACwBUAAEAAAAAAAYACABfAAEAAAAAAAoAKwBnAAEAAAAAAAsAEwCSAAMAAQQJAAAAagClAAMAAQQJAAEAEAEPAAMAAQQJAAIADgEfAAMAAQQJAAMAEAEtAAMAAQQJAAQAEAE9AAMAAQQJAAUAFgFNAAMAAQQJAAYAEAFjAAMAAQQJAAoAVgFzAAMAAQQJAAsAJgHJQ29weXJpZ2h0IChDKSAyMDE3IGJ5IG9yaWdpbmFsIGF1dGhvcnMgQCBmb250ZWxsby5jb21mb250ZWxsb1JlZ3VsYXJmb250ZWxsb2ZvbnRlbGxvVmVyc2lvbiAxLjBmb250ZWxsb0dlbmVyYXRlZCBieSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAEMAbwBwAHkAcgBpAGcAaAB0ACAAKABDACkAIAAyADAAMQA3ACAAYgB5ACAAbwByAGkAZwBpAG4AYQBsACAAYQB1AHQAaABvAHIAcwAgAEAAIABmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQBmAG8AbgB0AGUAbABsAG8AUgBlAGcAdQBsAGEAcgBmAG8AbgB0AGUAbABsAG8AZgBvAG4AdABlAGwAbABvAFYAZQByAHMAaQBvAG4AIAAxAC4AMABmAG8AbgB0AGUAbABsAG8ARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQAHcGVyc2lzdAJvawZkb25hdGUEYmFjawVhYm91dARoZWxwB25ldHdvcmsHcmVzdG9yZQh0ZXJtaW5hbAZnaXRodWIEd2lmaQAAAAEAAf//AA8AAAAAAAAAAAAAAAAAAAAAABgAGAAYABgDhP+cA4T/nLAALCCwAFVYRVkgIEu4AA5RS7AGU1pYsDQbsChZYGYgilVYsAIlYbkIAAgAY2MjYhshIbAAWbAAQyNEsgABAENgQi2wASywIGBmLbACLCBkILDAULAEJlqyKAEKQ0VjRVJbWCEjIRuKWCCwUFBYIbBAWRsgsDhQWCGwOFlZILEBCkNFY0VhZLAoUFghsQEKQ0VjRSCwMFBYIbAwWRsgsMBQWCBmIIqKYSCwClBYYBsgsCBQWCGwCmAbILA2UFghsDZgG2BZWVkbsAErWVkjsABQWGVZWS2wAywgRSCwBCVhZCCwBUNQWLAFI0KwBiNCGyEhWbABYC2wBCwjISMhIGSxBWJCILAGI0KxAQpDRWOxAQpDsAFgRWOwAyohILAGQyCKIIqwASuxMAUlsAQmUVhgUBthUllYI1khILBAU1iwASsbIbBAWSOwAFBYZVktsAUssAdDK7IAAgBDYEItsAYssAcjQiMgsAAjQmGwAmJmsAFjsAFgsAUqLbAHLCAgRSCwC0NjuAQAYiCwAFBYsEBgWWawAWNgRLABYC2wCCyyBwsAQ0VCKiGyAAEAQ2BCLbAJLLAAQyNEsgABAENgQi2wCiwgIEUgsAErI7AAQ7AEJWAgRYojYSBkILAgUFghsAAbsDBQWLAgG7BAWVkjsABQWGVZsAMlI2FERLABYC2wCywgIEUgsAErI7AAQ7AEJWAgRYojYSBksCRQWLAAG7BAWSOwAFBYZVmwAyUjYUREsAFgLbAMLCCwACNCsgsKA0VYIRsjIVkqIS2wDSyxAgJFsGRhRC2wDiywAWAgILAMQ0qwAFBYILAMI0JZsA1DSrAAUlggsA0jQlktsA8sILAQYmawAWMguAQAY4ojYbAOQ2AgimAgsA4jQiMtsBAsS1RYsQRkRFkksA1lI3gtsBEsS1FYS1NYsQRkRFkbIVkksBNlI3gtsBIssQAPQ1VYsQ8PQ7ABYUKwDytZsABDsAIlQrEMAiVCsQ0CJUKwARYjILADJVBYsQEAQ2CwBCVCioogiiNhsA4qISOwAWEgiiNhsA4qIRuxAQBDYLACJUKwAiVhsA4qIVmwDENHsA1DR2CwAmIgsABQWLBAYFlmsAFjILALQ2O4BABiILAAUFiwQGBZZrABY2CxAAATI0SwAUOwAD6yAQEBQ2BCLbATLACxAAJFVFiwDyNCIEWwCyNCsAojsAFgQiBgsAFhtRAQAQAOAEJCimCxEgYrsHIrGyJZLbAULLEAEystsBUssQETKy2wFiyxAhMrLbAXLLEDEystsBgssQQTKy2wGSyxBRMrLbAaLLEGEystsBsssQcTKy2wHCyxCBMrLbAdLLEJEystsB4sALANK7EAAkVUWLAPI0IgRbALI0KwCiOwAWBCIGCwAWG1EBABAA4AQkKKYLESBiuwcisbIlktsB8ssQAeKy2wICyxAR4rLbAhLLECHistsCIssQMeKy2wIyyxBB4rLbAkLLEFHistsCUssQYeKy2wJiyxBx4rLbAnLLEIHistsCgssQkeKy2wKSwgPLABYC2wKiwgYLAQYCBDI7ABYEOwAiVhsAFgsCkqIS2wKyywKiuwKiotsCwsICBHICCwC0NjuAQAYiCwAFBYsEBgWWawAWNgI2E4IyCKVVggRyAgsAtDY7gEAGIgsABQWLBAYFlmsAFjYCNhOBshWS2wLSwAsQACRVRYsAEWsCwqsAEVMBsiWS2wLiwAsA0rsQACRVRYsAEWsCwqsAEVMBsiWS2wLywgNbABYC2wMCwAsAFFY7gEAGIgsABQWLBAYFlmsAFjsAErsAtDY7gEAGIgsABQWLBAYFlmsAFjsAErsAAWtAAAAAAARD4jOLEvARUqLbAxLCA8IEcgsAtDY7gEAGIgsABQWLBAYFlmsAFjYLAAQ2E4LbAyLC4XPC2wMywgPCBHILALQ2O4BABiILAAUFiwQGBZZrABY2CwAENhsAFDYzgtsDQssQIAFiUgLiBHsAAjQrACJUmKikcjRyNhIFhiGyFZsAEjQrIzAQEVFCotsDUssAAWsAQlsAQlRyNHI2GwCUMrZYouIyAgPIo4LbA2LLAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjILAIQyCKI0cjRyNhI0ZgsARDsAJiILAAUFiwQGBZZrABY2AgsAErIIqKYSCwAkNgZCOwA0NhZFBYsAJDYRuwA0NgWbADJbACYiCwAFBYsEBgWWawAWNhIyAgsAQmI0ZhOBsjsAhDRrACJbAIQ0cjRyNhYCCwBEOwAmIgsABQWLBAYFlmsAFjYCMgsAErI7AEQ2CwASuwBSVhsAUlsAJiILAAUFiwQGBZZrABY7AEJmEgsAQlYGQjsAMlYGRQWCEbIyFZIyAgsAQmI0ZhOFktsDcssAAWICAgsAUmIC5HI0cjYSM8OC2wOCywABYgsAgjQiAgIEYjR7ABKyNhOC2wOSywABawAyWwAiVHI0cjYbAAVFguIDwjIRuwAiWwAiVHI0cjYSCwBSWwBCVHI0cjYbAGJbAFJUmwAiVhuQgACABjYyMgWGIbIVljuAQAYiCwAFBYsEBgWWawAWNgIy4jICA8ijgjIVktsDossAAWILAIQyAuRyNHI2EgYLAgYGawAmIgsABQWLBAYFlmsAFjIyAgPIo4LbA7LCMgLkawAiVGUlggPFkusSsBFCstsDwsIyAuRrACJUZQWCA8WS6xKwEUKy2wPSwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xKwEUKy2wPiywNSsjIC5GsAIlRlJYIDxZLrErARQrLbA/LLA2K4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrErARQrsARDLrArKy2wQCywABawBCWwBCYgLkcjRyNhsAlDKyMgPCAuIzixKwEUKy2wQSyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2GwAiVGYTgjIDwjOBshICBGI0ewASsjYTghWbErARQrLbBCLLA1Ky6xKwEUKy2wQyywNishIyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=") format("truetype"); }
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/*
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'fontello';
+ src: url('../font/fontello.svg?60007293#fontello') format('svg');
+ }
+}
+*/
+[class^="icn-"]:before, [class*=" icn-"]:before {
+ font-family: "fontello";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ }
+
+.icn-persist:before {
+ content: '\e800'; }
+
+/* '' */
+.icn-ok:before {
+ content: '\e801'; }
+
+/* '' */
+.icn-donate:before {
+ content: '\e802'; }
+
+/* '' */
+.icn-back:before {
+ content: '\e80b'; }
+
+/* '' */
+.icn-about:before {
+ content: '\e81f'; }
+
+/* '' */
+.icn-help:before {
+ content: '\e826'; }
+
+/* '' */
+.icn-network:before {
+ content: '\e82e'; }
+
+/* '' */
+.icn-restore:before {
+ content: '\e836'; }
+
+/* '' */
+.icn-terminal:before {
+ content: '\f108'; }
+
+/* '' */
+.icn-github:before {
+ content: '\f113'; }
+
+/* '' */
+.icn-wifi:before {
+ content: '\f1eb'; }
+
+/* '' */
html {
box-sizing: border-box; }
*, *::after, *::before {
box-sizing: inherit; }
-.hidden {
- display: none !important; }
-
-[onclick] {
- cursor: pointer; }
-
-.Modal {
- position: fixed;
- width: 100%;
- height: 100%;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- display: flex;
- justify-content: center;
- align-items: center;
- transition: opacity .5s;
- background: rgba(0, 0, 0, 0.65);
- opacity: 0; }
- .Modal.visible {
- opacity: 1; }
- .Modal.hidden {
- display: none; }
-
-.Dialog {
- margin: 0.61805rem;
- padding: 1rem;
- overflow: hidden;
- max-width: 100%;
- max-height: 100%;
- flex: 0 1 30rem;
- background: #1c1c1e;
- border-left: 6px solid #2972ba;
- border-right: 6px solid #2972ba;
- box-shadow: 0 0 2px 0 #434349, 0 0 6px 0 black;
- border-radius: 6px; }
- .Dialog h1, .Dialog h2 {
- margin-top: 0; }
- .Dialog p:last-child {
- margin-bottom: 0; }
-
-/*
-// "toast"
-.NotifyMsg {
- position: fixed;
- bottom: dist(2);
- padding: dist(-1) dist(0);
-
- // center horizontally
- left: 50%;
- @include translate(-50%,0);
- // hack to remove blur in chrome
- -webkit-font-smoothing: subpixel-antialiased;
- -webkit-transform: translateZ(0) scale(1.0, 1.0);
-
- background: #37a349;
- &.error {
- background: #d03e42;
- }
-
- color: white;
- text-shadow: 0 0 2px black;
- box-shadow: 0 0 6px 0 rgba(black, .6);
- border-radius: 5px;
-
- max-width: 80%;
-
- @include media($phone) {
- width: calc(100% - 1rem);
- }
-
- transition: opacity .5s;
- opacity: 0;
- &.visible { opacity: 1 }
- &.hidden { display: none }
-}
-*/
html {
font-family: Arial, sans-serif;
color: #D0D0D0;
@@ -414,74 +418,141 @@ a:hover {
color: #5abfff;
text-decoration: underline; }
-.Box {
- display: block;
- max-width: 900px;
- margin-top: 1rem;
- padding: 0.61805rem 1rem;
- border-radius: 3px;
- background-color: rgba(255, 255, 255, 0.07); }
- @media screen and (max-width: 544px) {
- .Box {
- margin-top: 0.61805rem;
- padding: 0.23608rem 0.38198rem; } }
- .Box p:first-child {
- margin-top: 0; }
+.hidden {
+ display: none !important; }
-body {
+[onclick] {
+ cursor: pointer; }
+
+ul > * {
+ padding-top: .2em;
+ padding-bottom: .2em; }
+
+/* Main outer container */
+#outer {
+ display: flex;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ overflow: hidden;
+ flex-direction: row; }
+
+@media screen and (max-width: 544px) {
+ #outer {
+ display: block;
+ overflow-y: scroll; } }
+#menu {
+ flex: 0 0 15rem;
+ background: #3983CD; }
+ #menu > * {
+ display: block;
+ text-decoration: none;
+ padding: 0.61805rem 1rem;
+ white-space: nowrap;
+ word-wrap: normal;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none; }
+ #menu #brand {
+ color: white;
+ background: #2b6aa8;
+ font-size: 120%;
+ text-align: center;
+ position: relative;
+ transition: none;
+ font-weight: bold;
+ margin-bottom: 1rem; }
+ @media screen and (max-width: 544px) {
+ #menu #brand {
+ background: #3983CD;
+ cursor: pointer;
+ margin-bottom: 0.38198rem; }
+ #menu #brand::after {
+ position: absolute;
+ color: rgba(0, 0, 0, 0.4);
+ right: 1rem;
+ content: '▸';
+ top: 50%;
+ font-size: 120%;
+ font-weight: bold;
+ transform: translate(0, -50%) rotate(90deg); } }
+ #menu.expanded #brand {
+ background: #2b6aa8; }
+ @media screen and (max-width: 544px) {
+ #menu.expanded #brand:after {
+ transform: translate(-25%, -50%) rotate(-90deg); } }
+ #menu a {
+ font-size: 130%;
+ color: white;
+ transition: background-color 0.2s;
+ text-shadow: 0 0 5px rgba(0, 0, 0, 0.4); }
+ #menu a:hover, #menu a.selected {
+ background: #5badff;
+ text-shadow: 0 0 5px rgba(0, 0, 0, 0.6); }
+ #menu a.selected {
+ position: relative;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); }
+ #menu a:focus {
+ outline-color: #ff0099; }
+ #menu a::before {
+ vertical-align: -2px;
+ margin-left: 0;
+ margin-right: 15px; }
+ @media screen and (max-width: 544px) {
+ #menu a {
+ display: none; }
+ #menu a::before {
+ margin-left: 10px; } }
+ #menu.expanded a {
+ display: block; }
+ @media screen and (min-width: 545px) and (max-width: 1000px) {
+ #menu {
+ flex-basis: 10rem; }
+ #menu #brand {
+ font-size: 95%;
+ margin-bottom: 0.61805rem; }
+ #menu a {
+ font-size: 105%; }
+ #menu > * {
+ padding: 0.38198rem 0.61805rem; } }
+
+#content {
+ flex-grow: 1;
position: relative;
padding: 1rem;
overflow-y: auto; }
@media screen and (max-width: 544px) {
- body {
+ #content {
padding: 0.61805rem; } }
- body > * {
+ #content > * {
margin-left: auto;
margin-right: auto; }
-
-h1, h2 {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none; }
-
-h1 {
- text-align: center;
- font-size: 2.02729em;
- margin-top: 0;
- margin-bottom: 1rem; }
+ #content h1 {
+ text-align: center;
+ font-size: 2.2807em;
+ margin-top: 0;
+ margin-bottom: 1rem; }
+ #content h2 {
+ font-size: 1.42383em;
+ margin-bottom: 0.61805rem; }
@media screen and (max-width: 544px) {
- h1 {
- font-size: 1.42383em;
+ #content h1 {
+ font-size: 1.80203em;
+ margin-bottom: 0.61805rem; }
+ #content h2 {
+ font-size: 1.26563em;
margin-bottom: 0.61805rem; } }
- @media screen and (min-width: 545px) and (max-width: 1000px) {
- h1 {
- font-size: 1.80203em; } }
-
-h2 {
- font-size: 1.26563em;
- margin-bottom: 0.61805rem; }
-
-td, th {
- padding: 0.38198rem;
- white-space: nowrap; }
- @media screen and (max-width: 544px) {
- td, th {
- padding: 0.23608rem; } }
-
-tbody th {
- text-align: right;
- width: 130px;
- color: white; }
- @media screen and (max-width: 544px) {
- tbody th {
- width: auto; } }
-
-tbody td input[type="text"], tbody td input[type="number"] {
- width: 10em; }
- @media screen and (max-width: 544px) {
- tbody td input[type="text"], tbody td input[type="number"] {
- width: 8em; } }
+ #content td, #content th {
+ padding: 0.38198rem; }
+ #content tbody th {
+ text-align: right;
+ width: 160px;
+ color: white; }
#loader {
position: absolute;
@@ -496,20 +567,151 @@ tbody td input[type="text"], tbody td input[type="number"] {
#loader.show {
opacity: 1; }
-ul > * {
- padding-top: .1em;
- padding-bottom: .1em; }
+.Box {
+ display: block;
+ max-width: 900px;
+ margin-top: 1rem;
+ padding: 0.61805rem 1rem;
+ border-radius: 3px;
+ background-color: rgba(255, 255, 255, 0.07);
+ box-shadow: 0 0 4px black;
+ border: 1px solid #4f4f4f; }
+ .Box::after {
+ content: '';
+ display: block;
+ clear: both; }
+ @media screen and (max-width: 544px) {
+ .Box {
+ margin-top: 0.61805rem; } }
+ .Box h1, .Box h2 {
+ overflow: hidden; }
+ h1 + .Box {
+ margin-top: 0; }
+ .Box h2 {
+ margin-top: 0;
+ margin-bottom: 0 !important; }
+ .Box p:last-child {
+ margin-bottom: 0.5em; }
+ .Box.wide {
+ width: initial;
+ max-width: initial; }
+ .Box.medium {
+ max-width: 1200px; }
+ .Box.str {
+ position: relative; }
+ .Box.str .Row.buttons {
+ position: absolute; }
+ @media screen and (max-width: 544px) {
+ .Box.str .Row.buttons {
+ right: 1rem;
+ top: 1.8em;
+ margin: 1rem auto; } }
+ @media screen and (min-width: 545px) {
+ .Box.str .Row.buttons {
+ right: 0;
+ top: 0;
+ margin-top: 0.61805rem; } }
+ .Box.str.mobopen .Row.buttons {
+ top: 0;
+ margin-top: 0.61805rem; }
+ .Box .Row.explain {
+ max-width: 600px;
+ margin-left: 0; }
+ @media screen and (max-width: 544px) {
+ .Box .Row.explain {
+ margin-top: 60px; } }
+ .Box.mobopen .Row.explain {
+ margin-top: 12px; }
+ @media screen and (max-width: 544px) {
+ .Box.mobopen .Row.explain {
+ margin-top: 18px; } }
-#botnav {
- padding-top: 1.5em;
- text-align: center; }
- #botnav a {
- padding: 0 0.38198rem;
- text-decoration: underline; }
- #botnav a, #botnav a:visited, #botnav a:link {
- color: #2e4d6e; }
- #botnav a:hover {
- color: #5abfff; }
+@media screen and (max-width: 544px) {
+ .Box.mobcol h2 {
+ position: relative;
+ cursor: pointer;
+ padding-right: 1.3rem; }
+ .Box.mobcol h2::after {
+ position: absolute;
+ right: 0;
+ content: '▸';
+ top: 50%;
+ font-size: 120%;
+ font-weight: bold;
+ transform: translate(0, -50%) rotate(90deg); }
+ .Box.mobcol.expanded h2::after {
+ transform: translate(-25%, -50%) rotate(-90deg);
+ margin-bottom: 1rem; }
+ .Box.mobcol .Row {
+ display: none; }
+ .Box.mobcol #ap-box {
+ display: none; }
+ .Box.mobcol.expanded .Row {
+ display: flex; }
+ .Box.mobcol.expanded #ap-box {
+ display: block; } }
+.Modal {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: opacity .5s;
+ background: rgba(0, 0, 0, 0.65);
+ opacity: 0; }
+ .Modal.visible {
+ opacity: 1; }
+ .Modal.hidden {
+ display: none; }
+
+.Dialog {
+ margin: 0.61805rem;
+ padding: 1rem;
+ overflow: hidden;
+ max-width: 100%;
+ max-height: 100%;
+ flex: 0 1 30rem;
+ background: #1c1c1e;
+ border-left: 6px solid #2972ba;
+ border-right: 6px solid #2972ba;
+ border-top: 1px solid #2972ba;
+ border-bottom: 1px solid #2972ba;
+ box-shadow: 0 0 6px 0 black;
+ border-radius: 6px; }
+ .Dialog h1, .Dialog h2 {
+ margin-top: 0; }
+ .Dialog p:last-child {
+ margin-bottom: 0; }
+
+.NotifyMsg {
+ position: fixed;
+ top: 1.618rem;
+ right: 2.61792rem;
+ padding: 0.61805rem 1rem;
+ -webkit-font-smoothing: subpixel-antialiased;
+ -webkit-transform: translateZ(0) scale(1, 1);
+ background: #3887d0;
+ color: white;
+ text-shadow: 0 0 2px black;
+ box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.6);
+ border-radius: 5px;
+ max-width: 80%;
+ transition: opacity .5s;
+ opacity: 0; }
+ .NotifyMsg.error {
+ background: #d03e42; }
+ @media screen and (max-width: 544px) {
+ .NotifyMsg {
+ width: calc(100% - 1rem); } }
+ .NotifyMsg.visible {
+ opacity: 1; }
+ .NotifyMsg.hidden {
+ display: none; }
button, input[type=submit], .button {
text-align: center;
@@ -518,7 +720,6 @@ button, input[type=submit], .button {
border-radius: 2px;
padding: 0 0.6em;
border: 0 none;
- outline: 0 none !important;
line-height: 1.8em;
font-size: 1.1em;
margin-bottom: 3px;
@@ -527,7 +728,7 @@ button, input[type=submit], .button {
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
- text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.6);
+ text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.4);
background-color: #3983cd;
box-shadow: 0 3px 0 #265f98;
text-decoration: none !important; }
@@ -536,6 +737,9 @@ button, input[type=submit], .button {
top: 2px; }
button.narrow, input[type=submit].narrow, .button.narrow {
min-width: initial; }
+ button::before, input[type=submit]::before, .button::before {
+ vertical-align: -1px;
+ margin-left: 0; }
button, button:link, button:visited, input[type=submit], input[type=submit]:link, input[type=submit]:visited, .button, .button:link, .button:visited {
color: #FEFEFE; }
button:hover, button:active, button.active, button.selected, input[type=submit]:hover, input[type=submit]:active, input[type=submit].active, input[type=submit].selected, .button:hover, .button:active, .button.active, .button.selected {
@@ -545,6 +749,8 @@ button, input[type=submit], .button {
box-shadow: 0 3px 0 #154c80; }
button:active, input[type=submit]:active, .button:active {
box-shadow: 0 1px 0 #154c80; }
+ button:focus, input[type=submit]:focus, .button:focus {
+ outline-color: #ff0099; }
button, input[type=submit], .button {
text-align: center;
@@ -553,7 +759,6 @@ button, input[type=submit], .button {
border-radius: 2px;
padding: 0 0.6em;
border: 0 none;
- outline: 0 none !important;
line-height: 1.8em;
font-size: 1.1em;
margin-bottom: 3px;
@@ -562,7 +767,7 @@ button, input[type=submit], .button {
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
- text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.6);
+ text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.4);
background-color: #3983cd;
box-shadow: 0 3px 0 #265f98;
text-decoration: none !important; }
@@ -571,6 +776,9 @@ button, input[type=submit], .button {
top: 2px; }
button.narrow, input[type=submit].narrow, .button.narrow {
min-width: initial; }
+ button::before, input[type=submit]::before, .button::before {
+ vertical-align: -1px;
+ margin-left: 0; }
button, button:link, button:visited, input[type=submit], input[type=submit]:link, input[type=submit]:visited, .button, .button:link, .button:visited {
color: #FEFEFE; }
button:hover, button:active, button.active, button.selected, input[type=submit]:hover, input[type=submit]:active, input[type=submit].active, input[type=submit].selected, .button:hover, .button:active, .button.active, .button.selected {
@@ -580,25 +788,176 @@ button, input[type=submit], .button {
box-shadow: 0 3px 0 #154c80; }
button:active, input[type=submit]:active, .button:active {
box-shadow: 0 1px 0 #154c80; }
+ button:focus, input[type=submit]:focus, .button:focus {
+ outline-color: #ff0099; }
input[type="number"], input[type="password"], input[type="text"], textarea, select {
border: 0 none;
- border-bottom: 2px solid #214e7a;
- background-color: #303030;
+ border-bottom: 2px solid #2972ba;
+ background-color: #3c3c3c;
color: white;
padding: 6px;
line-height: 1em;
- outline: 0 none !important;
- -moz-outline: 0 none !important;
font-weight: normal; }
input[type="number"]:focus, input[type="number"]:hover, input[type="password"]:focus, input[type="password"]:hover, input[type="text"]:focus, input[type="text"]:hover, textarea:focus, textarea:hover, select:focus, select:hover {
- border-bottom-color: #2972ba; }
+ border-bottom-color: #2ea1f9; }
+
+.Row.checkbox {
+ line-height: 27px; }
+ .Row.checkbox .box {
+ overflow: hidden;
+ width: 27px;
+ height: 27px;
+ border: 1px solid #808080;
+ border-radius: 3px;
+ background: #3c3c3c;
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ color: #2ea1f9; }
+ .Row.checkbox .box::before {
+ font-weight: bold;
+ position: absolute;
+ content: '×';
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ line-height: 26px;
+ text-align: center;
+ font-size: 27px;
+ vertical-align: middle;
+ display: none; }
+ .Row.checkbox .box.checked::before {
+ display: block; }
+
+.Row.range .display {
+ margin-left: 1ex; }
+.Row.range label .display {
+ font-weight: normal; }
#psk-modal form > *, #wificonfbox form > * {
margin-right: 0.38198rem; }
#psk-modal form > *:last-child, #wificonfbox form > *:last-child {
margin-right: 0; }
+form {
+ border: 0 none;
+ margin: 0;
+ padding: 0;
+ text-decoration: none; }
+
+input[type="number"], input[type="password"], input[type="text"], textarea, select, label.select-wrap {
+ width: 250px; }
+
+input[type="number"], input.short {
+ width: 125px; }
+
+.Box.errors .list {
+ color: crimson;
+ font-weight: bold; }
+.Box.errors .lead {
+ color: white; }
+
+.Row {
+ vertical-align: middle;
+ margin: 12px auto;
+ text-align: left;
+ display: flex;
+ flex-direction: row;
+ align-items: center; }
+ .Row:first-child {
+ margin-top: 0; }
+ .Row:last-child {
+ margin-bottom: 0; }
+ .Row .spacer {
+ width: 160px; }
+ @media screen and (max-width: 544px) {
+ .Row .spacer {
+ display: none; } }
+ .Row.buttons {
+ margin: 16px auto; }
+ .Row.buttons input, .Row.buttons .button {
+ margin-right: 0.61805rem; }
+ .Row.centered {
+ justify-content: center; }
+ .Row.message {
+ font-size: 1em;
+ text-shadow: 1px 1px 3px black;
+ text-align: center; }
+ .Row.message.error {
+ color: crimson; }
+ .Row.message.ok {
+ color: #0fe851; }
+ .Row.separator {
+ padding-top: 14px;
+ border-top: 2px solid rgba(255, 255, 255, 0.1); }
+ .Row textarea {
+ display: inline-block;
+ vertical-align: top;
+ min-height: 10rem;
+ flex-grow: 1;
+ resize: vertical; }
+ .Row label {
+ font-weight: bold;
+ color: white;
+ display: inline-block;
+ width: 160px;
+ text-align: right;
+ text-shadow: 1px 1px 3px black;
+ padding: 8px;
+ align-self: flex-start;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ white-space: nowrap;
+ word-wrap: normal; }
+ .Row label.error {
+ color: crimson; }
+ .Row input[type="range"] {
+ width: 200px; }
+ @media screen and (max-width: 544px) {
+ .Row {
+ flex-direction: column;
+ margin: 6px auto; }
+ .Row.buttons, .Row.centered, .Row.checkbox {
+ flex-direction: row; }
+ .Row.buttons {
+ justify-content: center; }
+ .Row.buttons :last-child {
+ margin-right: 0; }
+ .Row label {
+ padding-left: 0;
+ text-align: left;
+ width: auto; }
+ .Row .checkbox-wrap {
+ order: 1;
+ text-align: left;
+ padding-bottom: 0;
+ border-radius: .4px;
+ width: auto; }
+ .Row .checkbox-wrap + label {
+ width: auto; }
+ .Row input[type="number"], .Row input[type="password"], .Row input[type="text"], .Row textarea, .Row input[type="range"], .Row textarea, .Row select {
+ width: 100%; } }
+
+form span.required {
+ color: red; }
+
+.RadioGroup {
+ display: inline-block;
+ line-height: 1.5em;
+ vertical-align: middle; }
+ .RadioGroup label {
+ width: auto;
+ text-align: left;
+ cursor: pointer;
+ font-weight: normal; }
+ .RadioGroup input[type="radio"] {
+ vertical-align: middle;
+ margin: 0 0 0 5px; }
+
#ap-list {
column-count: 3;
column-gap: 0;
@@ -610,17 +969,23 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele
#ap-list {
column-count: 1; } }
-#ap-loader, #ap-noscan {
+#ap-loader, #ap-noscan, #ap-scan {
background: rgba(255, 255, 255, 0.1);
border-radius: 5px;
padding: 0.38198rem;
- margin-bottom: 0.38198rem; }
+ margin-bottom: 0.38198rem;
+ margin-top: 0.38198rem; }
#ap-noscan {
font-weight: bold; }
#ap-box {
- padding-bottom: 0.38198rem; }
+ padding-top: 0.38198rem; }
+ #ap-box label {
+ display: block;
+ color: white;
+ font-weight: bold;
+ margin-bottom: 0.23608rem; }
#psk-modal form {
display: flex;
@@ -629,6 +994,40 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele
#psk-modal form input[type=password] {
min-width: 5rem; }
+.AP .inner, .AP-preview .wrap {
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ position: relative;
+ border-radius: 3px;
+ color: #222;
+ background: #afafaf;
+ transition: background-color 0.5s;
+ display: flex; }
+ .AP .inner:active, .AP-preview .wrap:active {
+ left: 0;
+ top: 1px; }
+ .AP .inner:hover, .AP-preview .wrap:hover {
+ background: white; }
+ .AP .inner .rssi, .AP-preview .wrap .rssi {
+ min-width: 2.5rem;
+ flex: 0 0 15%;
+ text-align: right; }
+ .AP .inner .rssi:after, .AP-preview .wrap .rssi:after {
+ padding-left: 0.09018rem;
+ content: '%';
+ font-size: 0.88889em; }
+ .AP .inner .essid, .AP-preview .wrap .essid {
+ flex: 1 1 70%;
+ min-width: 0;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-weight: bold; }
+ .AP .inner .auth, .AP-preview .wrap .auth {
+ flex: 0 0 15%; }
+
.AP {
break-inside: avoid-column;
max-width: 500px;
@@ -637,50 +1036,62 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele
background: #42a6f9 !important;
cursor: default;
top: 0 !important; }
- .AP .inner {
- cursor: pointer;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- position: relative;
- border-radius: 3px;
- color: #222;
- background: #afafaf;
- transition: background-color 0.5s;
- display: flex; }
- .AP .inner:active {
- left: 0;
- top: 1px; }
- .AP .inner:hover {
- background: white; }
- .AP .inner > * {
+ .AP .inner > * {
+ padding: 0.61805rem;
+ white-space: nowrap;
+ word-wrap: normal; }
+
+.AP-preview-nil {
+ padding: 8px;
+ border-radius: 5px;
+ border: 1px dashed #ddd;
+ width: 250px;
+ height: 94px; }
+
+.AP-preview .wrap {
+ flex-direction: row;
+ background: #ddd !important;
+ cursor: default;
+ top: 0 !important;
+ overflow: hidden; }
+ .AP-preview .wrap .inner {
+ display: flex;
+ flex-direction: column; }
+ .AP-preview .wrap .inner > * {
padding: 0.61805rem;
white-space: nowrap;
word-wrap: normal; }
- .AP .inner .rssi {
- min-width: 2.5rem;
- flex: 0 0 15%;
- text-align: right; }
- .AP .inner .rssi:after {
- padding-left: 0.09018rem;
- content: '%';
- font-size: 0.88889em; }
- .AP .inner .essid {
- flex: 1 1 70%;
- min-width: 0;
- text-overflow: ellipsis;
- overflow: hidden;
- font-weight: bold; }
- .AP .inner .auth {
- flex: 0 0 15%; }
-
-.page-term h1 {
+ .AP-preview .wrap .forget {
+ align-self: stretch;
+ line-height: 100%;
+ padding: 0.61805rem;
+ border-left: 1px solid #bbb;
+ display: flex;
+ align-items: center;
+ font-size: 28px; }
+ .AP-preview .wrap .forget, .AP-preview .wrap .forget:hover {
+ color: black;
+ text-decoration: none; }
+ .AP-preview .wrap .forget:hover {
+ background: #dc4a6a;
+ color: white;
+ border-left: 1px solid #666;
+ border-bottom-right-radius: 3px;
+ border-top-right-radius: 3px; }
+ .AP-preview .wrap .forget:active {
+ position: relative;
+ padding-top: calc(0.61805rem + 1px); }
+ .AP-preview .wrap .essid, .AP-preview .wrap .passwd, .AP-preview .wrap .nopasswd {
+ padding-bottom: 0; }
+ .AP-preview .wrap .x-passwd {
+ font-family: monospace; }
+
+body.term h1 {
font-size: 1.80203em; }
@media screen and (max-width: 544px) {
- .page-term h1 {
+ body.term h1 {
font-size: 1.42383em; } }
-.page-term #screen {
+body.term #screen {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -692,25 +1103,33 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele
padding: 6px;
display: inline-block;
border: 2px solid #3983CD; }
- .page-term #screen span {
+ body.term #screen span {
white-space: pre;
cursor: pointer; }
- .page-term #screen span:hover {
+ body.term #screen span:hover {
outline: 1px solid rgba(255, 255, 255, 0.4); }
@media screen and (max-width: 544px) {
- .page-term #screen span:hover {
+ body.term #screen span:hover {
outline: 0 none; } }
-.page-term #buttons {
+body.term #buttons {
margin-top: 10px;
white-space: nowrap; }
- .page-term #buttons button {
+ body.term #buttons button {
margin: 0 3px;
- padding: 10px 0;
- width: 18%;
- max-width: 65px;
- min-width: initial;
+ padding: 8px 5px;
+ min-width: 65px;
cursor: pointer;
font-weight: bold; }
+body.term #botnav {
+ padding-top: 1.5em;
+ text-align: center; }
+ body.term #botnav a {
+ padding: 0 0.38198rem;
+ text-decoration: underline; }
+ body.term #botnav a, body.term #botnav a:visited, body.term #botnav a:link {
+ color: #2e4d6e; }
+ body.term #botnav a:hover {
+ color: #5abfff; }
#termwrap {
text-align: center; }
@@ -724,7 +1143,8 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele
float: right;
height: 130px; }
.page-about #logo2 {
- max-width: 100%; }
+ max-width: 100%;
+ margin: 1rem; }
.page-about td {
white-space: normal; }
@@ -856,13 +1276,15 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele
@media screen and (min-width: 545px) {
.mq-phone {
- display: none; } }
+ display: none !important; } }
@media screen and (max-width: 544px) {
- .mq-tablet-min {
- display: none; } }
+ .mq-tablet-min, .mq-no-phone {
+ display: none !important; } }
@media screen and (min-width: 1001px) {
.mq-tablet-max {
- display: none; } }
+ display: none !important; } }
@media screen and (max-width: 1000px) {
.mq-normal-min {
- display: none; } }
+ display: none !important; } }
+
+/*# sourceMappingURL=app.css.map */
diff --git a/html_orig/css/app.css.bak b/html_orig/css/app.css.bak
new file mode 100644
index 0000000..5baf098
--- /dev/null
+++ b/html_orig/css/app.css.bak
@@ -0,0 +1,1284 @@
+@charset "UTF-8";
+/* normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS and IE text size adjust after device orientation change,
+ * without disabling user zoom.
+ */
+*, *:before, *:after {
+ box-sizing: border-box; }
+
+html {
+ font-family: sans-serif;
+ /* 1 */
+ -ms-text-size-adjust: 100%;
+ /* 2 */
+ -webkit-text-size-adjust: 100%;
+ /* 2 */ }
+
+/**
+ * Remove default margin.
+ */
+body {
+ margin: 0; }
+
+/* HTML5 display definitions
+ ========================================================================== */
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+figure,
+nav
+ {
+ display: block; }
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+canvas,
+progress
+ {
+ display: inline-block;
+ /* 1 */
+ vertical-align: baseline;
+ /* 2 */ }
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
+ */
+[hidden]
+ {
+ display: none; }
+
+/* Links
+ ========================================================================== */
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+a {
+ background-color: transparent; }
+
+/**
+ * Improve readability of focused elements when they are also in an
+ * active/hover state.
+ */
+a:active,
+a:hover {
+ outline: 0; }
+
+/* Text-level semantics
+ ========================================================================== */
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+b
+ {
+ font-weight: bold; }
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0; }
+
+h2 {
+ font-size: 2em;
+ margin: 0.67em 0; }
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+small {
+ font-size: 80%; }
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline; }
+
+sup {
+ top: -0.5em; }
+
+sub {
+ bottom: -0.25em; }
+
+/* Embedded content
+ ========================================================================== */
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+img {
+ border: 0; }
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+svg:not(:root) {
+ overflow: hidden; }
+
+/* Grouping content
+ ========================================================================== */
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+/**
+ * Address differences between Firefox and other browsers.
+ */
+hr {
+ box-sizing: content-box;
+ height: 0; }
+
+/**
+ * Contain overflow in all browsers.
+ */
+pre {
+ overflow: auto; }
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+code,
+pre
+ {
+ font-family: monospace;
+ font-size: 1em; }
+
+/* Forms
+ ========================================================================== */
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+button,
+input,
+select,
+textarea {
+ color: inherit;
+ /* 1 */
+ font: inherit;
+ /* 2 */
+ margin: 0;
+ /* 3 */ }
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+button {
+ overflow: visible; }
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+button,
+select {
+ text-transform: none; }
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+button,
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button;
+ /* 2 */
+ cursor: pointer;
+ /* 3 */ }
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+button[disabled],
+html input[disabled] {
+ cursor: default; }
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0; }
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+input {
+ line-height: normal; }
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box;
+ /* 1 */
+ padding: 0;
+ /* 2 */ }
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
+ */
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+/**
+ * Define consistent border, margin, and padding.
+ */
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+legend {
+ border: 0;
+ /* 1 */
+ padding: 0;
+ /* 2 */ }
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+textarea {
+ overflow: auto; }
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+/* Tables
+ ========================================================================== */
+/**
+ * Remove most spacing between table cells.
+ */
+table {
+ border-collapse: collapse;
+ border-spacing: 0; }
+
+td,
+th {
+ padding: 0; }
+
+@font-face {
+ font-family: 'fontello';
+ src: url("data:application/octet-stream;base64,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") format("woff"), url("data:application/octet-stream;base64,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I7AEQ2CwASuwBSVhsAUlsAJiILAAUFiwQGBZZrABY7AEJmEgsAQlYGQjsAMlYGRQWCEbIyFZIyAgsAQmI0ZhOFktsDcssAAWICAgsAUmIC5HI0cjYSM8OC2wOCywABYgsAgjQiAgIEYjR7ABKyNhOC2wOSywABawAyWwAiVHI0cjYbAAVFguIDwjIRuwAiWwAiVHI0cjYSCwBSWwBCVHI0cjYbAGJbAFJUmwAiVhuQgACABjYyMgWGIbIVljuAQAYiCwAFBYsEBgWWawAWNgIy4jICA8ijgjIVktsDossAAWILAIQyAuRyNHI2EgYLAgYGawAmIgsABQWLBAYFlmsAFjIyAgPIo4LbA7LCMgLkawAiVGUlggPFkusSsBFCstsDwsIyAuRrACJUZQWCA8WS6xKwEUKy2wPSwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xKwEUKy2wPiywNSsjIC5GsAIlRlJYIDxZLrErARQrLbA/LLA2K4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrErARQrsARDLrArKy2wQCywABawBCWwBCYgLkcjRyNhsAlDKyMgPCAuIzixKwEUKy2wQSyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2GwAiVGYTgjIDwjOBshICBGI0ewASsjYTghWbErARQrLbBCLLA1Ky6xKwEUKy2wQyywNishIyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=") format("truetype"); }
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/*
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'fontello';
+ src: url('../font/fontello.svg?60007293#fontello') format('svg');
+ }
+}
+*/
+[class^="icn-"]:before, [class*=" icn-"]:before {
+ font-family: "fontello";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ }
+
+.icn-persist:before {
+ content: '\e800'; }
+
+/* '' */
+.icn-ok:before {
+ content: '\e801'; }
+
+/* '' */
+.icn-donate:before {
+ content: '\e802'; }
+
+/* '' */
+.icn-back:before {
+ content: '\e80b'; }
+
+/* '' */
+.icn-about:before {
+ content: '\e81f'; }
+
+/* '' */
+.icn-help:before {
+ content: '\e826'; }
+
+/* '' */
+.icn-network:before {
+ content: '\e82e'; }
+
+/* '' */
+.icn-restore:before {
+ content: '\e836'; }
+
+/* '' */
+.icn-terminal:before {
+ content: '\f108'; }
+
+/* '' */
+.icn-github:before {
+ content: '\f113'; }
+
+/* '' */
+.icn-wifi:before {
+ content: '\f1eb'; }
+
+/* '' */
+html {
+ box-sizing: border-box; }
+
+*, *::after, *::before {
+ box-sizing: inherit; }
+
+html {
+ font-family: Arial, sans-serif;
+ color: #D0D0D0;
+ background: #131315; }
+
+html, body {
+ border: 0 none;
+ margin: 0;
+ padding: 0;
+ text-decoration: none;
+ width: 100%;
+ height: 100%;
+ overflow: hidden; }
+
+a, a:visited, a:link {
+ cursor: pointer;
+ color: #5abfff;
+ text-decoration: none; }
+
+a:hover {
+ color: #5abfff;
+ text-decoration: underline; }
+
+.hidden {
+ display: none !important; }
+
+[onclick] {
+ cursor: pointer; }
+
+ul > * {
+ padding-top: .2em;
+ padding-bottom: .2em; }
+
+/* Main outer container */
+#outer {
+ display: flex;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ overflow: hidden;
+ flex-direction: row; }
+
+@media screen and (max-width: 544px) {
+ #outer {
+ display: block;
+ overflow-y: scroll; } }
+#menu {
+ flex: 0 0 15rem;
+ background: #3983CD; }
+ #menu > * {
+ display: block;
+ text-decoration: none;
+ padding: 0.61805rem 1rem;
+ white-space: nowrap;
+ word-wrap: normal;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none; }
+ #menu #brand {
+ color: white;
+ background: #2b6aa8;
+ font-size: 120%;
+ text-align: center;
+ position: relative;
+ transition: none;
+ font-weight: bold;
+ margin-bottom: 1rem; }
+ @media screen and (max-width: 544px) {
+ #menu #brand {
+ background: #3983CD;
+ cursor: pointer;
+ margin-bottom: 0.38198rem; }
+ #menu #brand::after {
+ position: absolute;
+ color: rgba(0, 0, 0, 0.4);
+ right: 1rem;
+ content: '▸';
+ top: 50%;
+ font-size: 120%;
+ font-weight: bold;
+ transform: translate(0, -50%) rotate(90deg); } }
+ #menu.expanded #brand {
+ background: #2b6aa8; }
+ @media screen and (max-width: 544px) {
+ #menu.expanded #brand:after {
+ transform: translate(-25%, -50%) rotate(-90deg); } }
+ #menu a {
+ font-size: 130%;
+ color: white;
+ transition: background-color 0.2s;
+ text-shadow: 0 0 5px rgba(0, 0, 0, 0.4); }
+ #menu a:hover, #menu a.selected {
+ background: #5badff;
+ text-shadow: 0 0 5px rgba(0, 0, 0, 0.6); }
+ #menu a.selected {
+ position: relative;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); }
+ #menu a:focus {
+ outline-color: #ff0099; }
+ #menu a::before {
+ vertical-align: -2px;
+ margin-left: 0;
+ margin-right: 15px; }
+ @media screen and (max-width: 544px) {
+ #menu a {
+ display: none; }
+ #menu a::before {
+ margin-left: 10px; } }
+ #menu.expanded a {
+ display: block; }
+ @media screen and (min-width: 545px) and (max-width: 1000px) {
+ #menu {
+ flex-basis: 10rem; }
+ #menu #brand {
+ font-size: 95%;
+ margin-bottom: 0.61805rem; }
+ #menu a {
+ font-size: 105%; }
+ #menu > * {
+ padding: 0.38198rem 0.61805rem; } }
+
+#content {
+ flex-grow: 1;
+ position: relative;
+ padding: 1rem;
+ overflow-y: auto; }
+ @media screen and (max-width: 544px) {
+ #content {
+ padding: 0.61805rem; } }
+ #content > * {
+ margin-left: auto;
+ margin-right: auto; }
+ #content h1 {
+ text-align: center;
+ font-size: 2.2807em;
+ margin-top: 0;
+ margin-bottom: 1rem; }
+ #content h2 {
+ font-size: 1.42383em;
+ margin-bottom: 0.61805rem; }
+ @media screen and (max-width: 544px) {
+ #content h1 {
+ font-size: 1.80203em;
+ margin-bottom: 0.61805rem; }
+ #content h2 {
+ font-size: 1.26563em;
+ margin-bottom: 0.61805rem; } }
+ #content td, #content th {
+ padding: 0.38198rem; }
+ #content tbody th {
+ text-align: right;
+ width: 160px;
+ color: white; }
+
+#loader {
+ position: absolute;
+ right: 1.618rem;
+ top: 1.618rem;
+ transition: opacity .2s;
+ opacity: 0; }
+ @media screen and (max-width: 544px) {
+ #loader {
+ top: 1rem;
+ right: 1rem; } }
+ #loader.show {
+ opacity: 1; }
+
+.Box {
+ display: block;
+ max-width: 900px;
+ margin-top: 1rem;
+ padding: 0.61805rem 1rem;
+ border-radius: 3px;
+ background-color: rgba(255, 255, 255, 0.07);
+ box-shadow: 0 0 4px black;
+ border: 1px solid #4f4f4f; }
+ @media screen and (max-width: 544px) {
+ .Box {
+ margin-top: 0.61805rem; } }
+ .Box h1, .Box h2 {
+ overflow: hidden; }
+ h1 + .Box {
+ margin-top: 0; }
+ .Box h2 {
+ margin-top: 0;
+ margin-bottom: 0 !important; }
+ .Box.wide {
+ width: initial;
+ max-width: initial; }
+ .Box.medium {
+ max-width: 1200px; }
+ .Box.str {
+ position: relative; }
+ .Box.str .Row.buttons {
+ position: absolute; }
+ @media screen and (max-width: 544px) {
+ .Box.str .Row.buttons {
+ right: 1rem;
+ top: 1.8em;
+ margin: 1rem auto; } }
+ @media screen and (min-width: 545px) {
+ .Box.str .Row.buttons {
+ right: 0;
+ top: 0;
+ margin-top: 0.61805rem; } }
+ .Box.str.mobopen .Row.buttons {
+ top: 0;
+ margin-top: 0.61805rem; }
+ .Box .Row.explain {
+ max-width: 600px;
+ margin-left: 0; }
+ @media screen and (max-width: 544px) {
+ .Box .Row.explain {
+ margin-top: 60px; } }
+ .Box.mobopen .Row.explain {
+ margin-top: 12px; }
+ @media screen and (max-width: 544px) {
+ .Box.mobopen .Row.explain {
+ margin-top: 18px; } }
+
+@media screen and (max-width: 544px) {
+ .Box.mobcol h2 {
+ position: relative;
+ cursor: pointer;
+ padding-right: 1.3rem; }
+ .Box.mobcol h2::after {
+ position: absolute;
+ right: 0;
+ content: '▸';
+ top: 50%;
+ font-size: 120%;
+ font-weight: bold;
+ transform: translate(0, -50%) rotate(90deg); }
+ .Box.mobcol.expanded h2::after {
+ transform: translate(-25%, -50%) rotate(-90deg);
+ margin-bottom: 1rem; }
+ .Box.mobcol .Row {
+ display: none; }
+ .Box.mobcol #ap-box {
+ display: none; }
+ .Box.mobcol.expanded .Row {
+ display: flex; }
+ .Box.mobcol.expanded #ap-box {
+ display: block; } }
+.Modal {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: opacity .5s;
+ background: rgba(0, 0, 0, 0.65);
+ opacity: 0; }
+ .Modal.visible {
+ opacity: 1; }
+ .Modal.hidden {
+ display: none; }
+
+.Dialog {
+ margin: 0.61805rem;
+ padding: 1rem;
+ overflow: hidden;
+ max-width: 100%;
+ max-height: 100%;
+ flex: 0 1 30rem;
+ background: #1c1c1e;
+ border-left: 6px solid #2972ba;
+ border-right: 6px solid #2972ba;
+ border-top: 1px solid #2972ba;
+ border-bottom: 1px solid #2972ba;
+ box-shadow: 0 0 6px 0 black;
+ border-radius: 6px; }
+ .Dialog h1, .Dialog h2 {
+ margin-top: 0; }
+ .Dialog p:last-child {
+ margin-bottom: 0; }
+
+.NotifyMsg {
+ position: fixed;
+ top: 1.618rem;
+ right: 2.61792rem;
+ padding: 0.61805rem 1rem;
+ -webkit-font-smoothing: subpixel-antialiased;
+ -webkit-transform: translateZ(0) scale(1, 1);
+ background: #3887d0;
+ color: white;
+ text-shadow: 0 0 2px black;
+ box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.6);
+ border-radius: 5px;
+ max-width: 80%;
+ transition: opacity .5s;
+ opacity: 0; }
+ .NotifyMsg.error {
+ background: #d03e42; }
+ @media screen and (max-width: 544px) {
+ .NotifyMsg {
+ width: calc(100% - 1rem); } }
+ .NotifyMsg.visible {
+ opacity: 1; }
+ .NotifyMsg.hidden {
+ display: none; }
+
+button, input[type=submit], .button {
+ text-align: center;
+ cursor: pointer;
+ display: inline-block;
+ border-radius: 2px;
+ padding: 0 0.6em;
+ border: 0 none;
+ line-height: 1.8em;
+ font-size: 1.1em;
+ margin-bottom: 3px;
+ min-width: 5em;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.4);
+ background-color: #3983cd;
+ box-shadow: 0 3px 0 #265f98;
+ text-decoration: none !important; }
+ button:active, input[type=submit]:active, .button:active {
+ position: relative;
+ top: 2px; }
+ button.narrow, input[type=submit].narrow, .button.narrow {
+ min-width: initial; }
+ button::before, input[type=submit]::before, .button::before {
+ vertical-align: -1px;
+ margin-left: 0; }
+ button, button:link, button:visited, input[type=submit], input[type=submit]:link, input[type=submit]:visited, .button, .button:link, .button:visited {
+ color: #FEFEFE; }
+ button:hover, button:active, button.active, button.selected, input[type=submit]:hover, input[type=submit]:active, input[type=submit].active, input[type=submit].selected, .button:hover, .button:active, .button.active, .button.selected {
+ background-color: #2076C6;
+ color: #FEFEFE; }
+ button:hover, button.selected, button.active, input[type=submit]:hover, input[type=submit].selected, input[type=submit].active, .button:hover, .button.selected, .button.active {
+ box-shadow: 0 3px 0 #154c80; }
+ button:active, input[type=submit]:active, .button:active {
+ box-shadow: 0 1px 0 #154c80; }
+ button:focus, input[type=submit]:focus, .button:focus {
+ outline-color: #ff0099; }
+
+button, input[type=submit], .button {
+ text-align: center;
+ cursor: pointer;
+ display: inline-block;
+ border-radius: 2px;
+ padding: 0 0.6em;
+ border: 0 none;
+ line-height: 1.8em;
+ font-size: 1.1em;
+ margin-bottom: 3px;
+ min-width: 5em;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.4);
+ background-color: #3983cd;
+ box-shadow: 0 3px 0 #265f98;
+ text-decoration: none !important; }
+ button:active, input[type=submit]:active, .button:active {
+ position: relative;
+ top: 2px; }
+ button.narrow, input[type=submit].narrow, .button.narrow {
+ min-width: initial; }
+ button::before, input[type=submit]::before, .button::before {
+ vertical-align: -1px;
+ margin-left: 0; }
+ button, button:link, button:visited, input[type=submit], input[type=submit]:link, input[type=submit]:visited, .button, .button:link, .button:visited {
+ color: #FEFEFE; }
+ button:hover, button:active, button.active, button.selected, input[type=submit]:hover, input[type=submit]:active, input[type=submit].active, input[type=submit].selected, .button:hover, .button:active, .button.active, .button.selected {
+ background-color: #2076C6;
+ color: #FEFEFE; }
+ button:hover, button.selected, button.active, input[type=submit]:hover, input[type=submit].selected, input[type=submit].active, .button:hover, .button.selected, .button.active {
+ box-shadow: 0 3px 0 #154c80; }
+ button:active, input[type=submit]:active, .button:active {
+ box-shadow: 0 1px 0 #154c80; }
+ button:focus, input[type=submit]:focus, .button:focus {
+ outline-color: #ff0099; }
+
+input[type="number"], input[type="password"], input[type="text"], textarea, select {
+ border: 0 none;
+ border-bottom: 2px solid #2972ba;
+ background-color: #3c3c3c;
+ color: white;
+ padding: 6px;
+ line-height: 1em;
+ font-weight: normal; }
+ input[type="number"]:focus, input[type="number"]:hover, input[type="password"]:focus, input[type="password"]:hover, input[type="text"]:focus, input[type="text"]:hover, textarea:focus, textarea:hover, select:focus, select:hover {
+ border-bottom-color: #2ea1f9; }
+
+.Row.checkbox {
+ line-height: 27px; }
+ .Row.checkbox .box {
+ overflow: hidden;
+ width: 27px;
+ height: 27px;
+ border: 1px solid #808080;
+ border-radius: 3px;
+ background: #3c3c3c;
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ color: #2ea1f9; }
+ .Row.checkbox .box::before {
+ font-weight: bold;
+ position: absolute;
+ content: '×';
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ line-height: 26px;
+ text-align: center;
+ font-size: 27px;
+ vertical-align: middle;
+ display: none; }
+ .Row.checkbox .box.checked::before {
+ display: block; }
+
+.Row.range .display {
+ margin-left: 1ex; }
+.Row.range label .display {
+ font-weight: normal; }
+
+#psk-modal form > *, #wificonfbox form > * {
+ margin-right: 0.38198rem; }
+ #psk-modal form > *:last-child, #wificonfbox form > *:last-child {
+ margin-right: 0; }
+
+form {
+ border: 0 none;
+ margin: 0;
+ padding: 0;
+ text-decoration: none; }
+
+input[type="number"], input[type="password"], input[type="text"], textarea, select, label.select-wrap {
+ width: 250px; }
+
+input[type="number"], input.short {
+ width: 125px; }
+
+.Box.errors .list {
+ color: crimson;
+ font-weight: bold; }
+.Box.errors .lead {
+ color: white; }
+
+.Row {
+ vertical-align: middle;
+ margin: 12px auto;
+ text-align: left;
+ display: flex;
+ flex-direction: row;
+ align-items: center; }
+ .Row:first-child {
+ margin-top: 0; }
+ .Row:last-child {
+ margin-bottom: 0; }
+ .Row .spacer {
+ width: 160px; }
+ @media screen and (max-width: 544px) {
+ .Row .spacer {
+ display: none; } }
+ .Row.buttons {
+ margin: 16px auto; }
+ .Row.buttons input, .Row.buttons .button {
+ margin-right: 0.61805rem; }
+ .Row.centered {
+ justify-content: center; }
+ .Row.message {
+ font-size: 1em;
+ text-shadow: 1px 1px 3px black;
+ text-align: center; }
+ .Row.message.error {
+ color: crimson; }
+ .Row.message.ok {
+ color: #0fe851; }
+ .Row.separator {
+ padding-top: 14px;
+ border-top: 2px solid rgba(255, 255, 255, 0.1); }
+ .Row textarea {
+ display: inline-block;
+ vertical-align: top;
+ min-height: 10rem;
+ flex-grow: 1;
+ resize: vertical; }
+ .Row label {
+ font-weight: bold;
+ color: white;
+ display: inline-block;
+ width: 160px;
+ text-align: right;
+ text-shadow: 1px 1px 3px black;
+ padding: 8px;
+ align-self: flex-start;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ white-space: nowrap;
+ word-wrap: normal; }
+ .Row label.error {
+ color: crimson; }
+ .Row input[type="range"] {
+ width: 200px; }
+ @media screen and (max-width: 544px) {
+ .Row {
+ flex-direction: column;
+ margin: 6px auto; }
+ .Row.buttons, .Row.centered, .Row.checkbox {
+ flex-direction: row; }
+ .Row.buttons {
+ justify-content: center; }
+ .Row.buttons :last-child {
+ margin-right: 0; }
+ .Row label {
+ padding-left: 0;
+ text-align: left;
+ width: auto; }
+ .Row .checkbox-wrap {
+ order: 1;
+ text-align: left;
+ padding-bottom: 0;
+ border-radius: .4px;
+ width: auto; }
+ .Row .checkbox-wrap + label {
+ width: auto; }
+ .Row input[type="number"], .Row input[type="password"], .Row input[type="text"], .Row textarea, .Row input[type="range"], .Row textarea, .Row select {
+ width: 100%; } }
+
+form span.required {
+ color: red; }
+
+.RadioGroup {
+ display: inline-block;
+ line-height: 1.5em;
+ vertical-align: middle; }
+ .RadioGroup label {
+ width: auto;
+ text-align: left;
+ cursor: pointer;
+ font-weight: normal; }
+ .RadioGroup input[type="radio"] {
+ vertical-align: middle;
+ margin: 0 0 0 5px; }
+
+#ap-list {
+ column-count: 3;
+ column-gap: 0;
+ margin: 0 -0.23608rem; }
+ @media screen and (min-width: 545px) and (max-width: 1000px) {
+ #ap-list {
+ column-count: 2; } }
+ @media screen and (max-width: 544px) {
+ #ap-list {
+ column-count: 1; } }
+
+#ap-loader, #ap-noscan, #ap-scan {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 5px;
+ padding: 0.38198rem;
+ margin-bottom: 0.38198rem; }
+
+#ap-noscan {
+ font-weight: bold; }
+
+#ap-box {
+ padding-top: 0.38198rem; }
+ #ap-box label {
+ display: block;
+ color: white;
+ font-weight: bold;
+ margin-bottom: 0.23608rem; }
+
+#psk-modal form {
+ display: flex;
+ align-items: center;
+ margin: 0.38198rem; }
+ #psk-modal form input[type=password] {
+ min-width: 5rem; }
+
+.AP .inner, .AP-preview .wrap {
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ position: relative;
+ border-radius: 3px;
+ color: #222;
+ background: #afafaf;
+ transition: background-color 0.5s;
+ display: flex; }
+ .AP .inner:active, .AP-preview .wrap:active {
+ left: 0;
+ top: 1px; }
+ .AP .inner:hover, .AP-preview .wrap:hover {
+ background: white; }
+ .AP .inner .rssi, .AP-preview .wrap .rssi {
+ min-width: 2.5rem;
+ flex: 0 0 15%;
+ text-align: right; }
+ .AP .inner .rssi:after, .AP-preview .wrap .rssi:after {
+ padding-left: 0.09018rem;
+ content: '%';
+ font-size: 0.88889em; }
+ .AP .inner .essid, .AP-preview .wrap .essid {
+ flex: 1 1 70%;
+ min-width: 0;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-weight: bold; }
+ .AP .inner .auth, .AP-preview .wrap .auth {
+ flex: 0 0 15%; }
+
+.AP {
+ break-inside: avoid-column;
+ max-width: 500px;
+ padding: 0.23608rem; }
+ .AP.selected .inner {
+ background: #42a6f9 !important;
+ cursor: default;
+ top: 0 !important; }
+ .AP .inner > * {
+ padding: 0.61805rem;
+ white-space: nowrap;
+ word-wrap: normal; }
+
+.AP-preview-nil {
+ padding: 8px;
+ border-radius: 5px;
+ border: 1px dashed #ddd;
+ width: 250px;
+ height: 94px; }
+
+.AP-preview .wrap {
+ flex-direction: row;
+ background: #ddd !important;
+ cursor: default;
+ top: 0 !important;
+ overflow: hidden; }
+ .AP-preview .wrap .inner {
+ display: flex;
+ flex-direction: column; }
+ .AP-preview .wrap .inner > * {
+ padding: 0.61805rem;
+ white-space: nowrap;
+ word-wrap: normal; }
+ .AP-preview .wrap .forget {
+ align-self: stretch;
+ line-height: 100%;
+ padding: 0.61805rem;
+ border-left: 1px solid #bbb;
+ display: flex;
+ align-items: center;
+ font-size: 28px; }
+ .AP-preview .wrap .forget, .AP-preview .wrap .forget:hover {
+ color: black;
+ text-decoration: none; }
+ .AP-preview .wrap .forget:hover {
+ background: #dc4a6a;
+ color: white;
+ border-left: 1px solid #666;
+ border-bottom-right-radius: 3px;
+ border-top-right-radius: 3px; }
+ .AP-preview .wrap .forget:active {
+ position: relative;
+ padding-top: calc(0.61805rem + 1px); }
+ .AP-preview .wrap .essid, .AP-preview .wrap .passwd, .AP-preview .wrap .nopasswd {
+ padding-bottom: 0; }
+ .AP-preview .wrap .x-passwd {
+ font-family: monospace; }
+
+body.term h1 {
+ font-size: 1.80203em; }
+ @media screen and (max-width: 544px) {
+ body.term h1 {
+ font-size: 1.42383em; } }
+body.term #screen {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ font-family: monospace;
+ font-size: 16pt;
+ white-space: nowrap;
+ background: #111213;
+ padding: 6px;
+ display: inline-block;
+ border: 2px solid #3983CD; }
+ body.term #screen span {
+ white-space: pre;
+ cursor: pointer; }
+ body.term #screen span:hover {
+ outline: 1px solid rgba(255, 255, 255, 0.4); }
+ @media screen and (max-width: 544px) {
+ body.term #screen span:hover {
+ outline: 0 none; } }
+body.term #buttons {
+ margin-top: 10px;
+ white-space: nowrap; }
+ body.term #buttons button {
+ margin: 0 3px;
+ padding: 10px 0;
+ width: 18%;
+ max-width: 65px;
+ min-width: initial;
+ cursor: pointer;
+ font-weight: bold; }
+body.term #botnav {
+ padding-top: 1.5em;
+ text-align: center; }
+ body.term #botnav a {
+ padding: 0 0.38198rem;
+ text-decoration: underline; }
+ body.term #botnav a, body.term #botnav a:visited, body.term #botnav a:link {
+ color: #2e4d6e; }
+ body.term #botnav a:hover {
+ color: #5abfff; }
+
+#termwrap {
+ text-align: center; }
+
+.page-about .Box {
+ padding-left: 1rem;
+ padding-right: 1rem; }
+ .page-about .Box a {
+ font-weight: bold; }
+.page-about #logo {
+ float: right;
+ height: 130px; }
+.page-about #logo2 {
+ max-width: 100%; }
+.page-about td {
+ white-space: normal; }
+
+.colorprev {
+ margin-top: 0.38198rem;
+ margin-bottom: 0.38198rem; }
+ .colorprev span {
+ display: inline-block;
+ width: 2em;
+ padding: 0.38198rem 0;
+ text-align: center; }
+
+.ansiref, .ansiref td, .ansiref th {
+ border: 1px solid #666; }
+.ansiref th, .ansiref td {
+ white-space: normal; }
+.ansiref th {
+ background-color: rgba(255, 255, 255, 0.1); }
+.ansiref td:nth-child(1) {
+ font-family: monospace; }
+.ansiref.w100 {
+ width: 100%; }
+ .ansiref.w100 td:nth-child(1) {
+ width: 9em; }
+ .ansiref.w100 td:nth-child(2) {
+ width: 8em; }
+
+.fg0 {
+ color: #111213; }
+
+.bg0 {
+ background-color: #111213; }
+
+.fg1 {
+ color: #CC0000; }
+
+.bg1 {
+ background-color: #CC0000; }
+
+.fg2 {
+ color: #4E9A06; }
+
+.bg2 {
+ background-color: #4E9A06; }
+
+.fg3 {
+ color: #C4A000; }
+
+.bg3 {
+ background-color: #C4A000; }
+
+.fg4 {
+ color: #3465A4; }
+
+.bg4 {
+ background-color: #3465A4; }
+
+.fg5 {
+ color: #75507B; }
+
+.bg5 {
+ background-color: #75507B; }
+
+.fg6 {
+ color: #06989A; }
+
+.bg6 {
+ background-color: #06989A; }
+
+.fg7 {
+ color: #D3D7CF; }
+
+.bg7 {
+ background-color: #D3D7CF; }
+
+.fg8 {
+ color: #555753; }
+
+.bg8 {
+ background-color: #555753; }
+
+.fg9 {
+ color: #EF2929; }
+
+.bg9 {
+ background-color: #EF2929; }
+
+.fg10 {
+ color: #8AE234; }
+
+.bg10 {
+ background-color: #8AE234; }
+
+.fg11 {
+ color: #FCE94F; }
+
+.bg11 {
+ background-color: #FCE94F; }
+
+.fg12 {
+ color: #729FCF; }
+
+.bg12 {
+ background-color: #729FCF; }
+
+.fg13 {
+ color: #AD7FA8; }
+
+.bg13 {
+ background-color: #AD7FA8; }
+
+.fg14 {
+ color: #34E2E2; }
+
+.bg14 {
+ background-color: #34E2E2; }
+
+.fg15 {
+ color: #EEEEEC; }
+
+.bg15 {
+ background-color: #EEEEEC; }
+
+.fg8, .fg9, .fg10, .fg11, .fg12, .fg13, .fg14, .fg15 {
+ font-weight: bold; }
+
+.nb {
+ font-weight: normal !important; }
+
+@media screen and (min-width: 545px) {
+ .mq-phone {
+ display: none !important; } }
+@media screen and (max-width: 544px) {
+ .mq-tablet-min, .mq-no-phone {
+ display: none !important; } }
+@media screen and (min-width: 1001px) {
+ .mq-tablet-max {
+ display: none !important; } }
+@media screen and (max-width: 1000px) {
+ .mq-normal-min {
+ display: none !important; } }
+
+/*# sourceMappingURL=app.css.map */
diff --git a/html_orig/dump_js_lang.php b/html_orig/dump_js_lang.php
new file mode 100755
index 0000000..41cf168
--- /dev/null
+++ b/html_orig/dump_js_lang.php
@@ -0,0 +1,21 @@
+title . ' :: ' . APP_NAME;
+$_GET['BODYCLASS'] = $_pages[$_GET['page']]->bodyclass;
+
+require __DIR__ . '/pages/_head.php';
+$_pf = __DIR__ . '/pages/'.$_GET['page'].'.php';
+if (file_exists($_pf)) {
+ $f = file_get_contents($_pf);
+ $reps = DEBUG ? require(__DIR__ . '/_debug_replacements.php') : [];
+ $str = str_replace(array_keys($reps), array_values($reps), $f);
+ include_str($str);
+} else {
+ echo "404";
+}
+
+require __DIR__ . '/pages/_tail.php';
diff --git a/html_orig/js/app.js b/html_orig/js/app.js
index 7aed10d..7ed55a3 100644
--- a/html_orig/js/app.js
+++ b/html_orig/js/app.js
@@ -393,8 +393,9 @@
return cb;
};
// Toggle class
- cb.toggleClass = function (classes) {
- classHelper(classes, 'toggle', nodes);
+ cb.toggleClass = function (classes, set) {
+ var method = ((typeof set === 'undefined') ? 'toggle' : (+set ? 'add' : 'remove'));
+ classHelper(classes, method, nodes);
return cb;
};
// Has class
@@ -704,18 +705,27 @@
function mk(e) {return document.createElement(e)}
/** Find one by query */
-function qq(s) {return document.querySelector(s)}
+function qs(s) {return document.querySelector(s)}
/** Find all by query */
-function qa(s) {return document.querySelectorAll(s)}
+function qsa(s) {return document.querySelectorAll(s)}
/** Convert any to bool safely */
function bool(x) {
return (x === 1 || x === '1' || x === true || x === 'true');
}
-function intval(x) {
- return parseInt(x);
+/**
+ * Filter 'spacebar' and 'return' from keypress handler,
+ * and when they're pressed, fire the callback.
+ * use $(...).on('keypress', cr(handler))
+ */
+function cr(hdl) {
+ return function(e) {
+ if (e.which == 10 || e.which == 13 || e.which == 32) {
+ hdl();
+ }
+ };
}
/** Extend an objects with options */
@@ -738,23 +748,23 @@ function rgxe(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
+/** Format number to N decimal places, output as string */
function numfmt(x, places) {
var pow = Math.pow(10, places);
return Math.round(x*pow) / pow;
}
-function estimateLoadTime(fs, n) {
- return (1000/fs)*n+1500;
-}
-
+/** Get millisecond timestamp */
function msNow() {
return +(new Date);
}
+/** Get ms elapsed since msNow() */
function msElapsed(start) {
return msNow() - start;
}
+/** Shim for log base 10 */
Math.log10 = Math.log10 || function(x) {
return Math.log(x) / Math.LN10;
};
@@ -795,6 +805,25 @@ String.prototype.format = function () {
return out;
};
+/** HTML escape */
+function e(str) {
+ return $.htmlEscape(str);
+}
+
+/** Check for undefined */
+function undef(x) {
+ return typeof x == 'undefined';
+}
+
+/** Safe json parse */
+function jsp() {
+ try {
+ return JSON.parse(e);
+ } catch(e) {
+ console.error(e);
+ return null;
+ }
+}
/** Module for toggling a modal overlay */
(function () {
var modal = {};
@@ -836,8 +865,73 @@ String.prototype.format = function () {
window.Modal = modal;
})();
+(function (nt) {
+ var sel = '#notif';
+
+ var hideTmeo1; // timeout to start hiding (transition)
+ var hideTmeo2; // timeout to add the hidden class
+
+ nt.show = function (message, timeout) {
+ $(sel).html(message);
+ Modal.show(sel);
+
+ clearTimeout(hideTmeo1);
+ clearTimeout(hideTmeo2);
+
+ if (undef(timeout)) timeout = 2500;
+
+ hideTmeo1 = setTimeout(nt.hide, timeout);
+ };
+
+ nt.hide = function () {
+ var $m = $(sel);
+ $m.removeClass('visible');
+ hideTmeo2 = setTimeout(function () {
+ $m.addClass('hidden');
+ }, 250); // transition time
+ };
+
+ nt.init = function() {
+ $(sel).on('click', function() {
+ nt.hide(this);
+ });
+ };
+})(window.Notify = {});
/** Global generic init */
$.ready(function () {
+ // Checkbox UI (checkbox CSS and hidden input with int value)
+ $('.Row.checkbox').forEach(function(x) {
+ var inp = x.querySelector('input');
+ var box = x.querySelector('.box');
+
+ $(box).toggleClass('checked', inp.value);
+
+ var hdl = function() {
+ inp.value = 1 - inp.value;
+ $(box).toggleClass('checked', inp.value)
+ };
+
+ $(x).on('click', hdl).on('keypress', cr(hdl));
+ });
+
+ // Expanding boxes on mobile
+ $('.Box.mobcol').forEach(function(x) {
+ var h = x.querySelector('h2');
+
+ var hdl = function() {
+ $(x).toggleClass('expanded');
+ };
+ $(h).on('click', hdl).on('keypress', cr(hdl));
+ });
+
+ qsa('form').forEach(function(x) {
+ $(x).on('keypress', function(e) {
+ if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) {
+ x.submit();
+ }
+ })
+ });
+
// loader dots...
setInterval(function () {
$('.anim-dots').each(function (x) {
@@ -850,12 +944,13 @@ $.ready(function () {
// flipping number boxes with the mouse wheel
$('input[type=number]').on('mousewheel', function(e) {
- var val = +$(this).val();
+ var $this = $(this);
+ var val = +$this.val();
if (isNaN(val)) val = 1;
- var step = +($(this).attr('step') || 1);
- var min = +$(this).attr('min');
- var max = +$(this).attr('max');
+ var step = +($this.attr('step') || 1);
+ var min = +$this.attr('min');
+ var max = +$this.attr('max');
if(e.wheelDelta > 0) {
val += step;
} else {
@@ -864,28 +959,243 @@ $.ready(function () {
if (typeof min != 'undefined') val = Math.max(val, +min);
if (typeof max != 'undefined') val = Math.min(val, +max);
- $(this).val(val);
+ $this.val(val);
if ("createEvent" in document) {
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
- $(this)[0].dispatchEvent(evt);
+ $this[0].dispatchEvent(evt);
} else {
- $(this)[0].fireEvent("onchange");
+ $this[0].fireEvent("onchange");
}
e.preventDefault();
});
+ var errAt = location.search.indexOf('err=');
+ if (errAt !== -1 && qs('.Box.errors')) {
+ var errs = location.search.substr(errAt+4).split(',');
+ var hres = [];
+ errs.forEach(function(er) {
+ var lbl = qs('label[for="'+er+'"]');
+ if (lbl) {
+ lbl.classList.add('error');
+ hres.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, ''));
+ } else {
+ hres.push(er);
+ }
+ });
+
+ qs('.Box.errors .list').innerHTML = hres.join(', ');
+ qs('.Box.errors').classList.remove('hidden');
+ }
+
Modal.init();
+ Notify.init();
+
+ // remove tabindixes from h2 if wide
+ if (window.innerWidth > 550) {
+ qsa('.Box h2').forEach(function (x) {
+ x.removeAttribute('tabindex');
+ });
+
+ var br = qs('#brand');
+ br && br.removeAttribute('tabindex');
+ }
});
$._loader = function(vis) {
- if(vis)
- $('#loader').addClass('show');
- else
- $('#loader').removeClass('show');
+ $('#loader').toggleClass('show', vis);
};
+// Generated from PHP locale file
+var _tr = {
+ "wifi.connected_ip_is": "Connected, IP is ",
+ "wifi.not_conn": "Not connected."
+};
+
+function tr(key) { return _tr[key] || '?'+key+'?'; }
+(function(w) {
+ var authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'];
+ var curSSID;
+
+ // Get XX % for a slider input
+ function rangePt(inp) {
+ return Math.round(((inp.value / inp.max)*100)) + '%';
+ }
+
+ // Display selected STA SSID etc
+ function selectSta(name, password, ip) {
+ $('#sta_ssid').val(name);
+ $('#sta_password').val(password);
+
+ $('#sta-nw').toggleClass('hidden', name.length == 0);
+ $('#sta-nw-nil').toggleClass('hidden', name.length > 0);
+
+ $('#sta-nw .essid').html(e(name));
+ var nopw = undef(password) || password.length == 0;
+ $('#sta-nw .x-passwd').html(e(password));
+ $('#sta-nw .passwd').toggleClass('hidden', nopw);
+ $('#sta-nw .nopasswd').toggleClass('hidden', !nopw);
+ $('#sta-nw .ip').html(ip.length>0 ? tr('wifi.connected_ip_is')+ip : tr('wifi.not_conn'));
+ }
+
+ function submitPskModal(e, open) {
+ var passwd = $('#conn-passwd').val();
+ var ssid = $('#conn-ssid').val();
+
+ if (open || passwd.length) {
+ $('#sta_password').val(passwd);
+ $('#sta_ssid').val(ssid);
+ selectSta(ssid, passwd, '');
+ }
+
+ if (e) e.preventDefault();
+ Modal.hide('#psk-modal');
+ return false;
+ }
+
+ /** Update display for received response */
+ function onScan(resp, status) {
+ //var ap_json = {
+ // "result": {
+ // "inProgress": "0",
+ // "APs": [
+ // {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
+ // {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
+ // ]
+ // }
+ //};
+
+ if (status != 200) {
+ // bad response
+ rescan(5000); // wait 5sm then retry
+ return;
+ }
+
+ try {
+ resp = JSON.parse(resp);
+ } catch (e) {
+ console.log(e);
+ rescan(5000);
+ return;
+ }
+
+ var done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0);
+ rescan(done ? 15000 : 1000);
+ if (!done) return; // no redraw yet
+
+ // clear the AP list
+ var $list = $('#ap-list');
+ // remove old APs
+ $('#ap-list .AP').remove();
+
+ $list.toggleClass('hidden', !done);
+ $('#ap-loader').toggleClass('hidden', done);
+
+ // scan done
+ resp.result.APs.sort(function (a, b) {
+ return b.rssi - a.rssi;
+ }).forEach(function (ap) {
+ ap.enc = parseInt(ap.enc);
+
+ if (ap.enc > 4) return; // hide unsupported auths
+ WiFi.scan_url = '/cfg/wifi/scan';
+
+ var item = mk('div');
+
+ var $item = $(item)
+ .data('ssid', ap.essid)
+ .data('pwd', ap.enc)
+ .attr('tabindex', 0)
+ .addClass('AP');
+
+ // mark current SSID
+ if (ap.essid == curSSID) {
+ $item.addClass('selected');
+ }
+
+ var inner = mk('div');
+ $(inner).addClass('inner')
+ .htmlAppend(''.format(ap.rssi_perc))
+ .htmlAppend('{0}
'.format($.htmlEscape(ap.essid)))
+ .htmlAppend('{0}
'.format(authStr[ap.enc]));
+
+ $item.on('click', function () {
+ var $th = $(this);
+
+ var ssid = $th.data('ssid');
+
+ $('#conn-ssid').val(ssid);
+ $('#conn-passwd').val('');
+
+ if (+$th.data('pwd')) {
+ // this AP needs a password
+ Modal.show('#psk-modal');
+ $('#conn-passwd')[0].focus();
+ } else {
+ //Modal.show('#reset-modal');
+ submitPskModal(null, true);
+ }
+ });
+
+
+ item.appendChild(inner);
+ $list[0].appendChild(item);
+ });
+ }
+
+ function startScanning() {
+ $('#ap-loader').removeClass('hidden');
+ $('#ap-scan').addClass('hidden');
+ $('#ap-loader .anim-dots').html('.');
+ scanAPs();
+ }
+
+ /** Ask the CGI what APs are visible (async) */
+ function scanAPs() {
+ $.get('http://'+_root+w.scan_url, onScan);
+ }
+
+ function rescan(time) {
+ setTimeout(scanAPs, time);
+ }
+
+ /** Set up the WiFi page */
+ function wifiInit(cfg) {
+ // Hide what should be hidden in this mode
+ cfg.mode = +cfg.mode;
+
+ $('#ap-noscan').toggleClass('hidden', cfg.mode != 2);
+ $('#ap-scan').toggleClass('hidden', cfg.mode == 2);
+
+ // Update slider value displays
+ $('.Row.range').forEach(function(x) {
+ var inp = x.querySelector('input');
+ var disp1 = x.querySelector('.x-disp1');
+ var disp2 = x.querySelector('.x-disp2');
+ var t = rangePt(inp);
+ $(disp1).html(t);
+ $(disp2).html(t);
+ $(inp).on('input', function() {
+ t = rangePt(inp);
+ $(disp1).html(t);
+ $(disp2).html(t);
+ });
+ });
+
+ // Forget STA credentials
+ $('#forget-sta').on('click', function() {
+ selectSta('', '', '');
+ return false;
+ });
+
+ selectSta(cfg.sta_ssid, cfg.sta_password, cfg.sta_active_ip);
+ curSSID = cfg.sta_active_ssid;
+ }
+
+ w.init = wifiInit;
+ w.startScanning = startScanning;
+})(window.WiFi = {});
(function() {
/**
* Terminal module
@@ -1023,7 +1333,7 @@ $._loader = function(vis) {
H = obj.h;
/* Build screen & show */
- var e, cell, scr = qq('#screen');
+ var e, cell, scr = qs('#screen');
// Empty the screen node
while (scr.firstChild) scr.removeChild(scr.firstChild);
@@ -1088,7 +1398,7 @@ $._loader = function(vis) {
console.warn("SOCKET CLOSED, code "+evt.code+". Reconnecting...");
setTimeout(function() {
init();
- }, 2000);
+ }, 1000);
}
function onMessage(evt) {
@@ -1169,7 +1479,7 @@ $._loader = function(vis) {
}
});
- qa('#buttons button').forEach(function(s) {
+ qsa('#buttons button').forEach(function(s) {
s.addEventListener('click', function() {
sendBtnMsg(+this.dataset['n']);
});
@@ -1187,183 +1497,5 @@ $._loader = function(vis) {
Term.init(obj);
Conn.init();
Input.init();
- }
-})();
-/** Wifi page */
-(function () {
- var authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'];
- var curSSID;
-
- /** Update display for received response */
- function onScan(resp, status) {
- if (status != 200) {
- // bad response
- rescan(5000); // wait 5sm then retry
- return;
- }
-
- resp = JSON.parse(resp);
-
- var done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0);
- rescan(done ? 15000 : 1000);
- if (!done) return; // no redraw yet
-
- // clear the AP list
- var $list = $('#ap-list');
- // remove old APs
- $('.AP').remove();
-
- $list.toggle(done);
- $('#ap-loader').toggle(!done);
-
- // scan done
- resp.result.APs.sort(function (a, b) {
- return b.rssi - a.rssi;
- }).forEach(function (ap) {
- ap.enc = intval(ap.enc);
-
- if (ap.enc > 4) return; // hide unsupported auths
-
- var item = document.createElement('div');
-
- var $item = $(item)
- .data('ssid', ap.essid)
- .data('pwd', ap.enc != 0)
- .addClass('AP');
-
- // mark current SSID
- if (ap.essid == curSSID) {
- $item.addClass('selected');
- }
-
- var inner = document.createElement('div');
- $(inner).addClass('inner')
- .htmlAppend(''.format(ap.rssi_perc))
- .htmlAppend('{0}
'.format($.htmlEscape(ap.essid)))
- .htmlAppend('{0}
'.format(authStr[ap.enc]));
-
- $item.on('click', function () {
- var $th = $(this);
-
- // populate the form
- $('#conn-essid').val($th.data('ssid'));
- $('#conn-passwd').val(''); // clear
-
- if ($th.data('pwd')) {
- // this AP needs a password
- Modal.show('#psk-modal');
- } else {
- Modal.show('#reset-modal');
- $('#conn-form').submit();
- }
- });
-
-
- item.appendChild(inner);
- $list[0].appendChild(item);
- });
- }
-
- /** Ask the CGI what APs are visible (async) */
- function scanAPs() {
- $.get('http://'+_root+'/wifi/scan', onScan);
- }
-
- function rescan(time) {
- setTimeout(scanAPs, time);
- }
-
- /** Set up the WiFi page */
- window.wifiInit = function (obj) {
- //var ap_json = {
- // "result": {
- // "inProgress": "0",
- // "APs": [
- // {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
- // {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
- // ]
- // }
- //};
-
- // Hide what should be hidden in this mode
- $('.x-hide-'+obj.mode).addClass('hidden');
- obj.mode = +obj.mode;
-
- // Channel writable only in AP mode
- if (obj.mode != 2) $('#channel').attr('readonly', 1);
-
- curSSID = obj.staSSID;
-
- // add SSID to the opmode field
- if (curSSID) {
- var box = $('#opmodebox');
- box.html(box.html() + ' (' + curSSID + ')');
- }
-
- // hide IP if IP not received
- if (!obj.staIP) $('.x-hide-noip').addClass('hidden');
-
- // scan if not AP
- if (obj.mode != 2) {
- scanAPs();
- }
-
- $('#modeswitch').html([
- 'Client+AP AP only ',
- 'Client+AP ',
- 'Client only AP only '
- ][obj.mode-1]);
- };
-
- window.wifiConn = function () {
- var xhr = new XMLHttpRequest();
- var abortTmeo;
-
- function getStatus() {
- xhr.open("GET", 'http://'+_root+"/wifi/connstatus");
- xhr.onreadystatechange = function () {
- if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300) {
- clearTimeout(abortTmeo);
- var data = JSON.parse(xhr.responseText);
- var done = false;
- var msg = '...';
-
- if (data.status == "idle") {
- msg = "Preparing to connect";
- }
- else if (data.status == "success") {
- msg = "Connected! Received IP " + data.ip + ".";
- done = true;
- }
- else if (data.status == "working") {
- msg = "Connecting to selected AP";
- }
- else if (data.status == "fail") {
- msg = "Connection failed, check your password and try again.";
- done = true;
- }
-
- $("#status").html(msg);
-
- if (done) {
- $('#backbtn').removeClass('hidden');
- $('.anim-dots').addClass('hidden');
- } else {
- window.setTimeout(getStatus, 1000);
- }
- }
- };
-
- abortTmeo = setTimeout(function () {
- xhr.abort();
- $("#status").html("Telemetry lost, try reconnecting to the AP.");
- $('#backbtn').removeClass('hidden');
- $('.anim-dots').addClass('hidden');
- }, 4000);
-
- xhr.send();
- }
-
- getStatus();
};
})();
diff --git a/html_orig/jssrc/appcommon.js b/html_orig/jssrc/appcommon.js
index 09fd0b1..0e5616b 100644
--- a/html_orig/jssrc/appcommon.js
+++ b/html_orig/jssrc/appcommon.js
@@ -1,5 +1,38 @@
/** Global generic init */
$.ready(function () {
+ // Checkbox UI (checkbox CSS and hidden input with int value)
+ $('.Row.checkbox').forEach(function(x) {
+ var inp = x.querySelector('input');
+ var box = x.querySelector('.box');
+
+ $(box).toggleClass('checked', inp.value);
+
+ var hdl = function() {
+ inp.value = 1 - inp.value;
+ $(box).toggleClass('checked', inp.value)
+ };
+
+ $(x).on('click', hdl).on('keypress', cr(hdl));
+ });
+
+ // Expanding boxes on mobile
+ $('.Box.mobcol').forEach(function(x) {
+ var h = x.querySelector('h2');
+
+ var hdl = function() {
+ $(x).toggleClass('expanded');
+ };
+ $(h).on('click', hdl).on('keypress', cr(hdl));
+ });
+
+ qsa('form').forEach(function(x) {
+ $(x).on('keypress', function(e) {
+ if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) {
+ x.submit();
+ }
+ })
+ });
+
// loader dots...
setInterval(function () {
$('.anim-dots').each(function (x) {
@@ -12,12 +45,13 @@ $.ready(function () {
// flipping number boxes with the mouse wheel
$('input[type=number]').on('mousewheel', function(e) {
- var val = +$(this).val();
+ var $this = $(this);
+ var val = +$this.val();
if (isNaN(val)) val = 1;
- var step = +($(this).attr('step') || 1);
- var min = +$(this).attr('min');
- var max = +$(this).attr('max');
+ var step = +($this.attr('step') || 1);
+ var min = +$this.attr('min');
+ var max = +$this.attr('max');
if(e.wheelDelta > 0) {
val += step;
} else {
@@ -26,25 +60,51 @@ $.ready(function () {
if (typeof min != 'undefined') val = Math.max(val, +min);
if (typeof max != 'undefined') val = Math.min(val, +max);
- $(this).val(val);
+ $this.val(val);
if ("createEvent" in document) {
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
- $(this)[0].dispatchEvent(evt);
+ $this[0].dispatchEvent(evt);
} else {
- $(this)[0].fireEvent("onchange");
+ $this[0].fireEvent("onchange");
}
e.preventDefault();
});
+ var errAt = location.search.indexOf('err=');
+ if (errAt !== -1 && qs('.Box.errors')) {
+ var errs = location.search.substr(errAt+4).split(',');
+ var hres = [];
+ errs.forEach(function(er) {
+ var lbl = qs('label[for="'+er+'"]');
+ if (lbl) {
+ lbl.classList.add('error');
+ hres.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, ''));
+ } else {
+ hres.push(er);
+ }
+ });
+
+ qs('.Box.errors .list').innerHTML = hres.join(', ');
+ qs('.Box.errors').classList.remove('hidden');
+ }
+
Modal.init();
+ Notify.init();
+
+ // remove tabindixes from h2 if wide
+ if (window.innerWidth > 550) {
+ qsa('.Box h2').forEach(function (x) {
+ x.removeAttribute('tabindex');
+ });
+
+ var br = qs('#brand');
+ br && br.removeAttribute('tabindex');
+ }
});
$._loader = function(vis) {
- if(vis)
- $('#loader').addClass('show');
- else
- $('#loader').removeClass('show');
+ $('#loader').toggleClass('show', vis);
};
diff --git a/html_orig/jssrc/chibi.js b/html_orig/jssrc/chibi.js
index 63dfa77..db51e2c 100755
--- a/html_orig/jssrc/chibi.js
+++ b/html_orig/jssrc/chibi.js
@@ -393,8 +393,9 @@
return cb;
};
// Toggle class
- cb.toggleClass = function (classes) {
- classHelper(classes, 'toggle', nodes);
+ cb.toggleClass = function (classes, set) {
+ var method = ((typeof set === 'undefined') ? 'toggle' : (+set ? 'add' : 'remove'));
+ classHelper(classes, method, nodes);
return cb;
};
// Has class
diff --git a/html_orig/jssrc/lang.js b/html_orig/jssrc/lang.js
new file mode 100644
index 0000000..6ef4755
--- /dev/null
+++ b/html_orig/jssrc/lang.js
@@ -0,0 +1,7 @@
+// Generated from PHP locale file
+var _tr = {
+ "wifi.connected_ip_is": "Connected, IP is ",
+ "wifi.not_conn": "Not connected."
+};
+
+function tr(key) { return _tr[key] || '?'+key+'?'; }
diff --git a/html_orig/jssrc/notif.js b/html_orig/jssrc/notif.js
new file mode 100644
index 0000000..ad08e51
--- /dev/null
+++ b/html_orig/jssrc/notif.js
@@ -0,0 +1,32 @@
+(function (nt) {
+ var sel = '#notif';
+
+ var hideTmeo1; // timeout to start hiding (transition)
+ var hideTmeo2; // timeout to add the hidden class
+
+ nt.show = function (message, timeout) {
+ $(sel).html(message);
+ Modal.show(sel);
+
+ clearTimeout(hideTmeo1);
+ clearTimeout(hideTmeo2);
+
+ if (undef(timeout)) timeout = 2500;
+
+ hideTmeo1 = setTimeout(nt.hide, timeout);
+ };
+
+ nt.hide = function () {
+ var $m = $(sel);
+ $m.removeClass('visible');
+ hideTmeo2 = setTimeout(function () {
+ $m.addClass('hidden');
+ }, 250); // transition time
+ };
+
+ nt.init = function() {
+ $(sel).on('click', function() {
+ nt.hide(this);
+ });
+ };
+})(window.Notify = {});
diff --git a/html_orig/jssrc/term.js b/html_orig/jssrc/term.js
index b9253d6..6206c85 100644
--- a/html_orig/jssrc/term.js
+++ b/html_orig/jssrc/term.js
@@ -135,7 +135,7 @@
H = obj.h;
/* Build screen & show */
- var e, cell, scr = qq('#screen');
+ var e, cell, scr = qs('#screen');
// Empty the screen node
while (scr.firstChild) scr.removeChild(scr.firstChild);
@@ -281,7 +281,7 @@
}
});
- qa('#buttons button').forEach(function(s) {
+ qsa('#buttons button').forEach(function(s) {
s.addEventListener('click', function() {
sendBtnMsg(+this.dataset['n']);
});
@@ -299,5 +299,5 @@
Term.init(obj);
Conn.init();
Input.init();
- }
+ };
})();
diff --git a/html_orig/jssrc/utils.js b/html_orig/jssrc/utils.js
index 4a7fd0b..6fa4b7d 100755
--- a/html_orig/jssrc/utils.js
+++ b/html_orig/jssrc/utils.js
@@ -2,18 +2,27 @@
function mk(e) {return document.createElement(e)}
/** Find one by query */
-function qq(s) {return document.querySelector(s)}
+function qs(s) {return document.querySelector(s)}
/** Find all by query */
-function qa(s) {return document.querySelectorAll(s)}
+function qsa(s) {return document.querySelectorAll(s)}
/** Convert any to bool safely */
function bool(x) {
return (x === 1 || x === '1' || x === true || x === 'true');
}
-function intval(x) {
- return parseInt(x);
+/**
+ * Filter 'spacebar' and 'return' from keypress handler,
+ * and when they're pressed, fire the callback.
+ * use $(...).on('keypress', cr(handler))
+ */
+function cr(hdl) {
+ return function(e) {
+ if (e.which == 10 || e.which == 13 || e.which == 32) {
+ hdl();
+ }
+ };
}
/** Extend an objects with options */
@@ -36,23 +45,23 @@ function rgxe(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
+/** Format number to N decimal places, output as string */
function numfmt(x, places) {
var pow = Math.pow(10, places);
return Math.round(x*pow) / pow;
}
-function estimateLoadTime(fs, n) {
- return (1000/fs)*n+1500;
-}
-
+/** Get millisecond timestamp */
function msNow() {
return +(new Date);
}
+/** Get ms elapsed since msNow() */
function msElapsed(start) {
return msNow() - start;
}
+/** Shim for log base 10 */
Math.log10 = Math.log10 || function(x) {
return Math.log(x) / Math.LN10;
};
@@ -93,3 +102,22 @@ String.prototype.format = function () {
return out;
};
+/** HTML escape */
+function e(str) {
+ return $.htmlEscape(str);
+}
+
+/** Check for undefined */
+function undef(x) {
+ return typeof x == 'undefined';
+}
+
+/** Safe json parse */
+function jsp() {
+ try {
+ return JSON.parse(e);
+ } catch(e) {
+ console.error(e);
+ return null;
+ }
+}
diff --git a/html_orig/jssrc/wifi.js b/html_orig/jssrc/wifi.js
index dd7f370..ece92c7 100644
--- a/html_orig/jssrc/wifi.js
+++ b/html_orig/jssrc/wifi.js
@@ -1,17 +1,68 @@
-/** Wifi page */
-(function () {
+(function(w) {
var authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'];
var curSSID;
+ // Get XX % for a slider input
+ function rangePt(inp) {
+ return Math.round(((inp.value / inp.max)*100)) + '%';
+ }
+
+ // Display selected STA SSID etc
+ function selectSta(name, password, ip) {
+ $('#sta_ssid').val(name);
+ $('#sta_password').val(password);
+
+ $('#sta-nw').toggleClass('hidden', name.length == 0);
+ $('#sta-nw-nil').toggleClass('hidden', name.length > 0);
+
+ $('#sta-nw .essid').html(e(name));
+ var nopw = undef(password) || password.length == 0;
+ $('#sta-nw .x-passwd').html(e(password));
+ $('#sta-nw .passwd').toggleClass('hidden', nopw);
+ $('#sta-nw .nopasswd').toggleClass('hidden', !nopw);
+ $('#sta-nw .ip').html(ip.length>0 ? tr('wifi.connected_ip_is')+ip : tr('wifi.not_conn'));
+ }
+
+ function submitPskModal(e, open) {
+ var passwd = $('#conn-passwd').val();
+ var ssid = $('#conn-ssid').val();
+
+ if (open || passwd.length) {
+ $('#sta_password').val(passwd);
+ $('#sta_ssid').val(ssid);
+ selectSta(ssid, passwd, '');
+ }
+
+ if (e) e.preventDefault();
+ Modal.hide('#psk-modal');
+ return false;
+ }
+
/** Update display for received response */
function onScan(resp, status) {
+ //var ap_json = {
+ // "result": {
+ // "inProgress": "0",
+ // "APs": [
+ // {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
+ // {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
+ // ]
+ // }
+ //};
+
if (status != 200) {
// bad response
rescan(5000); // wait 5sm then retry
return;
}
- resp = JSON.parse(resp);
+ try {
+ resp = JSON.parse(resp);
+ } catch (e) {
+ console.log(e);
+ rescan(5000);
+ return;
+ }
var done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0);
rescan(done ? 15000 : 1000);
@@ -20,62 +71,73 @@
// clear the AP list
var $list = $('#ap-list');
// remove old APs
- $('.AP').remove();
+ $('#ap-list .AP').remove();
- $list.toggle(done);
- $('#ap-loader').toggle(!done);
+ $list.toggleClass('hidden', !done);
+ $('#ap-loader').toggleClass('hidden', done);
// scan done
resp.result.APs.sort(function (a, b) {
- return b.rssi - a.rssi;
- }).forEach(function (ap) {
- ap.enc = intval(ap.enc);
-
- if (ap.enc > 4) return; // hide unsupported auths
-
- var item = document.createElement('div');
-
- var $item = $(item)
- .data('ssid', ap.essid)
- .data('pwd', ap.enc != 0)
- .addClass('AP');
-
- // mark current SSID
- if (ap.essid == curSSID) {
- $item.addClass('selected');
+ return b.rssi - a.rssi;
+ }).forEach(function (ap) {
+ ap.enc = parseInt(ap.enc);
+
+ if (ap.enc > 4) return; // hide unsupported auths
+ WiFi.scan_url = '/cfg/wifi/scan';
+
+ var item = mk('div');
+
+ var $item = $(item)
+ .data('ssid', ap.essid)
+ .data('pwd', ap.enc)
+ .attr('tabindex', 0)
+ .addClass('AP');
+
+ // mark current SSID
+ if (ap.essid == curSSID) {
+ $item.addClass('selected');
+ }
+
+ var inner = mk('div');
+ $(inner).addClass('inner')
+ .htmlAppend(''.format(ap.rssi_perc))
+ .htmlAppend('{0}
'.format($.htmlEscape(ap.essid)))
+ .htmlAppend('{0}
'.format(authStr[ap.enc]));
+
+ $item.on('click', function () {
+ var $th = $(this);
+
+ var ssid = $th.data('ssid');
+
+ $('#conn-ssid').val(ssid);
+ $('#conn-passwd').val('');
+
+ if (+$th.data('pwd')) {
+ // this AP needs a password
+ Modal.show('#psk-modal');
+ $('#conn-passwd')[0].focus();
+ } else {
+ //Modal.show('#reset-modal');
+ submitPskModal(null, true);
}
+ });
- var inner = document.createElement('div');
- $(inner).addClass('inner')
- .htmlAppend(''.format(ap.rssi_perc))
- .htmlAppend('{0}
'.format($.htmlEscape(ap.essid)))
- .htmlAppend('{0}
'.format(authStr[ap.enc]));
-
- $item.on('click', function () {
- var $th = $(this);
-
- // populate the form
- $('#conn-essid').val($th.data('ssid'));
- $('#conn-passwd').val(''); // clear
-
- if ($th.data('pwd')) {
- // this AP needs a password
- Modal.show('#psk-modal');
- } else {
- Modal.show('#reset-modal');
- $('#conn-form').submit();
- }
- });
+ item.appendChild(inner);
+ $list[0].appendChild(item);
+ });
+ }
- item.appendChild(inner);
- $list[0].appendChild(item);
- });
+ function startScanning() {
+ $('#ap-loader').removeClass('hidden');
+ $('#ap-scan').addClass('hidden');
+ $('#ap-loader .anim-dots').html('.');
+ scanAPs();
}
/** Ask the CGI what APs are visible (async) */
function scanAPs() {
- $.get('http://'+_root+'/wifi/scan', onScan);
+ $.get('http://'+_root+w.scan_url, onScan);
}
function rescan(time) {
@@ -83,96 +145,38 @@
}
/** Set up the WiFi page */
- window.wifiInit = function (obj) {
- //var ap_json = {
- // "result": {
- // "inProgress": "0",
- // "APs": [
- // {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
- // {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
- // ]
- // }
- //};
-
+ function wifiInit(cfg) {
// Hide what should be hidden in this mode
- $('.x-hide-'+obj.mode).addClass('hidden');
- obj.mode = +obj.mode;
-
- // Channel writable only in AP mode
- if (obj.mode != 2) $('#channel').attr('readonly', 1);
-
- curSSID = obj.staSSID;
-
- // add SSID to the opmode field
- if (curSSID) {
- var box = $('#opmodebox');
- box.html(box.html() + ' (' + curSSID + ')');
- }
-
- // hide IP if IP not received
- if (!obj.staIP) $('.x-hide-noip').addClass('hidden');
-
- // scan if not AP
- if (obj.mode != 2) {
- scanAPs();
- }
-
- $('#modeswitch').html([
- 'Client+AP AP only ',
- 'Client+AP ',
- 'Client only AP only '
- ][obj.mode-1]);
- };
-
- window.wifiConn = function () {
- var xhr = new XMLHttpRequest();
- var abortTmeo;
-
- function getStatus() {
- xhr.open("GET", 'http://'+_root+"/wifi/connstatus");
- xhr.onreadystatechange = function () {
- if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300) {
- clearTimeout(abortTmeo);
- var data = JSON.parse(xhr.responseText);
- var done = false;
- var msg = '...';
-
- if (data.status == "idle") {
- msg = "Preparing to connect";
- }
- else if (data.status == "success") {
- msg = "Connected! Received IP " + data.ip + ".";
- done = true;
- }
- else if (data.status == "working") {
- msg = "Connecting to selected AP";
- }
- else if (data.status == "fail") {
- msg = "Connection failed, check your password and try again.";
- done = true;
- }
-
- $("#status").html(msg);
-
- if (done) {
- $('#backbtn').removeClass('hidden');
- $('.anim-dots').addClass('hidden');
- } else {
- window.setTimeout(getStatus, 1000);
- }
- }
- };
+ cfg.mode = +cfg.mode;
+
+ $('#ap-noscan').toggleClass('hidden', cfg.mode != 2);
+ $('#ap-scan').toggleClass('hidden', cfg.mode == 2);
+
+ // Update slider value displays
+ $('.Row.range').forEach(function(x) {
+ var inp = x.querySelector('input');
+ var disp1 = x.querySelector('.x-disp1');
+ var disp2 = x.querySelector('.x-disp2');
+ var t = rangePt(inp);
+ $(disp1).html(t);
+ $(disp2).html(t);
+ $(inp).on('input', function() {
+ t = rangePt(inp);
+ $(disp1).html(t);
+ $(disp2).html(t);
+ });
+ });
- abortTmeo = setTimeout(function () {
- xhr.abort();
- $("#status").html("Telemetry lost, try reconnecting to the AP.");
- $('#backbtn').removeClass('hidden');
- $('.anim-dots').addClass('hidden');
- }, 4000);
+ // Forget STA credentials
+ $('#forget-sta').on('click', function() {
+ selectSta('', '', '');
+ return false;
+ });
- xhr.send();
- }
+ selectSta(cfg.sta_ssid, cfg.sta_password, cfg.sta_active_ip);
+ curSSID = cfg.sta_active_ssid;
+ }
- getStatus();
- };
-})();
+ w.init = wifiInit;
+ w.startScanning = startScanning;
+})(window.WiFi = {});
diff --git a/html_orig/lang/en.php b/html_orig/lang/en.php
new file mode 100644
index 0000000..c3d6b69
--- /dev/null
+++ b/html_orig/lang/en.php
@@ -0,0 +1,140 @@
+ 'ESPTerm',
+
+ 'menu.cfg_wifi' => 'WiFi Settings',
+ 'menu.cfg_network' => 'Network Configuration',
+ 'menu.cfg_app' => 'Terminal Settings',
+ 'menu.about' => 'About ESPTerm',
+ 'menu.help' => 'Quick Reference',
+ 'menu.term' => 'Back to Terminal',
+ 'menu.cfg_admin' => 'Reset & Restore',
+ 'menu.cfg_wifi_conn' => 'Connecting to External Network',
+
+ 'menu.settings' => 'Settings',
+
+ 'title.term' => 'Terminal',
+
+ 'net.ap' => 'DHCP Server (AP)',
+ 'net.sta' => 'DHCP Client (Station)',
+ 'net.sta_mac' => 'Station MAC',
+ 'net.ap_mac' => 'AP MAC',
+ 'net.details' => 'MAC addresses',
+
+ 'app.defaults' => 'Initial settings',
+ 'app.explain_initials' => '
+ Those are the initial settings used after ESPTerm restarts, and they
+ will also be applied immediately after you submit this form.
+ They can be subsequently changed by ESC commands, but those changes
+ aren\'t persistent and will be lost when the device powers off.',
+
+ 'app.term_title' => 'Header text',
+ 'app.term_width' => 'Screen width',
+ 'app.term_height' => 'Screen height',
+ 'app.default_fg' => 'Base text color',
+ 'app.default_bg' => 'Base background',
+ 'app.btn1' => 'Button 1 text',
+ 'app.btn2' => 'Button 2 text',
+ 'app.btn3' => 'Button 3 text',
+ 'app.btn4' => 'Button 4 text',
+ 'app.btn5' => 'Button 5 text',
+
+ 'color.0' => 'Black',
+ 'color.1' => 'Dark Red',
+ 'color.2' => 'Dark Green',
+ 'color.3' => 'Dim Yellow',
+ 'color.4' => 'Deep Blue',
+ 'color.5' => 'Dark Violet',
+ 'color.6' => 'Dark Cyan',
+ 'color.7' => 'Silver',
+ 'color.8' => 'Gray',
+ 'color.9' => 'Light Red',
+ 'color.10' => 'Light Green',
+ 'color.11' => 'Light Yellow',
+ 'color.12' => 'Light Blue',
+ 'color.13' => 'Light Violet',
+ 'color.14' => 'Light Cyan',
+ 'color.15' => 'White',
+
+ 'net.explain_sta' => '
+ Those settings affect the built-in DHCP client used for
+ connecting to an external network. Switching DHCP (dynamic IP) off
+ makes ESPTerm use the configured static IP. Please double-check
+ those settings before submitting, setting them incorrectly may
+ make it hard to access ESPTerm via the external network.',
+
+ 'net.explain_ap' => '
+ Those settings affect the built-in DHCP server in AP mode.
+ Please double-check those settings before submitting, setting them
+ incorrectly may render ESPTerm inaccessible via the AP.',
+
+ 'net.ap_dhcp_time' => 'Lease time',
+ 'net.ap_dhcp_start' => 'Pool start IP',
+ 'net.ap_dhcp_end' => 'Pool end IP',
+ 'net.ap_addr_ip' => 'Own IP address',
+ 'net.ap_addr_mask' => 'Subnet mask',
+
+ 'net.sta_dhcp_enable' => 'Use dynamic IP',
+ 'net.sta_addr_ip' => 'ESPTerm static IP',
+ 'net.sta_addr_mask' => 'Subnet mask',
+ 'net.sta_addr_gw' => 'Gateway IP',
+
+ 'wifi.ap' => 'Built-in Access Point',
+ 'wifi.sta' => 'Connect to External Network',
+
+ 'wifi.enable' => 'Enabled',
+ 'wifi.tpw' => 'Transmit power',
+ 'wifi.ap_channel' => 'Channel',
+ 'wifi.ap_ssid' => 'AP SSID',
+ 'wifi.ap_password' => 'Password',
+ 'wifi.ap_hidden' => 'Hide SSID',
+ 'wifi.sta_info' => 'Selected',
+
+ 'wifi.not_conn' => 'Not connected.',
+ 'wifi.sta_none' => 'None',
+ 'wifi.sta_active_pw' => '🔒',
+ 'wifi.sta_active_nopw' => '🔓 Open access',
+ 'wifi.connected_ip_is' => 'Connected, IP is ',
+ 'wifi.sta_password' => 'Password:',
+
+ 'wifi.scanning' => 'Scanning',
+ 'wifi.scan_now' => 'Start scanning!',
+ 'wifi.cant_scan_no_sta' => 'Can\'t scan with Client mode disabled.',
+ 'wifi.select_ssid' => 'Available networks:',
+
+ 'wifi.conn.status' => 'Status:',
+ 'wifi.conn.back_to_config' => 'Back to WiFi config',
+ 'wifi.conn.telemetry_lost' => 'Telemetry lost, something went wrong. Try again...',
+
+ 'wifi.conn.disabled' =>"Station mode is disabled.",
+ 'wifi.conn.idle' =>"Idle, not connected and with no IP.",
+ 'wifi.conn.success' => "Connected! Received IP ",
+ 'wifi.conn.working' => "Connecting to selected AP",
+ 'wifi.conn.fail' => "Connection failed, check settings & try again. Cause: ",
+
+ 'admin.confirm_restore' => 'Restore all settings to their default values?',
+ 'admin.confirm_restore_hard' =>
+ 'Restore to firmware default settings? This will reset ' .
+ 'all active settings and switch to AP mode with the default SSID.',
+ 'admin.confirm_store_defaults' =>
+ 'Enter admin password to confirm you want to store the current settings as defaults.',
+ 'admin.password' => 'Admin password:',
+ 'admin.restore_defaults' => 'Reset to default settings',
+ 'admin.write_defaults' => 'Save current settings as default',
+ 'admin.restore_hard' => 'Reset to firmware default settings',
+ 'admin.explain' => '
+ ESPTerm contains two persistent memory banks, one for default and
+ one for active settings. Active settings can be stored as defaults
+ by the administrator. Use the following button to revert all
+ active settings to their stored default values.
+ ',
+
+ 'apply' => 'Apply!',
+ 'enabled' => 'Enabled',
+ 'disabled' => 'Disabled',
+ 'yes' => 'Yes',
+ 'no' => 'No',
+ 'confirm' => 'OK',
+ 'form_errors' => 'Validation errors for:',
+];
diff --git a/html_orig/packjs.sh b/html_orig/packjs.sh
index 37c976c..6df5917 100755
--- a/html_orig/packjs.sh
+++ b/html_orig/packjs.sh
@@ -5,6 +5,8 @@ echo "Packing js..."
cat jssrc/chibi.js \
jssrc/utils.js \
jssrc/modal.js \
+ jssrc/notif.js \
jssrc/appcommon.js \
- jssrc/term.js \
- jssrc/wifi.js > js/app.js
+ jssrc/lang.js \
+ jssrc/wifi.js \
+ jssrc/term.js > js/app.js
diff --git a/html_orig/pages/_cfg_menu.php b/html_orig/pages/_cfg_menu.php
new file mode 100644
index 0000000..570c623
--- /dev/null
+++ b/html_orig/pages/_cfg_menu.php
@@ -0,0 +1,20 @@
+
+
+
diff --git a/html_orig/pages/_head.php b/html_orig/pages/_head.php
new file mode 100644
index 0000000..a6d428d
--- /dev/null
+++ b/html_orig/pages/_head.php
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ = $_GET['PAGE_TITLE'] ?>
+
+
+
+
+
+
+
+
+
+
+
+
= tr('menu.' . $_GET['page']) ?>
+
+
+ = tr('form_errors') ?>
+
+
+
diff --git a/html_orig/pages/_tail.php b/html_orig/pages/_tail.php
new file mode 100644
index 0000000..dfdca17
--- /dev/null
+++ b/html_orig/pages/_tail.php
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/html_orig/pages/about.php b/html_orig/pages/about.php
new file mode 100644
index 0000000..2708855
--- /dev/null
+++ b/html_orig/pages/about.php
@@ -0,0 +1,69 @@
+
+
+
+
Version
+
+
+ ESPTerm
+ v%vers_fw%, build %date% at %time%
+
+
+ libesphttpd
+ v%vers_httpd%
+
+
+ ESP IoT SDK
+ v%vers_sdk%
+
+
+
+
+
+
Issues
+
+ Please report any issues to the bugtracker or send them by e-mail (see above).
+
+
+ Firmware updates can be downloaded from the releases page and flashed
+ with esptool.py .
+
+
+
+
+
Contributing
+
+ You're welcome to submit your improvements and ideas to our GitHub repository !
+
+
+
+ If you'd like to donate, please try PayPal or
+ LiberaPay .
+
+
+
+
+
Thanks
+
+ The webserver is based on a fork of the
+ esphttpd library by Jeroen Domburg (Sprite_tm).
+
+
+ Using (modified) JS library chibi.js by
+ Kyle Barrow as a lightweight jQuery alternative.
+
+
diff --git a/html_orig/pages/cfg_admin.php b/html_orig/pages/cfg_admin.php
new file mode 100644
index 0000000..986f83e
--- /dev/null
+++ b/html_orig/pages/cfg_admin.php
@@ -0,0 +1,34 @@
+
+
+ = tr('admin.explain') ?>
+
+
+
+
+
+
+
+
+
+
diff --git a/html_orig/pages/cfg_app.php b/html_orig/pages/cfg_app.php
new file mode 100644
index 0000000..d967641
--- /dev/null
+++ b/html_orig/pages/cfg_app.php
@@ -0,0 +1,71 @@
+
diff --git a/html_orig/pages/cfg_network.php b/html_orig/pages/cfg_network.php
new file mode 100644
index 0000000..00cf133
--- /dev/null
+++ b/html_orig/pages/cfg_network.php
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
= tr('net.details') ?>
+
+
+ = tr('net.sta_mac') ?>
+
+
+ = tr('net.ap_mac') ?>
+
+
+
+
diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php
new file mode 100644
index 0000000..cd8fe1c
--- /dev/null
+++ b/html_orig/pages/cfg_wifi.php
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
diff --git a/html_orig/pages/cfg_wifi_conn.php b/html_orig/pages/cfg_wifi_conn.php
new file mode 100755
index 0000000..33c8b64
--- /dev/null
+++ b/html_orig/pages/cfg_wifi_conn.php
@@ -0,0 +1,53 @@
+= tr('menu.cfg_wifi_conn') ?>
+
+
+
+
diff --git a/html_orig/help.html b/html_orig/pages/help.php
similarity index 86%
rename from html_orig/help.html
rename to html_orig/pages/help.php
index 9cece09..6f6314b 100644
--- a/html_orig/help.html
+++ b/html_orig/pages/help.php
@@ -1,29 +1,15 @@
-
-
-
-
- Help - ESP8266 Remote Terminal
-
-
-
-
-
-
-
-Quick Reference
-
Wiring
- Communication UART on pins Rx, Tx at 115200-8-1-N
- Debug log on pin GPIO2 at 115200-8-1-N
- Use 3.3V logic, or 5V with protection resistors (470R or more)
- If the "LVD" LED on the ESP Term board lights up, the module doesn't get enough power. Check your connections.
+ Communication UART is on pins Rx, Tx at 115200-8-1-N. The baud rate can be changed in Terminal Settings.
+ Debug log is on pin GPIO2 (P2) at 115200-8-1-N. This baud rate is fixed.
+ Compatible with 3.3 V and 5 V logic. For 5 V, 470 R protection resistors are recommended.
+ If the "LVD" LED on the ESPTerm module lights up, it doesn't get enough power to run correctly. Check your connections.
Connect Rx and Tx with a piece of wire to test the terminal alone, you should see what you type in the browser.
- NOTE: This won't work if your ESP8266 board has a built-in USB-serial (like NodeMCU).
- For best performance, use the module in the Client mode. In AP mode, check that the channel used is clear;
- interference may cause lag in the terminal.
+ NOTE: This won't work if your ESP8266 board has a built-in USB-serial converter (like NodeMCU).
+ For best performance, use the module in Client mode (connected to external network).
+ In AP mode, check that the channel used is clear; interference may cause a flaky connection.
@@ -328,12 +314,3 @@
-
-
- Terminal WiFi config About
-
-
-
-
diff --git a/html_orig/pages/term.php b/html_orig/pages/term.php
new file mode 100644
index 0000000..5f96a80
--- /dev/null
+++ b/html_orig/pages/term.php
@@ -0,0 +1,37 @@
+
+
+%term_title%
+
+
+
+
+
+ %btn1% %btn2% %btn3% %btn4% %btn5%
+
+
+
+
+ = tr('menu.settings') ?> Help About
+
+
+
diff --git a/html_orig/sass/_fontello-embedded.scss b/html_orig/sass/_fontello-embedded.scss
new file mode 100644
index 0000000..506e911
--- /dev/null
+++ b/html_orig/sass/_fontello-embedded.scss
@@ -0,0 +1,57 @@
+@font-face {
+ font-family: 'fontello';
+ src: url('data:application/octet-stream;base64,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') format('woff'),
+ url('data:application/octet-stream;base64,AAEAAAAPAIAAAwBwR1NVQiCLJXoAAAD8AAAAVE9TLzI+IFOEAAABUAAAAFZjbWFwRp90gwAAAagAAAI8Y3Z0IAc5/2gAABiwAAAAIGZwZ22KkZBZAAAY0AAAC3BnYXNwAAAAEAAAGKgAAAAIZ2x5ZhgKlUQAAAPkAAAQqGhlYWQOsBL8AAAUjAAAADZoaGVhB+MEGQAAFMQAAAAkaG10eCvN//kAABToAAAAMGxvY2EYlBJOAAAVGAAAABptYXhwAVINngAAFTQAAAAgbmFtZcydHR8AABVUAAACzXBvc3QcABrmAAAYJAAAAIRwcmVw5UErvAAAJEAAAACGAAEAAAAKADAAPgACREZMVAAObGF0bgAaAAQAAAAAAAAAAQAAAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAgAAQAGAAAAAQAAAAEDpgGQAAUAAAJ6ArwAAACMAnoCvAAAAeAAMQECAAACAAUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBmRWQAQOgA8esDhP+cAFoDhABkAAAAAQAAAAAAAAAAAAUAAAADAAAALAAAAAQAAAGoAAEAAAAAAKIAAwABAAAALAADAAoAAAGoAAQAdgAAABQAEAADAAToAugL6B/oJugu6DbxCPET8ev//wAA6ADoC+gf6CboLug28QjxE/Hr//8AAAAAAAAAAAAAAAAAAAAAAAAAAQAUABgAGAAYABgAGAAYABgAGAAAAAEAAgADAAQABQAGAAcACAAJAAoACwAAAQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAlAAAAAAAAAALAADoAAAA6AAAAAABAADoAQAA6AEAAAACAADoAgAA6AIAAAADAADoCwAA6AsAAAAEAADoHwAA6B8AAAAFAADoJgAA6CYAAAAGAADoLgAA6C4AAAAHAADoNgAA6DYAAAAIAADxCAAA8QgAAAAJAADxEwAA8RMAAAAKAADx6wAA8esAAAALAAQAAP/jA1kDPQADACEAMQBFAFFATisqIyIECAQBRw0BBAYBCAJGAAoHAQQICgRgAAgAAwYIA2AABgABAAYBXgUCAgAJCQBSBQICAAAJWAAJAAlMQD04NRcmMxETOxEREAsFHSs3ITUhBTMRNCYvAS4BBxUUBiMhIiYnNSMRMzU0NjMhMhYHAzU0JisBIgYXFRQWNzMyNgURFAYjISImJxE0NjMhMhYfAR4B1gGt/lMB9EgMBZ0FHAgeF/6+Fh4BSEggFQHRFiAB1goIawcMAQoIawcMAWQeF/0SFx4BIBYCBRc2D5wQFivW1gH0CBoHnAYMAegWICAW6P026BYgIBYBHrIICgoIsgcMAQoK/foWICAWAu4WIBgOnQ82AAAAAAEAAAAAA6UCygAVAB1AGg8BAAEBRwACAQJvAAEAAW8AAABmFBcUAwUXKwEUBwEGIicBJjQ/ATYyHwEBNjIfARYDpRD+IBAsEP7qDw9MECwQpAFuECwQTBACSBYQ/iAPDwEWECwQTBAQpQFvEBBMDwABAAD/4wPoAz4AHAAhQB4RAQABAUcCAQEAAW8DAQAAZgEAFxUNCwAcARwEBRQrBSInAScuAzU0NjcyHgIXPgMXMhYUBwEGAfQOC/6kDwoqIhqOfSJIPi4TFCxARiN9joD+pQodCgFQDwo2NlAle4oBGCoiFRQkKBoBjPWA/rEKAAEAAP/yApgDdgAUAC21AQEAAQFHS7AkUFhACwAAAQBwAAEBDAFJG0AJAAEAAW8AAABmWbQXFwIFFisJAhYUDwEGIicBJjQ3ATYyHwEWFAKO/tcBKQoKXQscC/5iCwsBngoeCl0KAtz+2P7XCh4KXQoKAZ8KHgoBngsLXQoeAAAAAQAA//wDoQNyAB8ANUAKEg8KBAMFAAIBR0uwHFBYQAwBAQACAHAAAgIMAkkbQAoAAgACbwEBAABmWbUdFBcDBRcrARQPARMVFA4BLwEHBiImNTQ3EycmNTQ3JTc2Mh8BBRYDoQ/KMAwVDPv6DBYMATDLDh8BGH4LIAx9ARggAhsMD8X+6QwLEAEHhIQHEgoECAEXxQ8MFQUo/hcX/igFAAP//f/jA18DPQAPADcARABIQEUpAQUDCQECAQACRwAEAgMCBANtAAMFAgMFawAHAAIEBwJgAAUAAAEFAGAAAQYGAVQAAQEGWAAGAQZMFR4rExYmJiMIBRwrJTU0JisBIgYdARQWOwEyNhM0LgEjIgcGHwEWMzI3PgEyFhUUBgcOARcVFBY7ATI2NDY/AT4DFxQOASIuAj4BMh4BAfQKCGsICgoIawgKjz5cMYhHCQ1KBAYJBR4lOCoWGyM8AQoIawgKGBIcCh4UDNdyxujIbgZ6vPS6foRrCAoKCGsICgoBfzFULncNCzcEByYbHhIVGgwPQiUUCAoKEiILEAYaHChSdcR0dMTqxHR0xAAD//3/4wNZAz0ADAG9AfcCd0uwCVBYQTwAvQC7ALgAnwCWAIgABgADAAAAjwABAAIAAwDaANMAbQBZAFEAQgA+ADMAIAAZAAoABwACAZ4BmAGWAYwBiwF6AXUBZQFjAQMA4QDgAAwABgAHAVMBTQEoAAMACAAGAfQB2wHRAcsBwAG+ATgBMwAIAAEACAAGAEcbS7AKUFhBQwC7ALgAnwCIAAQABQAAAL0AAQADAAUAjwABAAIAAwDaANMAbQBZAFEAQgA+ADMAIAAZAAoABwACAZ4BmAGWAYwBiwF6AXUBZQFjAQMA4QDgAAwABgAHAVMBTQEoAAMACAAGAfQB2wHRAcsBwAG+ATgBMwAIAAEACAAHAEcAlgABAAUAAQBGG0E8AL0AuwC4AJ8AlgCIAAYAAwAAAI8AAQACAAMA2gDTAG0AWQBRAEIAPgAzACAAGQAKAAcAAgGeAZgBlgGMAYsBegF1AWUBYwEDAOEA4AAMAAYABwFTAU0BKAADAAgABgH0AdsB0QHLAcABvgE4ATMACAABAAgABgBHWVlLsAlQWEA1AAIDBwMCB20ABwYDBwZrAAYIAwYIawAIAQMIAWsAAQFuCQEAAwMAVAkBAAADWAUEAgMAA0wbS7AKUFhAOgQBAwUCBQNlAAIHBQIHawAHBgUHBmsABggFBghrAAgBBQgBawABAW4JAQAFBQBUCQEAAAVWAAUABUobQDUAAgMHAwIHbQAHBgMHBmsABggDBghrAAgBAwgBawABAW4JAQADAwBUCQEAAANYBQQCAwADTFlZQRkAAQAAAdgB1gG5AbcBVwFWAMcAxQC1ALQAsQCuAHkAdgAHAAYAAAAMAAEADAAKAAUAFCsBMh4BFA4BIi4CPgEBDgEHMj4BNT4BNzYXJjY/ATY/AQYmNRQHNCYGNS4ELwEmNC8BBwYUKgEUIgYiBzYnJiM2JiczLgInLgEHBhQfARYGHgEHBg8BBhYXFhQGIg8BBiYnJicmByYnJgcyJgc+ASM2PwE2JxY/ATY3NjIWMxY0JzInJicmBwYXIg8BBi8BJiciBzYmIzYnJiIPAQYeATIXFgciBiIGFgcuAScWJyMiBiInJjc0FycGBzI2PwE2FzcXJgcGBxYHJy4BJyIHBgceAhQ3FgcyFxYXFgcnJgYWMyIPAQYfAQYWNwYfAx4CFwYWByIGNR4CFBY3NicuAjUzMh8BBh4CMx4BBzIeBB8DFjI/ATYWFxY3Ih8BHgEVHgEXNjUGFjM2NQYvASY0JjYXMjYuAicGJicUBhUjNjQ/ATYvASYHIgcOAyYnLgE0PwE2JzY/ATY7ATI0NiYjFjYXFjcnJjcWNx4CHwEWNjcWFx4BPgEmNSc1LgE2NzQ2PwE2JzI3JyYiNzYnPgEzFjYnPgE3FjYmPgEVNzYjFjc2JzYmJzMyNTYnJgM2NyYiLwE2Ji8BJi8BJg8BIg8BFSYnIi4BDgEPASY2JgYPAQY2BhUOARUuATceARcWBwYHBhcUBhYBrXTGcnLG6MhuBnq8ARMCCAMBAgQDERUTCgEMAggGAwEHBgQECgUGBAEIAQIBAwMEBAQEBgEGAggJBQQGAgQDAQgMAQUcBAMCAgEIAQ4BAgcJAwQEAQQCAwEHCgIEBQ0DAxQOEwQIBgECAQIFCQIBEwkGBAIFBgoDCAQHBQIDBgkEBgEFCQQFAwMCBQQBDgcLDwQQAwMBCAQIAQgDAQgEAwICAwQCBBIFAwwMAQMDAgwZGwMGBQUTBQMLBA0LAQQCBgQIBAkEUTIEBQIGBQMBGAoBAgcFBAMEBAQBAgEBAQIKBwcSBAcJBAMIBAIOAQECAg4CBAICDwgDBAMCAwUBBAoKAQQIBAUMBwIDCAMJBxYGBgUICBAEFAoBAgQCBgMOAwQBCgUIEQoCAgICAQUCBAEKAgMMAwIIAQIIAwEDAgcLBAECAggUAwgKAQIBBAIDBQIBAwIBAwEEGAMJAwEBAQMNAg4EAgMBBAMFAgYIBAICAQgEBAcIBQcMBAQCAgIGAQUEAwIDBQwEAhIBBAICBQ4JAgIKCAUJAgYGBwUJDAppc1ABDAENAQQDFQEDBQIDAgIBBQwIAwYGBgYBAQQIBAoBBwYCCgIEAQwBAQICBAsPAQIJCgEDPXTE6sR0dMTqxHT+3QEIAgYGAQQIAwULAQwBAwICDAEKBwIDBAIEAQIGDAUGAwMCBAEBAwMEAgQBAwMCAggEAgYEAQMEAQQEBgcDCAcKBwQFBgUMAwECBAIBAwwJDgMEBQcIBQMRAgMOCAUMAwEDCQkGBAMGAQ4ECgQBAgUCAgYKBAcHBwEJBQgHCAMCBwMCBAIGAgQFCgMDDgIFAgIFBAcCAQoIDwIDAwcDAg4DAgMEBgQGBAQBAS1PBAEIBAMEBg8KAgYEBQQFDgkUCwIBBhoCARcFBAYDBRQDAxAFAgEECAUIBAELGA0FDAICBAQMCA4EDgEKCxQHCAEFAw0CAQIBEgMKBAQJBQYCAwoDAgMFDAIQCBIDAwQEBgIECgcOAQUCBAEEAgIQBQ8FAgUDAgsCCAQEAgIEGA4JDgUJAQQGAQIDAgEEAwYHBgUCDwoBBAECAwECAwgFFwQCCAgDBQ4CCgoFAQIDBAsJBQICAgIGAgoGCgQEBAMBBAoEBgEHAgEHBgUEAgMBBQQC/g0VVQICBQQGAg8BAQIBAgEBAwIKAwYCAgUGBwMOBgIBBQQCCAECCAICAgIFHAgRCQ4JDAIEEAcAAQAA/+MDWQM9ADEAPkA7KgEDBSUdAgQDAkcABAMBAwQBbQABAgMBAmsABQADBAUDYAACAAACVAACAgBYAAACAEwpNRcjFyQGBRorARQOAgciJicmND8BNhYXHgEzMj4DLgIiBgcXFgYrASImJzU0Nh8BPgEzMh4CA1lEcqBWYK48BAVMBhEEKXZDOmhQKgIuTGxvZChNERMX+g8UASwRSDyaUleedEIBkFeedEICUkkGDgRNBQEGNTouTGp0akwuKCVNEC0WDvoYExJIOT5EdJ4AAAAC////4wQvA4QADwAvADBALQkBAgEAIAEDAgJHAAMCA3AAAQQBAgMBAmAAAAAFWAAFBQwASTUmNiYmFAYFGisBETQmJyEiBgcRFBYzITI2ExEUBgchFB4BFxQGIyEiJic0PgE1ISImNxE0NjMhMhYD6AoI/IMHCgEMBgN9BwxGNCX+0RIQARQP/uIPFAESEv7QJDYBNCUDfSU0AVoB0QcKAQwG/i8HCgoB2P2hJTQBFC4iBw4WFg4IIiwVNiQCXyU0NAAABAAA/+MDoQL1AAwAGQAzAFoAS0BIWVJORwQCCA0AAgADAkcJAQcIB28ACAIIbwQBAgMCbwADAANvAQEABQBvAAUGBgVUAAUFBlgABgUGTFVUIx1LNyISKxwTCgUdKyUUDgEuAz4CHgEFFA4BLgM+Ah4BFzQmIyIHBiInJiMiBgcUHgM3MzI+AzcUBw4EByIuBCcmNTQ3JjU0NzIWFzYzMhc+ATcWFRQHFgFlDiIuJAwCECAyHhIBYw4iLiQMAhAgMh4SWE5BF1YoYCdVGEJMASQ2UkouXi5KUjgifiIWSlRqVjIrSFxOTDoTI0wPHD1aPVJaU0o6XDsdD0zdFi4oAiQyKDQiBCosGBYuKAIkMig0IgQqLBhDXgwGBgxeQzFILBYMAggaKEySdEUrPiIUBAEEChgiOCRFdIRZLTJAOSwvFBIuKgE5QDEtWQAEAAAAAARfAz0ACgAgADoAUgCLQIhHAQsILwEEBhUBAgcDAQABBEcRDQILCAYICwZtEAkCBwQCBAcCbQ8FAgMCAQIDAW0ADAAKCAwKYAAIAAYECAZgAAQAAgMEAmAAAQAAAVQAAQEAWA4BAAEATDs7ISELCwEAO1I7UkxLRUNAPyE6ITo0My0rJyULIAsgGhkTEg8OBgUACgEKEgUUKyUiJic0PgEWBxQGNyIuASIGDwEiJjU0Nz4CFhcWFRQGNyInLgEHIg4DIyImNTQ3PgEeARcWFRQGNyInLgIGBwYjIiYnNDc2JCAEFxYVFAYCOwtQAUYsSAFSjAEqSEhGFhYKVAUsgoKEKwVUjgYGTIJVL2BGOCACCVQGStDY0kkGVI4GB2PY/tZkBwYJVAEGaAEgASwBImcFVDJSCxIYAhwQC1KXHBwcDg5UCgcGKzACNCkGBwpUmAU6OAEYIiQYVAoHBUpSAk5MBQcKVJcFWFgCXFYFVAoHBmhycmgGBwpUAAABAAAAAQAAyfbumV8PPPUACwPoAAAAANWaZ1gAAAAA1ZpnWP/9/+MEXwOEAAAACAACAAAAAAAAAAEAAAOE/5wAAAR2//3/+gRfAAEAAAAAAAAAAAAAAAAAAAAMA+gAAANZAAAD6AAAA+gAAALKAAADoAAAA1n//QNZ//0DWQAABC///wOgAAAEdgAAAAAAAACQAMgBCAFIAZgCHgYiBowG7geUCFQAAAABAAAADAH4AAQAAAAAAAIAJAA0AHMAAACqC3AAAAAAAAAAEgDeAAEAAAAAAAAANQAAAAEAAAAAAAEACAA1AAEAAAAAAAIABwA9AAEAAAAAAAMACABEAAEAAAAAAAQACABMAAEAAAAAAAUACwBUAAEAAAAAAAYACABfAAEAAAAAAAoAKwBnAAEAAAAAAAsAEwCSAAMAAQQJAAAAagClAAMAAQQJAAEAEAEPAAMAAQQJAAIADgEfAAMAAQQJAAMAEAEtAAMAAQQJAAQAEAE9AAMAAQQJAAUAFgFNAAMAAQQJAAYAEAFjAAMAAQQJAAoAVgFzAAMAAQQJAAsAJgHJQ29weXJpZ2h0IChDKSAyMDE3IGJ5IG9yaWdpbmFsIGF1dGhvcnMgQCBmb250ZWxsby5jb21mb250ZWxsb1JlZ3VsYXJmb250ZWxsb2ZvbnRlbGxvVmVyc2lvbiAxLjBmb250ZWxsb0dlbmVyYXRlZCBieSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAEMAbwBwAHkAcgBpAGcAaAB0ACAAKABDACkAIAAyADAAMQA3ACAAYgB5ACAAbwByAGkAZwBpAG4AYQBsACAAYQB1AHQAaABvAHIAcwAgAEAAIABmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQBmAG8AbgB0AGUAbABsAG8AUgBlAGcAdQBsAGEAcgBmAG8AbgB0AGUAbABsAG8AZgBvAG4AdABlAGwAbABvAFYAZQByAHMAaQBvAG4AIAAxAC4AMABmAG8AbgB0AGUAbABsAG8ARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQAHcGVyc2lzdAJvawZkb25hdGUEYmFjawVhYm91dARoZWxwB25ldHdvcmsHcmVzdG9yZQh0ZXJtaW5hbAZnaXRodWIEd2lmaQAAAAEAAf//AA8AAAAAAAAAAAAAAAAAAAAAABgAGAAYABgDhP+cA4T/nLAALCCwAFVYRVkgIEu4AA5RS7AGU1pYsDQbsChZYGYgilVYsAIlYbkIAAgAY2MjYhshIbAAWbAAQyNEsgABAENgQi2wASywIGBmLbACLCBkILDAULAEJlqyKAEKQ0VjRVJbWCEjIRuKWCCwUFBYIbBAWRsgsDhQWCGwOFlZILEBCkNFY0VhZLAoUFghsQEKQ0VjRSCwMFBYIbAwWRsgsMBQWCBmIIqKYSCwClBYYBsgsCBQWCGwCmAbILA2UFghsDZgG2BZWVkbsAErWVkjsABQWGVZWS2wAywgRSCwBCVhZCCwBUNQWLAFI0KwBiNCGyEhWbABYC2wBCwjISMhIGSxBWJCILAGI0KxAQpDRWOxAQpDsAFgRWOwAyohILAGQyCKIIqwASuxMAUlsAQmUVhgUBthUllYI1khILBAU1iwASsbIbBAWSOwAFBYZVktsAUssAdDK7IAAgBDYEItsAYssAcjQiMgsAAjQmGwAmJmsAFjsAFgsAUqLbAHLCAgRSCwC0NjuAQAYiCwAFBYsEBgWWawAWNgRLABYC2wCCyyBwsAQ0VCKiGyAAEAQ2BCLbAJLLAAQyNEsgABAENgQi2wCiwgIEUgsAErI7AAQ7AEJWAgRYojYSBkILAgUFghsAAbsDBQWLAgG7BAWVkjsABQWGVZsAMlI2FERLABYC2wCywgIEUgsAErI7AAQ7AEJWAgRYojYSBksCRQWLAAG7BAWSOwAFBYZVmwAyUjYUREsAFgLbAMLCCwACNCsgsKA0VYIRsjIVkqIS2wDSyxAgJFsGRhRC2wDiywAWAgILAMQ0qwAFBYILAMI0JZsA1DSrAAUlggsA0jQlktsA8sILAQYmawAWMguAQAY4ojYbAOQ2AgimAgsA4jQiMtsBAsS1RYsQRkRFkksA1lI3gtsBEsS1FYS1NYsQRkRFkbIVkksBNlI3gtsBIssQAPQ1VYsQ8PQ7ABYUKwDytZsABDsAIlQrEMAiVCsQ0CJUKwARYjILADJVBYsQEAQ2CwBCVCioogiiNhsA4qISOwAWEgiiNhsA4qIRuxAQBDYLACJUKwAiVhsA4qIVmwDENHsA1DR2CwAmIgsABQWLBAYFlmsAFjILALQ2O4BABiILAAUFiwQGBZZrABY2CxAAATI0SwAUOwAD6yAQEBQ2BCLbATLACxAAJFVFiwDyNCIEWwCyNCsAojsAFgQiBgsAFhtRAQAQAOAEJCimCxEgYrsHIrGyJZLbAULLEAEystsBUssQETKy2wFiyxAhMrLbAXLLEDEystsBgssQQTKy2wGSyxBRMrLbAaLLEGEystsBsssQcTKy2wHCyxCBMrLbAdLLEJEystsB4sALANK7EAAkVUWLAPI0IgRbALI0KwCiOwAWBCIGCwAWG1EBABAA4AQkKKYLESBiuwcisbIlktsB8ssQAeKy2wICyxAR4rLbAhLLECHistsCIssQMeKy2wIyyxBB4rLbAkLLEFHistsCUssQYeKy2wJiyxBx4rLbAnLLEIHistsCgssQkeKy2wKSwgPLABYC2wKiwgYLAQYCBDI7ABYEOwAiVhsAFgsCkqIS2wKyywKiuwKiotsCwsICBHICCwC0NjuAQAYiCwAFBYsEBgWWawAWNgI2E4IyCKVVggRyAgsAtDY7gEAGIgsABQWLBAYFlmsAFjYCNhOBshWS2wLSwAsQACRVRYsAEWsCwqsAEVMBsiWS2wLiwAsA0rsQACRVRYsAEWsCwqsAEVMBsiWS2wLywgNbABYC2wMCwAsAFFY7gEAGIgsABQWLBAYFlmsAFjsAErsAtDY7gEAGIgsABQWLBAYFlmsAFjsAErsAAWtAAAAAAARD4jOLEvARUqLbAxLCA8IEcgsAtDY7gEAGIgsABQWLBAYFlmsAFjYLAAQ2E4LbAyLC4XPC2wMywgPCBHILALQ2O4BABiILAAUFiwQGBZZrABY2CwAENhsAFDYzgtsDQssQIAFiUgLiBHsAAjQrACJUmKikcjRyNhIFhiGyFZsAEjQrIzAQEVFCotsDUssAAWsAQlsAQlRyNHI2GwCUMrZYouIyAgPIo4LbA2LLAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjILAIQyCKI0cjRyNhI0ZgsARDsAJiILAAUFiwQGBZZrABY2AgsAErIIqKYSCwAkNgZCOwA0NhZFBYsAJDYRuwA0NgWbADJbACYiCwAFBYsEBgWWawAWNhIyAgsAQmI0ZhOBsjsAhDRrACJbAIQ0cjRyNhYCCwBEOwAmIgsABQWLBAYFlmsAFjYCMgsAErI7AEQ2CwASuwBSVhsAUlsAJiILAAUFiwQGBZZrABY7AEJmEgsAQlYGQjsAMlYGRQWCEbIyFZIyAgsAQmI0ZhOFktsDcssAAWICAgsAUmIC5HI0cjYSM8OC2wOCywABYgsAgjQiAgIEYjR7ABKyNhOC2wOSywABawAyWwAiVHI0cjYbAAVFguIDwjIRuwAiWwAiVHI0cjYSCwBSWwBCVHI0cjYbAGJbAFJUmwAiVhuQgACABjYyMgWGIbIVljuAQAYiCwAFBYsEBgWWawAWNgIy4jICA8ijgjIVktsDossAAWILAIQyAuRyNHI2EgYLAgYGawAmIgsABQWLBAYFlmsAFjIyAgPIo4LbA7LCMgLkawAiVGUlggPFkusSsBFCstsDwsIyAuRrACJUZQWCA8WS6xKwEUKy2wPSwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xKwEUKy2wPiywNSsjIC5GsAIlRlJYIDxZLrErARQrLbA/LLA2K4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrErARQrsARDLrArKy2wQCywABawBCWwBCYgLkcjRyNhsAlDKyMgPCAuIzixKwEUKy2wQSyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2GwAiVGYTgjIDwjOBshICBGI0ewASsjYTghWbErARQrLbBCLLA1Ky6xKwEUKy2wQyywNishIyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=') format('truetype');
+}
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/*
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'fontello';
+ src: url('../font/fontello.svg?60007293#fontello') format('svg');
+ }
+}
+*/
+
+ [class^="icn-"]:before, [class*=" icn-"]:before {
+ font-family: "fontello";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+}
+.icn-persist:before { content: '\e800'; } /* '' */
+.icn-ok:before { content: '\e801'; } /* '' */
+.icn-donate:before { content: '\e802'; } /* '' */
+.icn-back:before { content: '\e80b'; } /* '' */
+.icn-about:before { content: '\e81f'; } /* '' */
+.icn-help:before { content: '\e826'; } /* '' */
+.icn-network:before { content: '\e82e'; } /* '' */
+.icn-restore:before { content: '\e836'; } /* '' */
+.icn-terminal:before { content: '\f108'; } /* '' */
+.icn-github:before { content: '\f113'; } /* '' */
+.icn-wifi:before { content: '\f1eb'; } /* '' */
diff --git a/html_orig/sass/_layout.scss b/html_orig/sass/_layout.scss
deleted file mode 100644
index ffc2126..0000000
--- a/html_orig/sass/_layout.scss
+++ /dev/null
@@ -1,178 +0,0 @@
-html {
- font-family: Arial, sans-serif;
- color: #D0D0D0;
- background: #131315;
-}
-
-html, body {
- @include naked();
- width: 100%;
- height: 100%;
- overflow: hidden;
-}
-
-a, a:visited, a:link {
- cursor: pointer;
- color: #5abfff;
- text-decoration: none;
-}
-
-a:hover {
- color: #5abfff;
- text-decoration: underline;
-}
-
-.Box {
- display: block;
- max-width: 900px;
-
- margin-top: dist(0);
- padding: dist(-1) dist(0);
-
- @include media($phone) {
- margin-top: dist(-1);
- padding: dist(-3) dist(-2);
- }
- //
- //h1 + & {
- // margin-top: 0;
- //}
- //
- //h2 {
- // margin-top: 0;
- //}
-
- p:first-child {
- margin-top:0;
- }
-
- border-radius: 3px;
- background-color: rgba(white, .07);
-
- //&.wide {
- // width: initial;
- // max-width: initial;
- //}
- //
- //&.medium {
- // max-width: 1200px;
- //}
- //
- //.Valfield {
- // display: inline-block;
- // min-width: 10em;
- //}
-}
-
-body {
- position: relative;
-
- padding: dist(0);
- @include media($phone) {
- padding: dist(-1);
- }
-
- overflow-y: auto;
-
- & > * {
- margin-left: auto;
- margin-right: auto;
- }
-}
-
-h1,h2 {
- @include noselect();
-}
-
-h1 {
- text-align: center;
- font-size: fsize(6);
- margin-top: 0;
- margin-bottom: dist(0);
-
- @include media($phone) {
- font-size: fsize(3);
- margin-bottom: dist(-1);
- }
-
- @include media($tablet) {
- font-size: fsize(5);
- }
-}
-
-h2 {
- font-size: fsize(2);
- margin-bottom: dist(-1);
- //&:first-child{margin-top:0}
-}
-
-td, th {
- padding: dist(-2);
- white-space: nowrap;
-
- @include media($phone) {
- padding: dist(-3);
- }
-}
-
-tbody th {
- text-align: right;
- width: $form-label-w;
- color: $c-form-label-fg;
-
- @include media($phone) {
- width: auto;
- }
-}
-
-tbody td {
- input[type="text"], input[type="number"] {
- width: 10em;
-
- @include media($phone) {
- width: 8em;
- }
- }
-}
-
-// Loader wheel in top right corner
-#loader {
- position: absolute;
- right: dist(1);
- top: dist(1);
-
- transition: opacity .2s;
- opacity: 0;
-
- @include media($phone) {
- top: dist(0);
- right: dist(0);
- }
-
- &.show {
- opacity:1;
- }
-}
-
-ul > * {
- padding-top: .1em;
- padding-bottom: .1em;
-}
-
-#botnav {
- padding-top: 1.5em;
- text-align: center;
-
- a {
- padding: 0 dist(-2);
- text-decoration: underline;
-
- &, &:visited, &:link {
- color: #2e4d6e;
- }
-
- &:hover {
- color: #5abfff;
- }
- }
-}
diff --git a/html_orig/sass/app.scss b/html_orig/sass/app.scss
index c80c715..ecd627c 100755
--- a/html_orig/sass/app.scss
+++ b/html_orig/sass/app.scss
@@ -1,4 +1,5 @@
@import "normalize";
+@import "fontello-embedded";
@import "lib/bourbon/bourbon";
@import "grid-settings";
@@ -6,15 +7,16 @@
@import "utils";
-$form-label-w: 130px;
+$form-label-w: 160px;
$form-label-gap: 8px;
$form-field-w: 250px;
+$c-red-outline: #ff0099;
$c-form-label-fg: white;
-$c-form-field-bg: #303030;
+$c-form-field-bg: #3c3c3c;
$c-form-field-fg: white;
-$c-form-highlight: #214e7a;
-$c-form-highlight-a: #2972ba;
+$c-form-highlight: #2972ba;
+$c-form-highlight-a: #2ea1f9;
@function dist($x) {
@return modular-scale($x, 1rem, $golden);
@@ -24,16 +26,7 @@ $c-form-highlight-a: #2972ba;
@return modular-scale($x, 1em, $major-second);
}
-.hidden {
- display: none !important;
-}
-
-[onclick] {
- cursor: pointer;
-}
-
-@import "modal";
-@import "layout";
+@import "layout/index";
@import "form/index";
// import all our pages
@@ -45,17 +38,17 @@ $c-form-highlight-a: #2972ba;
// media queries
@include media($tablet-min) {
- .mq-phone { display: none; }
+ .mq-phone { display: none!important; }
}
@include media($phone) {
- .mq-tablet-min { display: none; }
+ .mq-tablet-min, .mq-no-phone { display: none !important; }
}
@include media($normal-min) {
- .mq-tablet-max { display: none; }
+ .mq-tablet-max { display: none !important; }
}
@include media($tablet-max) {
- .mq-normal-min { display: none; }
+ .mq-normal-min { display: none !important; }
}
diff --git a/html_orig/sass/form/_buttons.scss b/html_orig/sass/form/_buttons.scss
index 7c0b5b3..5f36222 100755
--- a/html_orig/sass/form/_buttons.scss
+++ b/html_orig/sass/form/_buttons.scss
@@ -31,9 +31,18 @@ button, input[type=submit], .button {
min-width: initial;
}
- text-shadow: 1.5px 1.5px 2px rgba(black, 0.6);
+ &::before {
+ vertical-align: -1px;
+ margin-left: 0;
+ }
+
+ text-shadow: 1.5px 1.5px 2px rgba(black, 0.4);
@include fancy-btn-colors($btn-blue-f, $btn-blue-b, $btn-blue-fa, $btn-blue-ba);
+
+ &:focus {
+ outline-color: $c-red-outline;
+ }
}
//input[type="submit"], .btn-green {
diff --git a/html_orig/sass/form/_fancy_button_mixins.scss b/html_orig/sass/form/_fancy_button_mixins.scss
index 0ddb476..c7d2a4f 100755
--- a/html_orig/sass/form/_fancy_button_mixins.scss
+++ b/html_orig/sass/form/_fancy_button_mixins.scss
@@ -7,7 +7,7 @@
border-radius: 2px;
padding: 0 0.6em;
border: 0 none;
- outline: 0 none !important;
+ //outline: 0 none !important;
line-height: 1.8em;
font-size: 1.1em;
margin-bottom: 3px;
diff --git a/html_orig/sass/form/_form_elements.scss b/html_orig/sass/form/_form_elements.scss
index cbc8ef2..83c2aea 100755
--- a/html_orig/sass/form/_form_elements.scss
+++ b/html_orig/sass/form/_form_elements.scss
@@ -7,8 +7,8 @@
color: $c-form-field-fg;
padding: 6px;
line-height: 1em;
- outline: 0 none !important;
- -moz-outline: 0 none !important;
+ //outline: 0 none !important;
+ //-moz-outline: 0 none !important;
font-weight: normal;
&:focus, &:hover {
@@ -16,6 +16,48 @@
}
}
+.Row.checkbox {
+ $h: 27px;
+ line-height: $h;
+
+ .box {
+ overflow: hidden;
+ width: $h;
+ height: $h;
+ border: 1px solid #808080;
+ border-radius: 3px;
+ background: $c-form-field-bg;
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ color: $c-form-highlight-a;
+
+ &::before {
+ font-weight: bold;
+ position: absolute;
+ content: '×';
+ left: 0; top: 0; right: 0; bottom: 0;
+ line-height: $h - 1px;
+ text-align: center;
+ font-size: $h;
+ vertical-align: middle;
+ display: none;
+ }
+
+ &.checked::before {
+ display: block;
+ }
+ }
+}
+
+.Row.range {
+ .display {
+ margin-left: 1ex;
+ }
+
+ label .display { font-weight: normal; }
+}
+
//#{$all-text-inputs} {
// @include can-select();
//}
diff --git a/html_orig/sass/form/_form_layout.scss b/html_orig/sass/form/_form_layout.scss
index 73320ab..ec373a7 100755
--- a/html_orig/sass/form/_form_layout.scss
+++ b/html_orig/sass/form/_form_layout.scss
@@ -5,13 +5,29 @@ form { @include naked(); }
width: $form-field-w;
}
-form .Row {
+input[type="number"], input.short {
+ width: $form-field-w/2;
+}
+
+.Box.errors {
+ .list {
+ color: crimson;
+ font-weight: bold;
+ }
+
+ .lead {
+ color: white;
+ }
+}
+
+.Row {
vertical-align: middle;
- margin: 14px auto;
+ margin: 12px auto;
text-align: left;
display: flex;
flex-direction: row;
+ align-items: center;
&:first-child {
margin-top: 0;
@@ -30,6 +46,7 @@ form .Row {
}
&.buttons {
+ margin: 16px auto;
input, .button {
margin-right: dist(-1);
}
@@ -80,34 +97,44 @@ form .Row {
align-self: flex-start;
@include noselect;
+ @include nowrap;
}
- .checkbox-wrap {
- display: inline-block;
- width: $form-label-w;
- padding: $form-label-gap;
- text-align: right;
- align-self: flex-start;
-
- input[type=checkbox] {
- margin: auto;
- width: auto;
- height: auto;
- }
+ label.error {
+ color: crimson;
+ }
- & + label {
- width: $form-field-w;
- padding-left: 0;
- text-align: left;
- cursor: pointer;
- }
+ //.checkbox-wrap {
+ // display: inline-block;
+ // width: $form-label-w;
+ // padding: $form-label-gap;
+ // text-align: right;
+ // align-self: flex-start;
+ //
+ // input[type=checkbox] {
+ // margin: auto;
+ // width: auto;
+ // height: auto;
+ // }
+ //
+ // & + label {
+ // width: $form-field-w;
+ // padding-left: 0;
+ // text-align: left;
+ // cursor: pointer;
+ // }
+ //}
+
+ input[type="range"] {
+ width: 200px;
}
// special phone style
@include media($phone) {
flex-direction: column;
+ margin: 6px auto;
- &.buttons, &.centered {
+ &.buttons, &.centered, &.checkbox {
flex-direction: row;
}
@@ -139,7 +166,7 @@ form .Row {
}
}
- #{$all-text-inputs}, textarea {
+ #{$all-text-inputs}, input[type="range"], textarea, select {
width: 100%;
}
}
diff --git a/html_orig/sass/form/_index.scss b/html_orig/sass/form/_index.scss
index 7736400..f2e9a7a 100755
--- a/html_orig/sass/form/_index.scss
+++ b/html_orig/sass/form/_index.scss
@@ -9,5 +9,5 @@
}
}
-//@import 'form_layout';
+@import 'form_layout';
//@import 'select';
diff --git a/html_orig/sass/layout/_base.scss b/html_orig/sass/layout/_base.scss
new file mode 100755
index 0000000..eda4c7e
--- /dev/null
+++ b/html_orig/sass/layout/_base.scss
@@ -0,0 +1,36 @@
+html {
+ font-family: Arial, sans-serif;
+ color: #D0D0D0;
+ background: #131315;
+}
+
+html, body {
+ @include naked();
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+a, a:visited, a:link {
+ cursor: pointer;
+ color: #5abfff;
+ text-decoration: none;
+}
+
+a:hover {
+ color: #5abfff;
+ text-decoration: underline;
+}
+
+.hidden {
+ display: none !important;
+}
+
+[onclick] {
+ cursor: pointer;
+}
+
+ul > * {
+ padding-top: .2em;
+ padding-bottom: .2em;
+}
diff --git a/html_orig/sass/layout/_box.scss b/html_orig/sass/layout/_box.scss
new file mode 100755
index 0000000..a61c850
--- /dev/null
+++ b/html_orig/sass/layout/_box.scss
@@ -0,0 +1,137 @@
+.Box {
+ display: block;
+ max-width: 900px;
+
+ margin-top: dist(0);
+ padding: dist(-1) dist(0);
+
+ // clear floats
+ &::after {
+ content: '';
+ display: block;
+ clear: both
+ }
+
+ @include media($phone) {
+ margin-top: dist(-1);
+ }
+
+ h1, h2 {
+ overflow: hidden;
+ }
+
+ h1 + & {
+ margin-top: 0;
+ }
+
+ h2 {
+ margin-top: 0;
+ margin-bottom: 0 !important;
+ }
+
+ p:last-child {
+ margin-bottom: 0.5em;
+ }
+
+ border-radius: 3px;
+ background-color: rgba(white, .07);
+ box-shadow: 0 0 4px black;
+ border: 1px solid #4f4f4f;
+
+ &.wide {
+ width: initial;
+ max-width: initial;
+ }
+
+ &.medium {
+ max-width: 1200px;
+ }
+
+ //.Valfield {
+ // display: inline-block;
+ // min-width: 10em;
+ //}
+
+ // Submit Top Right
+ &.str {
+ position: relative;
+ .Row.buttons {
+ position: absolute;
+
+ @include media($phone) {
+ right: dist(0);
+ top: 1.8em;
+ margin: 1rem auto;
+ }
+
+ @include media($tablet-min) {
+ right: 0;
+ top: 0;
+ margin-top: dist(-1);
+ }
+ }
+ }
+
+ &.str.mobopen .Row.buttons {
+ top: 0;
+ margin-top: dist(-1);
+ }
+
+ .Row.explain {
+ max-width: 600px; margin-left: 0;
+ @include media($phone) {
+ margin-top: 60px;
+ }
+ }
+ &.mobopen .Row.explain {
+ margin-top: 12px; // default from .Row
+
+ @include media($phone) {
+ margin-top: 18px;
+ }
+ }
+}
+
+@include media($phone) {
+ .Box.mobcol {
+ h2 {
+ position: relative;
+ cursor: pointer;
+ padding-right: 1.3rem;
+
+ &::after {
+ position: absolute;
+ right: 0;
+ content: '▸';
+
+ top:50%;
+ font-size: 120%;
+ font-weight: bold;
+ transform: translate(0,-50%) rotate(90deg);
+ }
+ }
+
+ &.expanded h2::after {
+ transform: translate(-25%,-50%) rotate(-90deg);
+ margin-bottom: dist(0);
+ }
+
+ .Row {
+ display: none;
+ }
+
+ #ap-box {
+ display: none;
+ }
+
+ &.expanded {
+ .Row {
+ display: flex;
+ }
+
+ #ap-box {
+ display: block;
+ }
+ }
+ }
+}
diff --git a/html_orig/sass/layout/_content.scss b/html_orig/sass/layout/_content.scss
new file mode 100755
index 0000000..b1d6b96
--- /dev/null
+++ b/html_orig/sass/layout/_content.scss
@@ -0,0 +1,50 @@
+#content {
+ flex-grow: 1;
+ position: relative;
+
+ padding: dist(0);
+ @include media($phone) {
+ padding: dist(-1);
+ }
+
+ overflow-y: auto;
+
+ & > * {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ h1 {
+ text-align: center;
+ font-size: fsize(7);
+ margin-top: 0;
+ margin-bottom: dist(0);
+ }
+
+ h2 {
+ font-size: fsize(3);
+ margin-bottom: dist(-1);
+ }
+
+ @include media($phone) {
+ h1 {
+ font-size: fsize(5);
+ margin-bottom: dist(-1);
+ }
+
+ h2 {
+ font-size: fsize(2);
+ margin-bottom: dist(-1);
+ }
+ }
+
+ td, th {
+ padding: dist(-2);
+ }
+
+ tbody th {
+ text-align: right;
+ width: $form-label-w;
+ color: $c-form-label-fg;
+ }
+}
diff --git a/html_orig/sass/layout/_index.scss b/html_orig/sass/layout/_index.scss
new file mode 100755
index 0000000..f37ef8d
--- /dev/null
+++ b/html_orig/sass/layout/_index.scss
@@ -0,0 +1,9 @@
+@import "base";
+
+@import "outer-wrap";
+@import "menu";
+@import "content";
+@import "loader";
+
+@import "box";
+@import "modal";
diff --git a/html_orig/sass/layout/_loader.scss b/html_orig/sass/layout/_loader.scss
new file mode 100644
index 0000000..62af4cc
--- /dev/null
+++ b/html_orig/sass/layout/_loader.scss
@@ -0,0 +1,18 @@
+// Loader wheel in top right corner
+#loader {
+ position: absolute;
+ right: dist(1);
+ top: dist(1);
+
+ transition: opacity .2s;
+ opacity: 0;
+
+ @include media($phone) {
+ top: dist(0);
+ right: dist(0);
+ }
+
+ &.show {
+ opacity:1;
+ }
+}
diff --git a/html_orig/sass/layout/_menu.scss b/html_orig/sass/layout/_menu.scss
new file mode 100755
index 0000000..e23fffe
--- /dev/null
+++ b/html_orig/sass/layout/_menu.scss
@@ -0,0 +1,110 @@
+#menu {
+ $menu-bg: #3983CD;
+ $menu-hl: #5badff; //#1bd886;
+ $menu-fg: white;
+
+ flex: 0 0 15rem;
+ background: $menu-bg;
+
+ & > * {
+ display: block;
+ text-decoration: none;
+ padding: dist(-1) dist(0);
+
+ @include nowrap;
+ @include noselect;
+ }
+
+ #brand {
+ color: $menu-fg;
+ background: darken($menu-bg, 10%);
+ font-size: 120%;
+ text-align: center;
+ position:relative;
+ transition: none;
+ font-weight: bold;
+
+ margin-bottom: dist(0);
+
+ @include media($phone) {
+ background: $menu-bg;
+ cursor: pointer;
+ margin-bottom: dist(-2);
+
+ &::after {
+ position: absolute;
+ color: rgba(black, .4);
+ right: dist(0);
+ content: '▸';
+
+ top:50%;
+ font-size: 120%;
+ font-weight: bold;
+ transform: translate(0,-50%) rotate(90deg);
+ }
+ }
+ }
+ &.expanded #brand {
+ background: darken($menu-bg, 10%);
+
+ @include media($phone) {
+ &:after { transform: translate(-25%,-50%) rotate(-90deg) }
+ }
+ }
+
+ a {
+ font-size: 130%;
+ color: $menu-fg;
+
+ transition: background-color 0.2s;
+ text-shadow: 0 0 5px rgba(black, .4);
+
+ &:hover, &.selected {
+ background: $menu-hl;
+ text-shadow: 0 0 5px rgba(black, .6);
+ }
+
+ &.selected {
+ position: relative;
+ box-shadow: 0 0 5px rgba(black, .5);
+ }
+
+ &:focus {
+ outline-color: $c-red-outline;
+ }
+
+ //&::before {
+ // content: "▸";
+ // padding-right: .5rem;
+ // position: relative;
+ // top: -0.1rem;
+ //}
+
+ // Fontello
+ &::before {
+ vertical-align: -2px;
+ margin-left: 0;
+ margin-right: 15px;
+ }
+
+ @include media($phone) {
+ display: none;
+ &::before {margin-left: 10px;}
+ }
+ }
+
+ &.expanded a { display:block }
+
+ @include media($tablet) {
+ #brand {
+ font-size: 95%;
+ margin-bottom: dist(-1);
+ }
+
+ a { font-size: 105%; }
+
+ flex-basis: 10rem;
+
+ & > * { padding: dist(-2) dist(-1); }
+ }
+}
diff --git a/html_orig/sass/_modal.scss b/html_orig/sass/layout/_modal.scss
similarity index 78%
rename from html_orig/sass/_modal.scss
rename to html_orig/sass/layout/_modal.scss
index c0c72fb..2b33c9a 100755
--- a/html_orig/sass/_modal.scss
+++ b/html_orig/sass/layout/_modal.scss
@@ -25,9 +25,11 @@
//min-height: 15rem;
background: #1c1c1e;
- border-left: 6px solid $c-form-highlight-a;
- border-right: 6px solid $c-form-highlight-a;
- box-shadow: 0 0 2px 0 #434349, 0 0 6px 0 black;
+ border-left: 6px solid $c-form-highlight;
+ border-right: 6px solid $c-form-highlight;
+ border-top: 1px solid $c-form-highlight;
+ border-bottom: 1px solid $c-form-highlight;
+ box-shadow: 0 0 6px 0 black;
border-radius: 6px;
@@ -40,21 +42,21 @@
}
}
-/*
// "toast"
.NotifyMsg {
position: fixed;
- bottom: dist(2);
+ top: dist(1);
+ right: dist(2);
padding: dist(-1) dist(0);
// center horizontally
- left: 50%;
- @include translate(-50%,0);
+ //left: 50%;
+ //@include translate(-50%,0);
// hack to remove blur in chrome
-webkit-font-smoothing: subpixel-antialiased;
-webkit-transform: translateZ(0) scale(1.0, 1.0);
- background: #37a349;
+ background: #3887d0;
&.error {
background: #d03e42;
}
@@ -75,4 +77,3 @@
&.visible { opacity: 1 }
&.hidden { display: none }
}
-*/
diff --git a/html_orig/sass/layout/_outer-wrap.scss b/html_orig/sass/layout/_outer-wrap.scss
new file mode 100755
index 0000000..c32d3a8
--- /dev/null
+++ b/html_orig/sass/layout/_outer-wrap.scss
@@ -0,0 +1,22 @@
+/* Main outer container */
+#outer {
+ display: flex;
+
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ overflow: hidden;
+
+ flex-direction: row;
+}
+
+@include media($phone) {
+ #outer {
+ display: block;
+ overflow-y: scroll;
+ }
+}
diff --git a/html_orig/sass/pages/_about.scss b/html_orig/sass/pages/_about.scss
index 9eba5e8..d87129f 100644
--- a/html_orig/sass/pages/_about.scss
+++ b/html_orig/sass/pages/_about.scss
@@ -14,6 +14,7 @@
// mobile friendly
#logo2 {
max-width: 100%;
+ margin: 1rem;
}
td {
diff --git a/html_orig/sass/pages/_term.scss b/html_orig/sass/pages/_term.scss
index d642e6d..17634ba 100755
--- a/html_orig/sass/pages/_term.scss
+++ b/html_orig/sass/pages/_term.scss
@@ -1,4 +1,4 @@
-.page-term {
+body.term {
h1 {
font-size: fsize(5);
@include media($phone) {
@@ -34,14 +34,33 @@
button {
margin: 0 3px;
- padding: 10px 0;
- width: 18%;
- max-width: 65px;
- min-width: initial;
+ padding: 8px 5px;
+ //width: 18%;
+ min-width: 65px;
+ //max-width: 65px;
+ //min-width: initial;
cursor: pointer;
font-weight: bold;
}
}
+
+ #botnav {
+ padding-top: 1.5em;
+ text-align: center;
+
+ a {
+ padding: 0 dist(-2);
+ text-decoration: underline;
+
+ &, &:visited, &:link {
+ color: #2e4d6e;
+ }
+
+ &:hover {
+ color: #5abfff;
+ }
+ }
+ }
}
#termwrap {
diff --git a/html_orig/sass/pages/_wifi.scss b/html_orig/sass/pages/_wifi.scss
index 78a52f1..90a758d 100755
--- a/html_orig/sass/pages/_wifi.scss
+++ b/html_orig/sass/pages/_wifi.scss
@@ -13,11 +13,12 @@
margin: 0 (- dist(-3));
}
-#ap-loader, #ap-noscan {
+#ap-loader, #ap-noscan, #ap-scan {
background: rgba(white, .1);
border-radius: 5px;
padding: dist(-2);
margin-bottom: dist(-2);
+ margin-top: dist(-2);
}
#ap-noscan {
@@ -25,7 +26,14 @@
}
#ap-box {
- padding-bottom: dist(-2);
+ padding-top: dist(-2);
+
+ label {
+ display: block;
+ color: white;
+ font-weight: bold;
+ margin-bottom: dist(-3);
+ }
}
#psk-modal form {
@@ -43,6 +51,50 @@
@extend %form-row-spacing;
}
+%ap-inner {
+ cursor: pointer;
+ @include noselect;
+
+ position: relative;
+ &:active {
+ left: 0;
+ top: 1px;
+ }
+
+ border-radius: 3px;
+ color: #222;
+
+ background: #afafaf;
+ transition: background-color 0.5s;
+ &:hover { background: white }
+
+ display: flex;
+
+ .rssi {
+ min-width: 2.5rem;
+ flex: 0 0 15%;
+ text-align: right;
+
+ &:after {
+ padding-left: dist(-5);
+ content: '%';
+ font-size: fsize(-1);
+ }
+ }
+
+ .essid {
+ flex: 1 1 70%;
+ min-width: 0;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-weight: bold;
+ }
+
+ .auth {
+ flex: 0 0 15%;
+ }
+}
+
.AP {
// can't use margins inside a column
@@ -56,53 +108,78 @@
top: 0 !important; // no click effect
}
- // the actual silver box
.inner {
- cursor: pointer;
- @include noselect;
+ @extend %ap-inner;
- position: relative;
- &:active {
- left: 0;
- top: 1px;
+ & > * {
+ padding: dist(-1);
+ @include nowrap;
}
+ }
+}
- border-radius: 3px;
- color: #222;
+.AP-preview-nil {
+ padding: 8px;
+ border-radius: 5px;
+ border: 1px dashed #ddd;
+ width: 250px;
+ height: 94px;
+}
- background: #afafaf;
- transition: background-color 0.5s;
- &:hover { background: white }
+.AP-preview {
+ .wrap {
+ @extend %ap-inner;
- display: flex;
+ flex-direction: row;
+ background: #ddd !important; // override the hover effect #43de81
+ cursor: default;
+ top: 0 !important; // no click effect
+ overflow: hidden;
- & > * {
- padding: dist(-1);
- @include nowrap;
+ .inner {
+ display: flex;
+ flex-direction: column;
+
+ & > * {
+ padding: dist(-1);
+ @include nowrap;
+ }
}
+ .forget {
+ align-self: stretch;
+ line-height: 100%;
+ padding: dist(-1);
+ border-left: 1px solid #bbb;
+ display: flex;
+ align-items: center;
- .rssi {
- min-width: 2.5rem;
- flex: 0 0 15%;
- text-align: right;
+ &, &:hover {
+ color: black;
+ text-decoration: none;
+ }
+
+ font-size: 28px;
+
+ &:hover {
+ background: #dc4a6a;
+ color: white;
+ border-left: 1px solid #666;
+ border-bottom-right-radius: 3px;
+ border-top-right-radius: 3px;
+ }
- &:after {
- padding-left: dist(-5);
- content: '%';
- font-size: fsize(-1);
+ &:active {
+ position: relative;
+ padding-top: calc(#{dist(-1)} + 1px);
}
}
- .essid {
- flex: 1 1 70%;
- min-width: 0;
- text-overflow: ellipsis;
- overflow: hidden;
- font-weight: bold;
+ .essid, .passwd, .nopasswd {
+ padding-bottom: 0;
}
- .auth {
- flex: 0 0 15%;
+ .x-passwd {
+ font-family: monospace;
}
}
}
diff --git a/html_orig/sass/utils/_background-tiling.scss b/html_orig/sass/utils/_background-tiling.scss
new file mode 100755
index 0000000..c1c5916
--- /dev/null
+++ b/html_orig/sass/utils/_background-tiling.scss
@@ -0,0 +1,68 @@
+
+// Utilities for background tiling
+
+// Use a tile as background (w, h - size of time)
+@mixin tile_xy($w, $h, $x, $y) {
+ background-position: (-$x*$w) (-$y*$h);
+}
+
+
+// Use a square tile as background (size - w & h of time)
+@mixin tile($size, $x, $y) {
+ @include tile_xy($size, $size, $x, $y);
+}
+
+
+// Button with sprite-sheet
+// A B
+// B:hover B:hover
+@mixin tile_btn_h($w, $h, $x) {
+ @include tile_xy($w, $h, $x, 0);
+ &:hover {
+ @include tile_xy($w, $h, $x, 1);
+ }
+}
+
+// active the same as hover
+@mixin tile_btn_h_act($w, $h, $x) {
+ @include tile_xy($w, $h, $x, 0);
+ &:hover, &.active {
+ @include tile_xy($w, $h, $x, 1);
+ }
+}
+
+
+
+// Button with sprite-sheet
+// A A:hover
+// B B:hover
+@mixin tile_btn_v($w, $h, $y) {
+ @include tile_xy($w, $h, 0, $y);
+ &:hover {
+ @include tile_xy($w, $h, 1, $y);
+ }
+}
+
+// active the same as hover
+@mixin tile_btn_v_act($w, $h, $y) {
+ @include tile_xy($w, $h, 0, $y);
+ &:hover, &.active {
+ @include tile_xy($w, $h, 1, $y);
+ }
+}
+
+@mixin inset-shadow-top($w, $c) {
+ box-shadow: inset 0 $w ($w*2) (-$w) $c;
+}
+
+@mixin inset-shadow-bottom($w, $c) {
+ box-shadow: inset 0 (-$w) ($w*2) (-$w) $c;
+}
+
+@mixin inset-shadow-left($w, $c) {
+ box-shadow: inset $w 0 ($w*2) (-$w) $c;
+}
+
+@mixin inset-shadow-right($w, $c) {
+ box-shadow: inset (-$w) 0 ($w*2) (-$w) $c;
+}
diff --git a/html_orig/sass/utils/_index.scss b/html_orig/sass/utils/_index.scss
new file mode 100755
index 0000000..4e98733
--- /dev/null
+++ b/html_orig/sass/utils/_index.scss
@@ -0,0 +1,3 @@
+@import "background-tiling";
+@import "pointer";
+@import "misc";
diff --git a/html_orig/sass/utils/_misc.scss b/html_orig/sass/utils/_misc.scss
new file mode 100755
index 0000000..91c0d77
--- /dev/null
+++ b/html_orig/sass/utils/_misc.scss
@@ -0,0 +1,34 @@
+// Add a highlight for debugging
+@mixin highlight($color) {
+ outline: 1px solid $color;
+ background: rgba($color, .05);
+ box-shadow: 0 0 2px 2px rgba($color, .2), inset 0 0 2px 2px rgba($color, .2);
+}
+
+// Ellipsis, but for block elements
+@mixin block-ellipsis($width: 100%) {
+ display: block;
+ max-width: $width;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ word-wrap: normal;
+}
+
+// No margins, padding, borders
+@mixin naked() {
+ border: 0 none;
+ margin: 0;
+ padding: 0;
+ text-decoration: none;
+}
+
+@mixin translate($x, $y) {
+ @include transform(translate($x, $y));
+}
+
+// Disallow wrapping
+@mixin nowrap() {
+ white-space: nowrap;
+ word-wrap: normal;
+}
diff --git a/html_orig/sass/utils/_pointer.scss b/html_orig/sass/utils/_pointer.scss
new file mode 100755
index 0000000..c6a9179
--- /dev/null
+++ b/html_orig/sass/utils/_pointer.scss
@@ -0,0 +1,26 @@
+
+@mixin click-through() {
+ pointer-events: none;
+}
+
+
+// Disallow text selection
+@mixin noselect() {
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+
+// Allow text selection
+@mixin can-select() {
+ -webkit-user-select: text;
+ -khtml-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
+
+ cursor: text;
+}
diff --git a/html_orig/server.sh b/html_orig/server.sh
index 4b0b817..467ae0b 100755
--- a/html_orig/server.sh
+++ b/html_orig/server.sh
@@ -1,3 +1,3 @@
#!/bin/bash
-xterm -e "php -S localhost:2000"
+xterm -e "php -S 0.0.0.0:2000"
diff --git a/html_orig/term.html b/html_orig/term.html
deleted file mode 100644
index c0d5106..0000000
--- a/html_orig/term.html
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
- ESP8266 Remote Terminal
-
-
-
-
-
-
-
-
-
-ESP8266 Remote Terminal
-
-
-
-
- WiFi config Help About
-
-
-
-
-
-
diff --git a/html_orig/term_test.php b/html_orig/term_test.php
deleted file mode 100644
index efa507a..0000000
--- a/html_orig/term_test.php
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
- WiFi Settings - ESP8266 Remote Terminal
-
-
-
-
-
-
-
-
-WiFi settings
-
-
-
-
- WiFi mode
- %WiFiMode%
-
-
- IP
- %StaIP%
-
-
- Switch to
-
-
-
- AP channel
-
-
-
-
-
- AP name
-
-
-
-
-
- Some changes require a reboot, dropping connection. It can take a while to re-connect.
-
- If you lose access , hold the BOOT button for 2 seconds (the Tx LED starts blinking) to re-enable AP mode.
- If that fails, hold the BOOT button for over 5 seconds (rapid Tx LED flashing) to perform a factory reset.
-
-
-
-
-
-
-
Select AP to join
-
Scanning.
-
Can't scan in AP-only mode.
-
-
-
-
- Terminal Help About
-
-
-
-
-
-
-
diff --git a/html_orig/wifi_conn.html b/html_orig/wifi_conn.html
deleted file mode 100755
index a9448dc..0000000
--- a/html_orig/wifi_conn.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
- Connecting…
-
-
-
-
-
-
-Connecting to network
-
-
-
-
-
-
diff --git a/html_orig/wifi_test.php b/html_orig/wifi_test.php
deleted file mode 100644
index b1863e7..0000000
--- a/html_orig/wifi_test.php
+++ /dev/null
@@ -1,15 +0,0 @@
->0)&0xff, ((ip)>>8)&0xff, ((ip)>>16)&0xff, ((ip)>>24)&0xff
+
+/**
+ * Helper that retrieves an arg from `connData->getArgs` and stores it in `buff`. Returns 1 on success
+ */
+#define GET_ARG(key) (httpdFindArg(connData->getArgs, key, buff, sizeof(buff)) > 0)
+
+#define STR_HELPER(x) #x
+#define STR(x) STR_HELPER(x)
+
+#endif //ESP_VT100_FIRMWARE_HELPERS_H
diff --git a/libesphttpd b/libesphttpd
index 03003ea..f3dd1a2 160000
--- a/libesphttpd
+++ b/libesphttpd
@@ -1 +1 @@
-Subproject commit 03003ea591a272df50159ba52f84ca84c5cad78e
+Subproject commit f3dd1a25993775bec062a1906ced7b07a7fc9db1
diff --git a/user/cgi_appcfg.c b/user/cgi_appcfg.c
new file mode 100644
index 0000000..e766139
--- /dev/null
+++ b/user/cgi_appcfg.c
@@ -0,0 +1,178 @@
+/*
+Cgi/template routines for configuring non-wifi settings
+*/
+
+#include
+#include "cgi_appcfg.h"
+#include "persist.h"
+#include "screen.h"
+#include "helpers.h"
+
+#define SET_REDIR_SUC "/cfg/app"
+#define SET_REDIR_ERR SET_REDIR_SUC"?err="
+
+/**
+ * Universal CGI endpoint to set Terminal params.
+ */
+httpd_cgi_state ICACHE_FLASH_ATTR
+cgiAppCfgSetParams(HttpdConnData *connData)
+{
+ char buff[50];
+
+ char redir_url_buf[300];
+ char *redir_url = redir_url_buf;
+ redir_url += sprintf(redir_url, SET_REDIR_ERR);
+ // we'll test if anything was printed by looking for \0 in failed_keys_buf
+
+ if (connData->conn == NULL) {
+ //Connection aborted. Clean up.
+ return HTTPD_CGI_DONE;
+ }
+
+ // width and height must always go together so we can do max size validation
+ if (GET_ARG("term_width")) {
+ dbg("Default screen width: %s", buff);
+ int w = atoi(buff);
+ if (w > 1) {
+ if (GET_ARG("term_height")) {
+ dbg("Default screen height: %s", buff);
+ int h = atoi(buff);
+ if (h > 1) {
+ if (w * h <= MAX_SCREEN_SIZE) {
+ termconf->width = w;
+ termconf->height = h;
+ } else {
+ warn("Bad dimensions: %d x %d (total %d)", w, h, w*h);
+ redir_url += sprintf(redir_url, "term_width,term_height,");
+ }
+ } else {
+ warn("Bad height: \"%s\"", buff);
+ redir_url += sprintf(redir_url, "term_width,");
+ }
+ } else {
+ warn("Missing height arg!");
+ // this wont happen normally when the form is used
+ redir_url += sprintf(redir_url, "term_width,term_height,");
+ }
+ } else {
+ warn("Bad width: \"%s\"", buff);
+ redir_url += sprintf(redir_url, "term_width,");
+ }
+ }
+
+ if (GET_ARG("default_bg")) {
+ dbg("Screen default BG: %s", buff);
+ int color = atoi(buff);
+ if (color >= 0 && color < 16) {
+ termconf->default_bg = (u8) color;
+ } else {
+ warn("Bad color %s", buff);
+ redir_url += sprintf(redir_url, "default_bg,");
+ }
+ }
+
+ if (GET_ARG("default_fg")) {
+ dbg("Screen default FG: %s", buff);
+ int color = atoi(buff);
+ if (color >= 0 && color < 16) {
+ termconf->default_fg = (u8) color;
+ } else {
+ warn("Bad color %s", buff);
+ redir_url += sprintf(redir_url, "default_fg,");
+ }
+ }
+
+ if (GET_ARG("term_title")) {
+ dbg("Terminal title default text: \"%s\"", buff);
+ strncpy_safe(termconf->title, buff, 64); // ATTN those must match the values in
+ }
+
+ if (GET_ARG("btn1")) {
+ dbg("Button1 default text: \"%s\"", buff);
+ strncpy_safe(termconf->btn1, buff, 10);
+ }
+
+ if (GET_ARG("btn2")) {
+ dbg("Button1 default text: \"%s\"", buff);
+ strncpy_safe(termconf->btn2, buff, 10);
+ }
+
+ if (GET_ARG("btn3")) {
+ dbg("Button1 default text: \"%s\"", buff);
+ strncpy_safe(termconf->btn3, buff, 10);
+ }
+
+ if (GET_ARG("btn4")) {
+ dbg("Button1 default text: \"%s\"", buff);
+ strncpy_safe(termconf->btn4, buff, 10);
+ }
+
+ if (GET_ARG("btn5")) {
+ dbg("Button1 default text: \"%s\"", buff);
+ strncpy_safe(termconf->btn5, buff, 10);
+ }
+
+ if (redir_url_buf[strlen(SET_REDIR_ERR)] == 0) {
+ // All was OK
+ info("Set app params - success, saving...");
+
+ terminal_apply_settings();
+ persist_store();
+
+ httpdRedirect(connData, SET_REDIR_SUC);
+ } else {
+ warn("Some settings did not validate, asking for correction");
+ // Some errors, appended to the URL as ?err=
+ httpdRedirect(connData, redir_url_buf);
+ }
+ return HTTPD_CGI_DONE;
+}
+
+
+httpd_cgi_state ICACHE_FLASH_ATTR
+tplAppCfg(HttpdConnData *connData, char *token, void **arg)
+{
+#define BUFLEN 100
+ char buff[BUFLEN];
+
+ if (token == NULL) {
+ // We're done
+ return HTTPD_CGI_DONE;
+ }
+
+ strcpy(buff, ""); // fallback
+
+ if (streq(token, "term_width")) {
+ sprintf(buff, "%d", termconf->width);
+ }
+ else if (streq(token, "term_height")) {
+ sprintf(buff, "%d", termconf->height);
+ }
+ else if (streq(token, "default_bg")) {
+ sprintf(buff, "%d", termconf->default_bg);
+ }
+ else if (streq(token, "default_fg")) {
+ sprintf(buff, "%d", termconf->default_fg);
+ }
+ else if (streq(token, "term_title")) {
+ strncpy_safe(buff, termconf->title, BUFLEN);
+ }
+ else if (streq(token, "btn1")) {
+ strncpy_safe(buff, termconf->btn1, BUFLEN);
+ }
+ else if (streq(token, "btn2")) {
+ strncpy_safe(buff, termconf->btn2, BUFLEN);
+ }
+ else if (streq(token, "btn3")) {
+ strncpy_safe(buff, termconf->btn3, BUFLEN);
+ }
+ else if (streq(token, "btn4")) {
+ strncpy_safe(buff, termconf->btn4, BUFLEN);
+ }
+ else if (streq(token, "btn5")) {
+ strncpy_safe(buff, termconf->btn5, BUFLEN);
+ }
+
+ httpdSend(connData, buff, -1);
+ return HTTPD_CGI_DONE;
+}
diff --git a/user/cgi_appcfg.h b/user/cgi_appcfg.h
new file mode 100644
index 0000000..823a0f1
--- /dev/null
+++ b/user/cgi_appcfg.h
@@ -0,0 +1,9 @@
+#ifndef CGIAPPCFG_H
+#define CGIAPPCFG_H
+
+#include "httpd.h"
+
+httpd_cgi_state cgiAppCfgSetParams(HttpdConnData *connData);
+httpd_cgi_state tplAppCfg(HttpdConnData *connData, char *token, void **arg);
+
+#endif
diff --git a/user/cgi_main.c b/user/cgi_main.c
index 997ebef..70a5ec7 100644
--- a/user/cgi_main.c
+++ b/user/cgi_main.c
@@ -5,9 +5,7 @@
#include "cgi_main.h"
#include "screen.h"
#include "user_main.h"
-
-#define STR_HELPER(x) #x
-#define STR(x) STR_HELPER(x)
+#include "helpers.h"
/**
* Main page template substitution
@@ -28,7 +26,33 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplScreen(HttpdConnData *connData, char *token
const int bufsiz = 512;
char buff[bufsiz];
- if (streq(token, "screenData")) {
+ if (streq(token, "term_title")) {
+ httpdSend(connData, termconf->title, -1);
+ }
+ else if (streq(token, "btn1")) {
+ httpdSend(connData, termconf->btn1, -1);
+ }
+ else if (streq(token, "btn2")) {
+ httpdSend(connData, termconf->btn2, -1);
+ }
+ else if (streq(token, "btn3")) {
+ httpdSend(connData, termconf->btn3, -1);
+ }
+ else if (streq(token, "btn4")) {
+ httpdSend(connData, termconf->btn4, -1);
+ }
+ else if (streq(token, "btn5")) {
+ httpdSend(connData, termconf->btn5, -1);
+ }
+ else if (streq(token, "default_bg")) {
+ sprintf(buff, "%d", termconf->default_bg);
+ httpdSend(connData, buff, -1);
+ }
+ else if (streq(token, "default_fg")) {
+ sprintf(buff, "%d", termconf->default_fg);
+ httpdSend(connData, buff, -1);
+ }
+ else if (streq(token, "screenData")) {
httpd_cgi_state cont = screenSerializeToBuffer(buff, bufsiz, arg);
httpdSend(connData, buff, -1);
return cont;
diff --git a/user/cgi_network.c b/user/cgi_network.c
new file mode 100644
index 0000000..ad4ac21
--- /dev/null
+++ b/user/cgi_network.c
@@ -0,0 +1,251 @@
+/*
+configuring the network settings
+*/
+
+#include
+#include "cgi_network.h"
+#include "wifimgr.h"
+#include "persist.h"
+#include "helpers.h"
+
+#define SET_REDIR_SUC "/cfg/network"
+#define SET_REDIR_ERR SET_REDIR_SUC"?err="
+
+/**
+ * Callback for async timer
+ */
+static void ICACHE_FLASH_ATTR applyNetSettingsLaterCb(void *arg)
+{
+ wifimgr_apply_settings();
+}
+
+/**
+ * Universal CGI endpoint to set network params.
+ * Those affect DHCP etc, may cause a disconnection.
+ */
+httpd_cgi_state ICACHE_FLASH_ATTR cgiNetworkSetParams(HttpdConnData *connData)
+{
+ static ETSTimer timer;
+
+ char buff[50];
+
+ char redir_url_buf[300];
+ char *redir_url = redir_url_buf;
+ redir_url += sprintf(redir_url, SET_REDIR_ERR);
+ // we'll test if anything was printed by looking for \0 in failed_keys_buf
+
+ if (connData->conn == NULL) {
+ //Connection aborted. Clean up.
+ return HTTPD_CGI_DONE;
+ }
+
+ // ---- AP DHCP server lease time ----
+
+ if (GET_ARG("ap_dhcp_time")) {
+ dbg("Setting DHCP lease time to: %s min.", buff);
+ int min = atoi(buff);
+ if (min >= 1 && min <= 2880) {
+ if (wificonf->ap_dhcp_time != min) {
+ wificonf->ap_dhcp_time = (u16) min;
+ wifi_change_flags.ap = true;
+ }
+ } else {
+ warn("Lease time %s out of allowed range 1-2880.", buff);
+ redir_url += sprintf(redir_url, "ap_dhcp_time,");
+ }
+ }
+
+ // ---- AP DHCP start and end IP ----
+
+ if (GET_ARG("ap_dhcp_start")) {
+ dbg("Setting DHCP range start IP to: \"%s\"", buff);
+ u32 ip = ipaddr_addr(buff);
+ if (ip != 0) {
+ if (wificonf->ap_dhcp_range.start_ip.addr != ip) {
+ wificonf->ap_dhcp_range.start_ip.addr = ip;
+ wifi_change_flags.ap = true;
+ }
+ } else {
+ warn("Bad IP: %s", buff);
+ redir_url += sprintf(redir_url, "ap_dhcp_start,");
+ }
+ }
+
+ if (GET_ARG("ap_dhcp_end")) {
+ dbg("Setting DHCP range end IP to: \"%s\"", buff);
+ u32 ip = ipaddr_addr(buff);
+ if (ip != 0) {
+ if (wificonf->ap_dhcp_range.end_ip.addr != ip) {
+ wificonf->ap_dhcp_range.end_ip.addr = ip;
+ wifi_change_flags.ap = true;
+ }
+ } else {
+ warn("Bad IP: %s", buff);
+ redir_url += sprintf(redir_url, "ap_dhcp_end,");
+ }
+ }
+
+ // ---- AP local address & config ----
+
+ if (GET_ARG("ap_addr_ip")) {
+ dbg("Setting AP local IP to: \"%s\"", buff);
+ u32 ip = ipaddr_addr(buff);
+ if (ip != 0) {
+ if (wificonf->ap_addr.ip.addr != ip) {
+ wificonf->ap_addr.ip.addr = ip;
+ wificonf->ap_addr.gw.addr = ip; // always the same, we're the router here
+ wifi_change_flags.ap = true;
+ }
+ } else {
+ warn("Bad IP: %s", buff);
+ redir_url += sprintf(redir_url, "ap_addr_ip,");
+ }
+ }
+
+ if (GET_ARG("ap_addr_mask")) {
+ dbg("Setting AP local IP netmask to: \"%s\"", buff);
+ u32 ip = ipaddr_addr(buff);
+ if (ip != 0) {
+ if (wificonf->ap_addr.netmask.addr != ip) {
+ // ideally this should be checked to match the IP.
+ // Let's hope users know what they're doing
+ wificonf->ap_addr.netmask.addr = ip;
+ wifi_change_flags.ap = true;
+ }
+ } else {
+ warn("Bad IP mask: %s", buff);
+ redir_url += sprintf(redir_url, "ap_addr_mask,");
+ }
+ }
+
+ // ---- Station enable/disable DHCP ----
+
+ // DHCP enable / disable (disable means static IP is enabled)
+ if (GET_ARG("sta_dhcp_enable")) {
+ dbg("DHCP enable = %s", buff);
+ int enable = atoi(buff);
+ if (wificonf->sta_dhcp_enable != enable) {
+ wificonf->sta_dhcp_enable = (bool)enable;
+ wifi_change_flags.sta = true;
+ }
+ }
+
+ // ---- Station IP config (Static IP) ----
+
+ if (GET_ARG("sta_addr_ip")) {
+ dbg("Setting Station mode static IP to: \"%s\"", buff);
+ u32 ip = ipaddr_addr(buff);
+ if (ip != 0) {
+ if (wificonf->sta_addr.ip.addr != ip) {
+ wificonf->sta_addr.ip.addr = ip;
+ wifi_change_flags.sta = true;
+ }
+ } else {
+ warn("Bad IP: %s", buff);
+ redir_url += sprintf(redir_url, "sta_addr_ip,");
+ }
+ }
+
+ if (GET_ARG("sta_addr_mask")) {
+ dbg("Setting Station mode static IP netmask to: \"%s\"", buff);
+ u32 ip = ipaddr_addr(buff);
+ if (ip != 0 && ip != 0xFFFFFFFFUL) {
+ if (wificonf->sta_addr.netmask.addr != ip) {
+ wificonf->sta_addr.netmask.addr = ip;
+ wifi_change_flags.sta = true;
+ }
+ } else {
+ warn("Bad IP mask: %s", buff);
+ redir_url += sprintf(redir_url, "sta_addr_mask,");
+ }
+ }
+
+ if (GET_ARG("sta_addr_gw")) {
+ dbg("Setting Station mode static IP default gateway to: \"%s\"", buff);
+ u32 ip = ipaddr_addr(buff);
+ if (ip != 0) {
+ if (wificonf->sta_addr.gw.addr != ip) {
+ wificonf->sta_addr.gw.addr = ip;
+ wifi_change_flags.sta = true;
+ }
+ } else {
+ warn("Bad gw IP: %s", buff);
+ redir_url += sprintf(redir_url, "sta_addr_gw,");
+ }
+ }
+
+ if (redir_url_buf[strlen(SET_REDIR_ERR)] == 0) {
+ // All was OK
+ info("Set network params - success, applying in 1000 ms");
+
+ // Settings are applied only if all was OK
+ persist_store();
+
+ // Delayed settings apply, so the response page has a chance to load.
+ // If user connects via the Station IF, they may not even notice the connection reset.
+ os_timer_disarm(&timer);
+ os_timer_setfn(&timer, applyNetSettingsLaterCb, NULL);
+ os_timer_arm(&timer, 1000, false);
+
+ httpdRedirect(connData, SET_REDIR_SUC);
+ } else {
+ warn("Some WiFi settings did not validate, asking for correction");
+ // Some errors, appended to the URL as ?err=
+ httpdRedirect(connData, redir_url_buf);
+ }
+ return HTTPD_CGI_DONE;
+}
+
+
+//Template code for the WLAN page.
+httpd_cgi_state ICACHE_FLASH_ATTR tplNetwork(HttpdConnData *connData, char *token, void **arg)
+{
+ char buff[100];
+ u8 mac[6];
+
+ if (token == NULL) {
+ // We're done
+ return HTTPD_CGI_DONE;
+ }
+
+ strcpy(buff, ""); // fallback
+
+ if (streq(token, "ap_dhcp_time")) {
+ sprintf(buff, "%d", wificonf->ap_dhcp_time);
+ }
+ else if (streq(token, "ap_dhcp_start")) {
+ sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->ap_dhcp_range.start_ip.addr));
+ }
+ else if (streq(token, "ap_dhcp_end")) {
+ sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->ap_dhcp_range.end_ip.addr));
+ }
+ else if (streq(token, "ap_addr_ip")) {
+ sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->ap_addr.ip.addr));
+ }
+ else if (streq(token, "ap_addr_mask")) {
+ sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->ap_addr.netmask.addr));
+ }
+ else if (streq(token, "sta_dhcp_enable")) {
+ sprintf(buff, "%d", wificonf->sta_dhcp_enable);
+ }
+ else if (streq(token, "sta_addr_ip")) {
+ sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.ip.addr));
+ }
+ else if (streq(token, "sta_addr_mask")) {
+ sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.netmask.addr));
+ }
+ else if (streq(token, "sta_addr_gw")) {
+ sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.gw.addr));
+ }
+ else if (streq(token, "sta_mac")) {
+ wifi_get_macaddr(STATION_IF, mac);
+ sprintf(buff, MACSTR, MAC2STR(mac));
+ }
+ else if (streq(token, "ap_mac")) {
+ wifi_get_macaddr(SOFTAP_IF, mac);
+ sprintf(buff, MACSTR, MAC2STR(mac));
+ }
+
+ httpdSend(connData, buff, -1);
+ return HTTPD_CGI_DONE;
+}
diff --git a/user/cgi_network.h b/user/cgi_network.h
new file mode 100644
index 0000000..818c953
--- /dev/null
+++ b/user/cgi_network.h
@@ -0,0 +1,9 @@
+#ifndef CGINET_H
+#define CGINET_H
+
+#include "httpd.h"
+
+httpd_cgi_state cgiNetworkSetParams(HttpdConnData *connData);
+httpd_cgi_state tplNetwork(HttpdConnData *connData, char *token, void **arg);
+
+#endif
diff --git a/user/cgi_persist.c b/user/cgi_persist.c
new file mode 100644
index 0000000..7f2faac
--- /dev/null
+++ b/user/cgi_persist.c
@@ -0,0 +1,74 @@
+/*
+Cgi/template routines for configuring non-wifi settings
+*/
+
+#include
+#include "cgi_persist.h"
+#include "persist.h"
+#include "helpers.h"
+
+#define SET_REDIR_SUC "/cfg/admin"
+
+static bool ICACHE_FLASH_ATTR
+verify_admin_pw(const char *pw)
+{
+ // This is not really for security, but to prevent someone who
+ // shouldn't touch those settings from fucking it up.
+ return streq(pw, STR(ADMIN_PASSWORD)); // the PW comes from the makefile
+}
+
+httpd_cgi_state ICACHE_FLASH_ATTR
+cgiPersistWriteDefaults(HttpdConnData *connData)
+{
+ char buff[50];
+
+ if (connData->conn == NULL) {
+ //Connection aborted. Clean up.
+ return HTTPD_CGI_DONE;
+ }
+
+ // width and height must always go together so we can do max size validation
+ if (GET_ARG("pw")) {
+ dbg("Entered password for admin: %s", buff);
+ if (verify_admin_pw(buff)) {
+ dbg("pw is OK");
+
+ persist_set_as_default();
+
+ httpdRedirect(connData, SET_REDIR_SUC);
+ return HTTPD_CGI_DONE;
+ }
+ // if pw failed, show the same error as if it's wrong
+ }
+
+ httpdRedirect(connData, SET_REDIR_SUC "?err=Password"); // this will show in the "validation errors" box
+ return HTTPD_CGI_DONE;
+}
+
+
+httpd_cgi_state ICACHE_FLASH_ATTR
+cgiPersistRestoreDefaults(HttpdConnData *connData)
+{
+ if (connData->conn == NULL) {
+ //Connection aborted. Clean up.
+ return HTTPD_CGI_DONE;
+ }
+
+ persist_restore_default();
+ httpdRedirect(connData, SET_REDIR_SUC);
+ return HTTPD_CGI_DONE;
+}
+
+httpd_cgi_state ICACHE_FLASH_ATTR
+cgiPersistRestoreHard(HttpdConnData *connData)
+{
+ if (connData->conn == NULL) {
+ //Connection aborted. Clean up.
+ return HTTPD_CGI_DONE;
+ }
+
+ persist_restore_hard_default();
+
+ httpdRedirect(connData, SET_REDIR_SUC);
+ return HTTPD_CGI_DONE;
+}
diff --git a/user/cgi_persist.h b/user/cgi_persist.h
new file mode 100644
index 0000000..4875407
--- /dev/null
+++ b/user/cgi_persist.h
@@ -0,0 +1,10 @@
+#ifndef CGIPERSIST_H
+#define CGIPERSIST_H
+
+#include "httpd.h"
+
+httpd_cgi_state cgiPersistWriteDefaults(HttpdConnData *connData);
+httpd_cgi_state cgiPersistRestoreDefaults(HttpdConnData *connData);
+httpd_cgi_state cgiPersistRestoreHard(HttpdConnData *connData);
+
+#endif
diff --git a/user/cgi_wifi.c b/user/cgi_wifi.c
new file mode 100644
index 0000000..653f928
--- /dev/null
+++ b/user/cgi_wifi.c
@@ -0,0 +1,585 @@
+/*
+Cgi/template routines for the /wifi url.
+*/
+
+/*
+ * ----------------------------------------------------------------------------
+ * "THE BEER-WARE LICENSE" (Revision 42):
+ * Jeroen Domburg wrote this file. As long as you retain
+ * this notice you can do whatever you want with this stuff. If we meet some day,
+ * and you think this stuff is worth it, you can buy me a beer in return.
+ * ----------------------------------------------------------------------------
+ *
+ * File adapted and improved by Ondřej Hruška
+ */
+
+#include
+#include "cgi_wifi.h"
+#include "wifimgr.h"
+#include "persist.h"
+#include "helpers.h"
+
+#define SET_REDIR_SUC "/cfg/wifi"
+#define SET_REDIR_ERR SET_REDIR_SUC"?err="
+
+/** WiFi access point data */
+typedef struct {
+ char ssid[32];
+ char bssid[8];
+ int channel;
+ char rssi;
+ char enc;
+} ApData;
+
+/** Scan result type */
+typedef struct {
+ char scanInProgress; //if 1, don't access the underlying stuff from the webpage.
+ ApData **apData;
+ int noAps;
+} ScanResultData;
+
+/** Static scan status storage. */
+static ScanResultData cgiWifiAps;
+
+/** Connection to AP periodic check timer */
+static os_timer_t staCheckTimer;
+
+/**
+ * Calculate approximate signal strength % from RSSI
+ */
+int ICACHE_FLASH_ATTR rssi2perc(int rssi)
+{
+ int r;
+
+ if (rssi > 200)
+ r = 100;
+ else if (rssi < 100)
+ r = 0;
+ else
+ r = 100 - 2 * (200 - rssi); // approx.
+
+ if (r > 100) r = 100;
+ if (r < 0) r = 0;
+
+ return r;
+}
+
+/**
+ * Convert Auth type to string
+ */
+const ICACHE_FLASH_ATTR char *auth2str(AUTH_MODE auth)
+{
+ switch (auth) {
+ case AUTH_OPEN:
+ return "Open";
+ case AUTH_WEP:
+ return "WEP";
+ case AUTH_WPA_PSK:
+ return "WPA";
+ case AUTH_WPA2_PSK:
+ return "WPA2";
+ case AUTH_WPA_WPA2_PSK:
+ return "WPA/WPA2";
+ default:
+ return "Unknown";
+ }
+}
+
+/**
+ * Convert WiFi opmode to string
+ */
+const ICACHE_FLASH_ATTR char *opmode2str(WIFI_MODE opmode)
+{
+ switch (opmode) {
+ case NULL_MODE:
+ return "Disabled";
+ case STATION_MODE:
+ return "Client";
+ case SOFTAP_MODE:
+ return "AP only";
+ case STATIONAP_MODE:
+ return "Client+AP";
+ default:
+ return "Unknown";
+ }
+}
+
+/**
+ * Callback the code calls when a wlan ap scan is done. Basically stores the result in
+ * the static cgiWifiAps struct.
+ *
+ * @param arg - a pointer to {struct bss_info}, which is a linked list of the found APs
+ * @param status - OK if the scan succeeded
+ */
+void ICACHE_FLASH_ATTR wifiScanDoneCb(void *arg, STATUS status)
+{
+ int n;
+ struct bss_info *bss_link = (struct bss_info *) arg;
+ dbg("wifiScanDoneCb %d", status);
+ if (status != OK) {
+ cgiWifiAps.scanInProgress = 0;
+ return;
+ }
+
+ // Clear prev ap data if needed.
+ if (cgiWifiAps.apData != NULL) {
+ for (n = 0; n < cgiWifiAps.noAps; n++) free(cgiWifiAps.apData[n]);
+ free(cgiWifiAps.apData);
+ }
+
+ // Count amount of access points found.
+ n = 0;
+ while (bss_link != NULL) {
+ bss_link = bss_link->next.stqe_next;
+ n++;
+ }
+ // Allocate memory for access point data
+ cgiWifiAps.apData = (ApData **) malloc(sizeof(ApData *) * n);
+ if (cgiWifiAps.apData == NULL) {
+ error("Out of memory allocating apData");
+ return;
+ }
+ cgiWifiAps.noAps = n;
+ info("Scan done: found %d APs", n);
+
+ // Copy access point data to the static struct
+ n = 0;
+ bss_link = (struct bss_info *) arg;
+ while (bss_link != NULL) {
+ if (n >= cgiWifiAps.noAps) {
+ // This means the bss_link changed under our nose. Shouldn't happen!
+ // Break because otherwise we will write in unallocated memory.
+ error("Huh? I have more than the allocated %d aps!", cgiWifiAps.noAps);
+ break;
+ }
+ // Save the ap data.
+ cgiWifiAps.apData[n] = (ApData *) malloc(sizeof(ApData));
+ if (cgiWifiAps.apData[n] == NULL) {
+ error("Can't allocate mem for ap buff.");
+ cgiWifiAps.scanInProgress = 0;
+ return;
+ }
+ cgiWifiAps.apData[n]->rssi = bss_link->rssi;
+ cgiWifiAps.apData[n]->channel = bss_link->channel;
+ cgiWifiAps.apData[n]->enc = bss_link->authmode;
+ strncpy(cgiWifiAps.apData[n]->ssid, (char *) bss_link->ssid, 32);
+ strncpy(cgiWifiAps.apData[n]->bssid, (char *) bss_link->bssid, 6);
+
+ bss_link = bss_link->next.stqe_next;
+ n++;
+ }
+ // We're done.
+ cgiWifiAps.scanInProgress = 0;
+}
+
+/**
+ * Routine to start a WiFi access point scan.
+ */
+static void ICACHE_FLASH_ATTR wifiStartScan(void)
+{
+ if (cgiWifiAps.scanInProgress) return;
+ cgiWifiAps.scanInProgress = 1;
+ wifi_station_scan(NULL, wifiScanDoneCb);
+}
+
+/**
+ * This CGI is called from the bit of AJAX-code in wifi.tpl. It will initiate a
+ * scan for access points and if available will return the result of an earlier scan.
+ * The result is embedded in a bit of JSON parsed by the javascript in wifi.tpl.
+ */
+httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiScan(HttpdConnData *connData)
+{
+ int pos = (int) connData->cgiData;
+ int len;
+ char buff[256];
+
+ // 2nd and following runs of the function via MORE:
+ if (!cgiWifiAps.scanInProgress && pos != 0) {
+ // Fill in json code for an access point
+ if (pos - 1 < cgiWifiAps.noAps) {
+ int rssi = cgiWifiAps.apData[pos - 1]->rssi;
+
+ len = sprintf(buff, "{\"essid\": \"%s\", \"bssid\": \""
+ MACSTR
+ "\", \"rssi\": %d, \"rssi_perc\": %d, \"enc\": %d, \"channel\": %d}%s",
+ cgiWifiAps.apData[pos - 1]->ssid,
+ MAC2STR(cgiWifiAps.apData[pos - 1]->bssid),
+ rssi,
+ rssi2perc(rssi),
+ cgiWifiAps.apData[pos - 1]->enc,
+ cgiWifiAps.apData[pos - 1]->channel,
+ (pos - 1 == cgiWifiAps.noAps - 1) ? "\n " : ",\n "); //<-terminator
+
+ httpdSend(connData, buff, len);
+ }
+ pos++;
+ if ((pos - 1) >= cgiWifiAps.noAps) {
+ len = sprintf(buff, " ]\n }\n}"); // terminate the whole object
+ httpdSend(connData, buff, len);
+ // Also start a new scan.
+ wifiStartScan();
+ return HTTPD_CGI_DONE;
+ }
+ else {
+ connData->cgiData = (void *) pos;
+ return HTTPD_CGI_MORE;
+ }
+ }
+
+ // First run of the function
+ httpdStartResponse(connData, 200);
+ httpdHeader(connData, "Content-Type", "application/json");
+ httpdEndHeaders(connData);
+
+ if (cgiWifiAps.scanInProgress == 1) {
+ // We're still scanning. Tell Javascript code that.
+ len = sprintf(buff, "{\n \"result\": {\n \"inProgress\": 1\n }\n}");
+ httpdSend(connData, buff, len);
+ return HTTPD_CGI_DONE;
+ }
+ else {
+ // We have a scan result. Pass it on.
+ len = sprintf(buff, "{\n \"result\": {\n \"inProgress\": 0,\n \"APs\": [\n ");
+ httpdSend(connData, buff, len);
+ if (cgiWifiAps.apData == NULL) cgiWifiAps.noAps = 0;
+ connData->cgiData = (void *) 1;
+ return HTTPD_CGI_MORE;
+ }
+}
+
+/**
+ * Cgi to get connection status.
+ *
+ * This endpoint returns JSON with keys:
+ * - status = 'idle', 'working' or 'fail',
+ * - ip = IP address, after connection succeeds
+ */
+httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiConnStatus(HttpdConnData *connData)
+{
+ char buff[100];
+ struct ip_info info;
+
+ httpdStartResponse(connData, 200);
+ httpdHeader(connData, "Content-Type", "application/json");
+ httpdEndHeaders(connData);
+
+ // if bad opmode or no SSID configured, skip any checks
+ if (!(wificonf->opmode & STATION_MODE) || wificonf->sta_ssid[0] == 0) {
+ httpdSend(connData, "{\"status\": \"disabled\"}", -1);
+ return HTTPD_CGI_DONE;
+ }
+
+ STATION_STATUS st = wifi_station_get_connect_status();
+ switch(st) {
+ case STATION_IDLE:
+ sprintf(buff, "{\"status\": \"idle\"}"); // unclear when this is used
+ break;
+
+ case STATION_CONNECTING:
+ sprintf(buff, "{\"status\": \"working\"}");
+ break;
+
+ case STATION_WRONG_PASSWORD:
+ sprintf(buff, "{\"status\": \"fail\", \"cause\": \"WRONG_PASSWORD\"}");
+ break;
+
+ case STATION_NO_AP_FOUND:
+ sprintf(buff, "{\"status\": \"fail\", \"cause\": \"AP_NOT_FOUND\"}");
+ break;
+
+ case STATION_CONNECT_FAIL:
+ sprintf(buff, "{\"status\": \"fail\", \"cause\": \"CONNECTION_FAILED\"}");
+ break;
+
+ case STATION_GOT_IP:
+ wifi_get_ip_info(STATION_IF, &info);
+ sprintf(buff, "{\"status\": \"success\", \"ip\": \""IPSTR"\"}", GOOD_IP2STR(info.ip.addr));
+ break;
+ }
+
+ httpdSend(connData, buff, -1);
+ return HTTPD_CGI_DONE;
+}
+
+/**
+ * Callback for async timer
+ */
+static void ICACHE_FLASH_ATTR applyWifiSettingsLaterCb(void *arg)
+{
+ (void*)arg;
+ wifimgr_apply_settings();
+}
+
+/**
+ * Universal CGI endpoint to set WiFi params.
+ * Note that some may cause a (delayed) restart.
+ */
+httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData)
+{
+ static ETSTimer timer;
+
+ char buff[50];
+
+ char redir_url_buf[300];
+ char *redir_url = redir_url_buf;
+ redir_url += sprintf(redir_url, SET_REDIR_ERR);
+ // we'll test if anything was printed by looking for \0 in failed_keys_buf
+
+ if (connData->conn == NULL) {
+ //Connection aborted. Clean up.
+ return HTTPD_CGI_DONE;
+ }
+
+ // ---- WiFi opmode ----
+
+ if (GET_ARG("opmode")) {
+ dbg("Setting WiFi opmode to: %s", buff);
+ int mode = atoi(buff);
+ if (mode > NULL_MODE && mode < MAX_MODE) {
+ wificonf->opmode = (WIFI_MODE) mode;
+ } else {
+ warn("Bad opmode value \"%s\"", buff);
+ redir_url += sprintf(redir_url, "opmode,");
+ }
+ }
+
+ if (GET_ARG("ap_enable")) {
+ dbg("Enable AP: %s", buff);
+ int enable = atoi(buff);
+
+ if (enable) {
+ wificonf->opmode |= SOFTAP_MODE;
+ } else {
+ wificonf->opmode &= ~SOFTAP_MODE;
+ }
+ }
+
+ if (GET_ARG("sta_enable")) {
+ dbg("Enable STA: %s", buff);
+ int enable = atoi(buff);
+
+ if (enable) {
+ wificonf->opmode |= STATION_MODE;
+ } else {
+ wificonf->opmode &= ~STATION_MODE;
+ }
+ }
+
+ // ---- AP transmit power ----
+
+ if (GET_ARG("tpw")) {
+ dbg("Setting AP power to: %s", buff);
+ int tpw = atoi(buff);
+ if (tpw >= 0 && tpw <= 82) { // 0 actually isn't 0 but quite low. 82 is very strong
+ if (wificonf->tpw != tpw) {
+ wificonf->tpw = (u8) tpw;
+ wifi_change_flags.ap = true;
+ }
+ } else {
+ warn("tpw %s out of allowed range 0-82.", buff);
+ redir_url += sprintf(redir_url, "tpw,");
+ }
+ }
+
+ // ---- AP channel (applies in AP-only mode) ----
+
+ if (GET_ARG("ap_channel")) {
+ info("ap_channel = %s", buff);
+ int channel = atoi(buff);
+ if (channel > 0 && channel < 15) {
+ if (wificonf->ap_channel != channel) {
+ wificonf->ap_channel = (u8) channel;
+ wifi_change_flags.ap = true;
+ }
+ } else {
+ warn("Bad channel value \"%s\", allowed 1-14", buff);
+ redir_url += sprintf(redir_url, "ap_channel,");
+ }
+ }
+
+ // ---- SSID name in AP mode ----
+
+ if (GET_ARG("ap_ssid")) {
+ // Replace all invalid ASCII with underscores
+ int i;
+ for (i = 0; i < 32; i++) {
+ char c = buff[i];
+ if (c == 0) break;
+ if (c < 32 || c >= 127) buff[i] = '_';
+ }
+ buff[i] = 0;
+
+ if (strlen(buff) > 0) {
+ if (!streq(wificonf->ap_ssid, buff)) {
+ info("Setting SSID to \"%s\"", buff);
+ strncpy_safe(wificonf->ap_ssid, buff, SSID_LEN);
+ wifi_change_flags.ap = true;
+ }
+ } else {
+ warn("Bad SSID len.");
+ redir_url += sprintf(redir_url, "ap_ssid,");
+ }
+ }
+
+ // ---- AP password ----
+
+ if (GET_ARG("ap_password")) {
+ // Users are free to use any stupid shit in ther password,
+ // but it may lock them out.
+ if (strlen(buff) == 0 || (strlen(buff) >= 8 && strlen(buff) < PASSWORD_LEN-1)) {
+ if (!streq(wificonf->ap_password, buff)) {
+ info("Setting AP password to \"%s\"", buff);
+ strncpy_safe(wificonf->ap_password, buff, PASSWORD_LEN);
+ wifi_change_flags.ap = true;
+ }
+ } else {
+ warn("Bad password len.");
+ redir_url += sprintf(redir_url, "ap_password,");
+ }
+ }
+
+ // ---- Hide AP network (do not announce) ----
+
+ if (GET_ARG("ap_hidden")) {
+ dbg("AP hidden = %s", buff);
+ int hidden = atoi(buff);
+ if (hidden != wificonf->ap_hidden) {
+ wificonf->ap_hidden = (hidden != 0);
+ wifi_change_flags.ap = true;
+ }
+ }
+
+ // ---- Station SSID (to connect to) ----
+
+ if (GET_ARG("sta_ssid")) {
+ if (!streq(wificonf->sta_ssid, buff)) {
+ // No verification needed, at worst it fails to connect
+ info("Setting station SSID to: \"%s\"", buff);
+ strncpy_safe(wificonf->sta_ssid, buff, SSID_LEN);
+ wifi_change_flags.sta = true;
+ }
+ }
+
+ // ---- Station password (empty for none is allowed) ----
+
+ if (GET_ARG("sta_password")) {
+ if (!streq(wificonf->sta_password, buff)) {
+ // No verification needed, at worst it fails to connect
+ info("Setting station password to: \"%s\"", buff);
+ strncpy_safe(wificonf->sta_password, buff, PASSWORD_LEN);
+ wifi_change_flags.sta = true;
+ }
+ }
+
+ if (redir_url_buf[strlen(SET_REDIR_ERR)] == 0) {
+ // All was OK
+ info("Set WiFi params - success, applying in 1000 ms");
+
+ // Settings are applied only if all was OK
+ //
+ // This is so that options that consist of multiple keys sent together are not applied
+ // only partially if set wrong, which could lead to eg. user losing access and having
+ // to reset to defaults.
+ persist_store();
+
+ // Delayed settings apply, so the response page has a chance to load.
+ // If user connects via the Station IF, they may not even notice the connection reset.
+ os_timer_disarm(&timer);
+ os_timer_setfn(&timer, applyWifiSettingsLaterCb, NULL);
+ os_timer_arm(&timer, 1000, false);
+
+ httpdRedirect(connData, SET_REDIR_SUC);
+ } else {
+ warn("Some WiFi settings did not validate, asking for correction");
+ // Some errors, appended to the URL as ?err=
+ httpdRedirect(connData, redir_url_buf);
+ }
+ return HTTPD_CGI_DONE;
+}
+
+
+//Template code for the WLAN page.
+httpd_cgi_state ICACHE_FLASH_ATTR tplWlan(HttpdConnData *connData, char *token, void **arg)
+{
+ char buff[100];
+ int x;
+ int connectStatus;
+
+ if (token == NULL) {
+ // We're done
+ return HTTPD_CGI_DONE;
+ }
+
+ strcpy(buff, ""); // fallback
+
+ if (streq(token, "opmode_name")) {
+ strcpy(buff, opmode2str(wificonf->opmode));
+ }
+ else if (streq(token, "opmode")) {
+ sprintf(buff, "%d", wificonf->opmode);
+ }
+ else if (streq(token, "sta_enable")) {
+ sprintf(buff, "%d", (wificonf->opmode & STATION_MODE) != 0);
+ }
+ else if (streq(token, "ap_enable")) {
+ sprintf(buff, "%d", (wificonf->opmode & SOFTAP_MODE) != 0);
+ }
+ else if (streq(token, "tpw")) {
+ sprintf(buff, "%d", wificonf->tpw);
+ }
+ else if (streq(token, "ap_channel")) {
+ sprintf(buff, "%d", wificonf->ap_channel);
+ }
+ else if (streq(token, "ap_ssid")) {
+ sprintf(buff, "%s", wificonf->ap_ssid);
+ }
+ else if (streq(token, "ap_password")) {
+ sprintf(buff, "%s", wificonf->ap_password);
+ }
+ else if (streq(token, "ap_hidden")) {
+ sprintf(buff, "%d", wificonf->ap_hidden);
+ }
+ else if (streq(token, "sta_ssid")) {
+ sprintf(buff, "%s", wificonf->sta_ssid);
+ }
+ else if (streq(token, "sta_password")) {
+ sprintf(buff, "%s", wificonf->sta_password);
+ }
+ else if (streq(token, "sta_rssi")) {
+ sprintf(buff, "%d", wifi_station_get_rssi());
+ }
+ else if (streq(token, "sta_active_ssid")) {
+ // For display of our current SSID
+ connectStatus = wifi_station_get_connect_status();
+ x = wifi_get_opmode();
+ if (x == SOFTAP_MODE || connectStatus != STATION_GOT_IP) {
+ strcpy(buff, "");
+ }
+ else {
+ struct station_config staconf;
+ wifi_station_get_config(&staconf);
+ strcpy(buff, (char *) staconf.ssid);
+ }
+ }
+ else if (streq(token, "sta_active_ip")) {
+ x = wifi_get_opmode();
+ connectStatus = wifi_station_get_connect_status();
+
+ if (x == SOFTAP_MODE || connectStatus != STATION_GOT_IP) {
+ strcpy(buff, "");
+ }
+ else {
+ struct ip_info info;
+ wifi_get_ip_info(STATION_IF, &info);
+ sprintf(buff, IPSTR, GOOD_IP2STR(info.ip.addr));
+
+// sprintf(buff, "ip: "IPSTR", mask: "IPSTR", gw: "IPSTR,
+// GOOD_IP2STR(info.ip.addr),
+// GOOD_IP2STR(info.netmask.addr),
+// GOOD_IP2STR(info.gw.addr));
+ }
+ }
+
+ httpdSend(connData, buff, -1);
+ return HTTPD_CGI_DONE;
+}
diff --git a/user/cgi_wifi.h b/user/cgi_wifi.h
new file mode 100644
index 0000000..53999dc
--- /dev/null
+++ b/user/cgi_wifi.h
@@ -0,0 +1,13 @@
+#ifndef CGIWIFI_H
+#define CGIWIFI_H
+
+#include "httpd.h"
+#include "helpers.h"
+
+httpd_cgi_state cgiWiFiScan(HttpdConnData *connData);
+
+httpd_cgi_state cgiWiFiSetParams(HttpdConnData *connData);
+httpd_cgi_state tplWlan(HttpdConnData *connData, char *token, void **arg);
+httpd_cgi_state cgiWiFiConnStatus(HttpdConnData *connData);
+
+#endif
diff --git a/user/io.c b/user/io.c
index e3ba069..4969e20 100644
--- a/user/io.c
+++ b/user/io.c
@@ -11,6 +11,8 @@
#include
#include "ansi_parser_callbacks.h"
+#include "wifimgr.h"
+#include "persist.h"
#define BTNGPIO 0
@@ -74,16 +76,16 @@ static void ICACHE_FLASH_ATTR resetBtnTimerCb(void *arg) {
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_U1TXD_BK);
- if (resetCnt>=10) { //5 secs pressed - FR
+ if (resetCnt>=10) { //5 secs pressed - FR (timer is at 500 ms)
info("BOOT-button triggered FACTORY RESET!");
- apars_handle_OSC_FactoryReset();
+ persist_restore_default();
}
else if (resetCnt>=2) { //1 sec pressed
- wifi_station_disconnect();
- wifi_set_opmode(STATIONAP_MODE); //reset to AP+STA mode
- info("BOOT-button triggered reset to AP mode, restarting...");
+ info("BOOT-button triggered reset to AP mode...");
- system_restart();
+ wificonf->opmode = STATIONAP_MODE;
+ persist_store();
+ wifimgr_apply_settings();
}
resetCnt=0;
}
diff --git a/user/persist.c b/user/persist.c
new file mode 100644
index 0000000..c1b7541
--- /dev/null
+++ b/user/persist.c
@@ -0,0 +1,171 @@
+//
+// Created by MightyPork on 2017/07/09.
+//
+
+#include "persist.h"
+#include
+#include "wifimgr.h"
+#include "screen.h"
+
+PersistBlock persist;
+
+#define PERSIST_SECTOR_ID 0x3D
+
+//region Persist and restore individual modules
+
+static void ICACHE_FLASH_ATTR
+apply_live_settings(void)
+{
+ dbg("[Persist] Applying live settings...");
+ terminal_apply_settings();
+ wifimgr_apply_settings();
+ // ...
+}
+
+static void ICACHE_FLASH_ATTR
+restore_live_settings_to_hard_defaults(void)
+{
+ wifimgr_restore_defaults();
+ terminal_restore_defaults();
+ // ...
+}
+
+//endregion
+
+/**
+ * Compute CRC32. Adapted from https://github.com/esp8266/Arduino
+ * @param data
+ * @param length
+ * @return crc32
+ */
+static uint32_t ICACHE_FLASH_ATTR
+calculateCRC32(const uint8_t *data, size_t length)
+{
+ uint32_t crc = 0xffffffff;
+ while (length--) {
+ uint8_t c = *data++;
+ for (uint32_t i = 0x80; i > 0; i >>= 1) {
+ bool bit = (bool) (crc & 0x80000000UL);
+ if (c & i) {
+ bit = !bit;
+ }
+ crc <<= 1;
+ if (bit) {
+ crc ^= 0x04c11db7UL;
+ }
+ }
+ }
+ return crc;
+}
+
+/**
+ * Compute a persist bundle checksum
+ *
+ * @param bundle
+ * @return
+ */
+static uint32_t ICACHE_FLASH_ATTR
+compute_checksum(AppConfigBundle *bundle)
+{
+ return calculateCRC32((uint8_t *) bundle, sizeof(AppConfigBundle) - 4) ^ CHECKSUM_SALT;
+}
+
+/**
+ * Load, verify and apply persistent config
+ */
+void ICACHE_FLASH_ATTR
+persist_load(void)
+{
+ info("[Persist] Loading stored settings from FLASH...");
+
+ dbg("sizeof(AppConfigBundle) = %d bytes", sizeof(AppConfigBundle));
+ dbg("sizeof(PersistBlock) = %d bytes", sizeof(PersistBlock));
+ dbg("sizeof(WiFiConfigBundle) = %d bytes", sizeof(WiFiConfigBundle));
+ dbg("sizeof(TerminalConfigBundle) = %d bytes", sizeof(TerminalConfigBundle));
+
+ bool hard_reset = false;
+
+ // Try to load
+ hard_reset |= !system_param_load(PERSIST_SECTOR_ID, 0, &persist, sizeof(PersistBlock));
+
+ // Verify checksums
+ if (hard_reset ||
+ (compute_checksum(&persist.defaults) != persist.defaults.checksum) ||
+ (compute_checksum(&persist.current) != persist.current.checksum)) {
+ error("[Persist] Checksum verification: FAILED");
+ hard_reset = true;
+ } else {
+ info("[Persist] Checksum verification: PASSED");
+ }
+
+ if (hard_reset) {
+ persist_restore_hard_default();
+ // this also stores them to flash and applies to modules
+ } else {
+ apply_live_settings();
+ }
+
+ info("[Persist] All settings loaded and applied.");
+}
+
+void ICACHE_FLASH_ATTR
+persist_store(void)
+{
+ info("[Persist] Storing all settings to FLASH...");
+
+ // Update checksums before write
+ persist.current.checksum = compute_checksum(&persist.current);
+ persist.defaults.checksum = compute_checksum(&persist.defaults);
+
+ if (!system_param_save_with_protect(PERSIST_SECTOR_ID, &persist, sizeof(PersistBlock))) {
+ error("[Persist] Store to flash failed!");
+ }
+ info("[Persist] All settings persisted.");
+}
+
+/**
+ * Restore to built-in defaults
+ */
+void ICACHE_FLASH_ATTR
+persist_restore_hard_default(void)
+{
+ info("[Persist] Restoring all settings to hard defaults...");
+
+ // Set live config to default values
+ restore_live_settings_to_hard_defaults();
+
+ // Store current -> default
+ memcpy(&persist.defaults, &persist.current, sizeof(AppConfigBundle));
+ persist_store();
+
+ info("[Persist] All settings restored to hard defaults.");
+
+ apply_live_settings(); // apply
+}
+
+/**
+ * Restore default settings & apply
+ */
+void ICACHE_FLASH_ATTR
+persist_restore_default(void)
+{
+ info("[Persist] Restoring live settings to stored defaults...");
+ memcpy(&persist.current, &persist.defaults, sizeof(AppConfigBundle));
+ apply_live_settings();
+ info("[Persist] Settings restored to stored defaults.");
+}
+
+/**
+ * Store current settings as defaults & write to flash
+ */
+void ICACHE_FLASH_ATTR
+persist_set_as_default(void)
+{
+ info("[Persist] Storing live settings as defaults..");
+
+ // current -> defaults
+ memcpy(&persist.defaults, &persist.current, sizeof(AppConfigBundle));
+ persist_store();
+
+ info("[Persist] Default settings updated.");
+}
diff --git a/user/persist.h b/user/persist.h
new file mode 100644
index 0000000..be334e4
--- /dev/null
+++ b/user/persist.h
@@ -0,0 +1,55 @@
+//
+// Created by MightyPork on 2017/07/09.
+//
+// There are 4 sets of settings.
+// - hard defaults - hardcoded in firmware, used for init defaults after flash or if stored data are corrupt
+// - defaults - persisted by privileged user
+// - current - persistent current config state, can be restored to defaults any time
+// - live - non-persistent settings valid only for the current runtime
+
+#ifndef ESP_VT100_FIRMWARE_PERSIST_H
+#define ESP_VT100_FIRMWARE_PERSIST_H
+
+#include "wifimgr.h"
+#include "screen.h"
+
+// Changing this could be used to force-erase the config area
+// after a firmware upgrade
+#define CHECKSUM_SALT 0x5F5F5F5F
+
+/** Struct for current or default settings */
+typedef struct {
+ WiFiConfigBundle wificonf;
+ TerminalConfigBundle termconf;
+
+ // --- Space for future settings ---
+ // Original size: 1024
+ //
+ // The size must be appropriately reduced each time something is added,
+ // and boolean flags defaulting to 0 should be used to detect unpopulated
+ // sections that must be restored to defaults on load.
+ //
+ // This ensures user settings are not lost each time they upgrade the firmware,
+ // which would lead to a checksum mismatch if the structure was changed and
+ // it grew to a different memory area.
+ uint8_t filler[1024];
+
+ uint32_t checksum; // computed before write and tested on load. If it doesn't match, values are reset to hard defaults.
+} AppConfigBundle;
+
+/** This is the entire data block stored in FLASH */
+typedef struct {
+ AppConfigBundle defaults; // defaults are stored here
+ AppConfigBundle current; // active settings adjusted by the user
+} PersistBlock;
+
+// Persist holds the data currently loaded from the flash
+extern PersistBlock persist;
+
+void persist_load(void);
+void persist_restore_hard_default(void);
+void persist_restore_default(void);
+void persist_set_as_default(void);
+void persist_store(void);
+
+#endif //ESP_VT100_FIRMWARE_PERSIST_H
diff --git a/user/routes.c b/user/routes.c
index 7c77313..ee2fcd7 100644
--- a/user/routes.c
+++ b/user/routes.c
@@ -2,15 +2,17 @@
#include
#include
#include
-#include
#include
#include "routes.h"
-
+#include "cgi_wifi.h"
#include "cgi_reset.h"
#include "cgi_ping.h"
#include "cgi_main.h"
#include "cgi_sockets.h"
+#include "cgi_network.h"
+#include "cgi_appcfg.h"
+#include "cgi_persist.h"
#define WIFI_PROTECT 0
#define WIFI_AUTH_NAME "wifi"
@@ -27,30 +29,38 @@ HttpdBuiltInUrl routes[] = {
// --- Web pages ---
ROUTE_TPL_FILE("/", tplScreen, "/term.tpl"),
- ROUTE_TPL_FILE("/about", tplAbout, "/about.tpl"),
- ROUTE_FILE("/help", "/help.tpl"),
+ ROUTE_TPL_FILE("/about/?", tplAbout, "/about.tpl"),
+ ROUTE_FILE("/help/?", "/help.tpl"),
// --- Sockets ---
ROUTE_WS(URL_WS_UPDATE, updateSockConnect),
// --- System control ---
- ROUTE_CGI("/system/reset", cgiResetDevice),
- ROUTE_CGI("/system/ping", cgiPing),
+ ROUTE_CGI("/system/reset/?", cgiResetDevice),
+ ROUTE_CGI("/system/ping/?", cgiPing),
- // --- WiFi config ---
+ // --- WiFi config --- (TODO make this conditional and configurable)
#if WIFI_PROTECT
ROUTE_AUTH("/wifi*", wifiPassFn),
#endif
- ROUTE_REDIRECT("/wifi/", "/wifi"),
- ROUTE_TPL_FILE("/wifi", tplWlan, "/wifi.tpl"),
+ ROUTE_REDIRECT("/cfg/?", "/cfg/wifi"),
+
+ ROUTE_TPL_FILE("/cfg/wifi/?", tplWlan, "/cfg_wifi.tpl"),
+ ROUTE_FILE("/cfg/wifi/connecting/?", "/cfg_wifi_conn.tpl"),
+ ROUTE_CGI("/cfg/wifi/scan", cgiWiFiScan),
+ ROUTE_CGI("/cfg/wifi/connstatus", cgiWiFiConnStatus),
+ ROUTE_CGI("/cfg/wifi/set", cgiWiFiSetParams),
+
+ ROUTE_TPL_FILE("/cfg/network/?", tplNetwork, "/cfg_network.tpl"),
+ ROUTE_CGI("/cfg/network/set", cgiNetworkSetParams),
+
+ ROUTE_TPL_FILE("/cfg/app/?", tplAppCfg, "/cfg_app.tpl"),
+ ROUTE_CGI("/cfg/app/set", cgiAppCfgSetParams),
- ROUTE_CGI("/wifi/scan", cgiWiFiScan),
- ROUTE_CGI("/wifi/connect", cgiWiFiConnect),
- ROUTE_CGI("/wifi/connstatus", cgiWiFiConnStatus),
- ROUTE_FILE("/wifi/connecting", "/wifi_conn.tpl"),
- ROUTE_CGI("/wifi/setmode", cgiWiFiSetMode),
- ROUTE_CGI("/wifi/setchannel", cgiWiFiSetChannel),
- ROUTE_CGI("/wifi/setname", cgiWiFiSetSSID),
+ ROUTE_FILE("/cfg/admin/?", "/cfg_admin.tpl"),
+ ROUTE_CGI("/cfg/admin/write_defaults", cgiPersistWriteDefaults),
+ ROUTE_CGI("/cfg/admin/restore_defaults", cgiPersistRestoreDefaults),
+ ROUTE_CGI("/cfg/admin/restore_hard", cgiPersistRestoreHard),
ROUTE_FILESYSTEM(),
ROUTE_END(),
diff --git a/user/screen.c b/user/screen.c
index 54fd004..ce9075d 100644
--- a/user/screen.c
+++ b/user/screen.c
@@ -1,9 +1,42 @@
#include
#include
#include "screen.h"
+#include "persist.h"
//region Data structures
+TerminalConfigBundle * const termconf = &persist.current.termconf;
+TerminalConfigBundle termconf_scratch;
+
+/**
+ * Restore hard defaults
+ */
+void terminal_restore_defaults(void)
+{
+ termconf->default_bg = 0;
+ termconf->default_fg = 7;
+ termconf->width = 26;
+ termconf->height = 10;
+ sprintf(termconf->title, "ESPTerm");
+ sprintf(termconf->btn1, "1");
+ sprintf(termconf->btn2, "2");
+ sprintf(termconf->btn3, "3");
+ sprintf(termconf->btn4, "4");
+ sprintf(termconf->btn5, "5");
+}
+
+/**
+ * Apply settings after eg. restore from defaults
+ */
+void terminal_apply_settings(void)
+{
+ memcpy(&termconf_scratch, termconf, sizeof(TerminalConfigBundle));
+ screen_init();
+}
+
+#define W termconf_scratch.width
+#define H termconf_scratch.height
+
/**
* Highest permissible value of the color attribute
*/
@@ -50,16 +83,6 @@ static struct {
Color bg;
} cursor_sav;
-/**
- * Active screen width
- */
-static int W = SCREEN_DEF_W;
-
-/**
- * Active screen height
- */
-static int H = SCREEN_DEF_H;
-
// XXX volatile is probably not needed
static volatile int notifyLock = 0;
@@ -99,8 +122,8 @@ cursor_reset(void)
{
cursor.x = 0;
cursor.y = 0;
- cursor.fg = SCREEN_DEF_FG;
- cursor.bg = SCREEN_DEF_BG;
+ cursor.fg = termconf_scratch.default_fg;
+ cursor.bg = termconf_scratch.default_bg;
cursor.visible = 1;
cursor.inverse = 0;
cursor.autowrap = 1;
@@ -363,8 +386,8 @@ screen_cursor_save(bool withAttrs)
cursor_sav.bg = cursor.bg;
cursor_sav.inverse = cursor.inverse;
} else {
- cursor_sav.fg = SCREEN_DEF_FG;
- cursor_sav.bg = SCREEN_DEF_BG;
+ cursor_sav.fg = termconf_scratch.default_fg;
+ cursor_sav.bg = termconf_scratch.default_bg;
cursor_sav.inverse = 0;
}
}
diff --git a/user/screen.h b/user/screen.h
index 701e759..5ab2bc6 100644
--- a/user/screen.h
+++ b/user/screen.h
@@ -34,6 +34,31 @@
*
*/
+typedef struct {
+ u32 width;
+ u32 height;
+ u8 default_bg;
+ u8 default_fg;
+ char title[64];
+ char btn1[10];
+ char btn2[10];
+ char btn3[10];
+ char btn4[10];
+ char btn5[10];
+} TerminalConfigBundle;
+
+// Live config
+extern TerminalConfigBundle * const termconf;
+
+/**
+ * Transient live config with no persist, can be modified via esc sequences.
+ * terminal_apply_settings() copies termconf to this struct, erasing old scratch changes
+ */
+extern TerminalConfigBundle termconf_scratch;
+
+void terminal_restore_defaults(void);
+void terminal_apply_settings(void);
+
/**
* Maximum screen size (determines size of the static data array)
*
@@ -42,20 +67,13 @@
*/
#define MAX_SCREEN_SIZE (80*25)
-#define SCREEN_DEF_W 26 //!< Default screen width
-#define SCREEN_DEF_H 10 //!< Default screen height
-
-#define SCREEN_DEF_BG 0 //!< Default screen background
-#define SCREEN_DEF_FG 7 //!< Default screen foreground
-
typedef enum {
CLEAR_TO_CURSOR=0, CLEAR_FROM_CURSOR=1, CLEAR_ALL=2
} ClearMode;
typedef uint8_t Color;
-httpd_cgi_state ICACHE_FLASH_ATTR
-screenSerializeToBuffer(char *buffer, size_t buf_len, void **data);
+httpd_cgi_state screenSerializeToBuffer(char *buffer, size_t buf_len, void **data);
/** Init the screen */
void screen_init(void);
diff --git a/user/user_main.c b/user/user_main.c
index 95364d4..8730ff9 100644
--- a/user/user_main.c
+++ b/user/user_main.c
@@ -26,6 +26,8 @@
#include "user_main.h"
#include "uart_driver.h"
#include "ansi_parser_callbacks.h"
+#include "wifimgr.h"
+#include "persist.h"
#ifdef ESPFS_POS
CgiUploadFlashDef uploadParams={
@@ -47,8 +49,6 @@ CgiUploadFlashDef uploadParams={
#define INCLUDE_FLASH_FNS
#endif
-static ETSTimer prHeapTimer;
-
/** Periodically show heap usage */
static void ICACHE_FLASH_ATTR prHeapTimerCb(void *arg)
{
@@ -77,11 +77,22 @@ static void ICACHE_FLASH_ATTR prHeapTimerCb(void *arg)
cnt++;
}
+// Deferred init
+static void user_start(void *unused);
+
//Main routine. Initialize stdout, the I/O, filesystem and the webserver and we're done.
void ICACHE_FLASH_ATTR user_init(void)
{
+ static ETSTimer userStartTimer;
+ static ETSTimer prHeapTimer;
+
serialInit();
+ // Prevent WiFi starting and connecting by default
+ // let wifi manager handle it
+ wifi_station_set_auto_connect(false);
+ wifi_set_opmode(NULL_MODE); // save to flash if changed - this might avoid the current spike on startup?
+
printf("\r\n");
banner("====== ESP8266 Remote Terminal ======");
banner_info("Firmware (c) Ondrej Hruska, 2017");
@@ -94,15 +105,6 @@ void ICACHE_FLASH_ATTR user_init(void)
ioInit();
- // Change AP name if AI-THINKER found (means un-initialized device)
- struct softap_config apconf;
- wifi_softap_get_config(&apconf);
- if (strstarts((char*)apconf.ssid, "AI-THINKER")) {
- warn("Un-initialized device, performing factory reset.");
- apars_handle_OSC_FactoryReset();
- return;
- }
-
// 0x40200000 is the base address for spi flash memory mapping, ESPFS_POS is the position
// where image is written in flash that is defined in Makefile.
#ifdef ESPFS_POS
@@ -111,17 +113,35 @@ void ICACHE_FLASH_ATTR user_init(void)
espFsInit((void *) (webpages_espfs_start));
#endif
- // Captive portal
- captdnsInit();
-
- // Server
- httpdInit(routes, 80);
-
// Heap use timer & blink
os_timer_disarm(&prHeapTimer);
os_timer_setfn(&prHeapTimer, prHeapTimerCb, NULL);
os_timer_arm(&prHeapTimer, 1000, 1);
+ // do later (some functions do not work if called from user_init)
+ os_timer_disarm(&userStartTimer);
+ os_timer_setfn(&userStartTimer, user_start, NULL);
+ os_timer_arm(&userStartTimer, 10, 0);
+}
+
+static void user_start(void *unused)
+{
+ // Change AP name if AI-THINKER found (means un-initialized device)
+// struct softap_config apconf;
+// wifi_softap_get_config(&apconf);
+// if (strstarts((char *) apconf.ssid, "AI-THINKER")) {
+// warn("Un-initialized device, performing factory reset.");
+// apars_handle_OSC_FactoryReset();
+// return;
+// }
+
+ // Load and apply stored settings, or defaults if stored settings are invalid
+ persist_load();
+ // Captive portal (DNS redirector)
+ captdnsInit();
+ // Server
+ httpdInit(routes, 80);
+
// The terminal screen
screen_init();
diff --git a/user/wifimgr.c b/user/wifimgr.c
new file mode 100644
index 0000000..5a4b132
--- /dev/null
+++ b/user/wifimgr.c
@@ -0,0 +1,197 @@
+//
+// Created by MightyPork on 2017/07/08.
+//
+
+#include "wifimgr.h"
+#include "persist.h"
+
+WiFiConfigBundle * const wificonf = &persist.current.wificonf;
+WiFiConfChangeFlags wifi_change_flags;
+
+/**
+ * Restore defaults in the WiFi config block.
+ * This is to be called if the WiFi config is corrupted on startup,
+ * before applying the config.
+ */
+void ICACHE_FLASH_ATTR
+wifimgr_restore_defaults(void)
+{
+ u8 mac[6];
+ wifi_get_macaddr(SOFTAP_IF, mac);
+
+ wificonf->opmode = SOFTAP_MODE;
+ wificonf->tpw = 20;
+ wificonf->ap_channel = 1;
+ sprintf((char *) wificonf->ap_ssid, "TERM-%02X%02X%02X", mac[3], mac[4], mac[5]);
+ wificonf->ap_password[0] = 0; // PSK2 always if password is not null.
+ wificonf->ap_hidden = false;
+
+ IP4_ADDR(&wificonf->ap_addr.ip, 192, 168, 4, 1);
+ IP4_ADDR(&wificonf->ap_addr.netmask, 255, 255, 255, 0);
+ wificonf->ap_addr.gw.addr = wificonf->ap_addr.gw.addr;
+
+ IP4_ADDR(&wificonf->ap_dhcp_range.start_ip, 192, 168, 4, 100);
+ IP4_ADDR(&wificonf->ap_dhcp_range.end_ip, 192, 168, 4, 200);
+ wificonf->ap_dhcp_range.enable = 1; // this will never get changed, idk why it's even there
+ wificonf->ap_dhcp_time = 120;
+
+ // --- Client config ---
+ wificonf->sta_ssid[0] = 0;
+ wificonf->sta_password[0] = 0;
+ wificonf->sta_dhcp_enable = true;
+
+ IP4_ADDR(&wificonf->sta_addr.ip, 192, 168, 0, (mac[5] == 1 ? 2 : mac[5])); // avoid being the same as "default gw"
+ IP4_ADDR(&wificonf->sta_addr.netmask, 255, 255, 255, 0);
+ IP4_ADDR(&wificonf->sta_addr.gw, 192, 168, 0, 1);
+
+ // DEBUG ONLY - TODO remove for release
+ wificonf->opmode = STATION_MODE;
+ sprintf((char*)wificonf->sta_ssid, "Chlivek");
+ sprintf((char*)wificonf->sta_password, "prase chrochta");
+}
+
+static void ICACHE_FLASH_ATTR
+configure_station(void)
+{
+ info("[WiFi] Configuring Station mode...");
+ struct station_config conf;
+ strcpy((char *) conf.ssid, (char *) wificonf->sta_ssid);
+ strcpy((char *) conf.password, (char *) wificonf->sta_password);
+ dbg("[WiFi] Connecting to \"%s\", password \"%s\"", conf.ssid, conf.password);
+ conf.bssid_set = 0;
+ conf.bssid[0] = 0;
+ wifi_station_disconnect();
+ wifi_station_set_config_current(&conf);
+
+ if (wificonf->sta_dhcp_enable) {
+ dbg("[WiFi] Starting DHCP...");
+ if (!wifi_station_dhcpc_start()) {
+ error("[WiFi] DHCP failed to start!");
+ return;
+ }
+ }
+ else {
+ info("[WiFi] Setting up static IP...");
+ dbg("[WiFi] Client.ip = "IPSTR, GOOD_IP2STR(wificonf->sta_addr.ip.addr));
+ dbg("[WiFi] Client.mask = "IPSTR, GOOD_IP2STR(wificonf->sta_addr.netmask.addr));
+ dbg("[WiFi] Client.gw = "IPSTR, GOOD_IP2STR(wificonf->sta_addr.gw.addr));
+
+ wifi_station_dhcpc_stop();
+ // Load static IP config
+ if (!wifi_set_ip_info(STATION_IF, &wificonf->sta_addr)) {
+ error("[WiFi] Error setting static IP!");
+ return;
+ }
+ }
+
+ info("[WiFi] Trying to connect to AP...");
+ wifi_station_connect();
+}
+
+static void ICACHE_FLASH_ATTR
+configure_ap(void)
+{
+ bool suc;
+
+ info("[WiFi] Configuring SoftAP mode...");
+ // AP is enabled
+ struct softap_config conf;
+ conf.channel = wificonf->ap_channel;
+ strcpy((char *) conf.ssid, (char *) wificonf->ap_ssid);
+ strcpy((char *) conf.password, (char *) wificonf->ap_password);
+ conf.authmode = (wificonf->ap_password[0] == 0 ? AUTH_OPEN : AUTH_WPA2_PSK);
+ conf.ssid_len = (uint8_t) strlen((char *) conf.ssid);
+ conf.ssid_hidden = wificonf->ap_hidden;
+ conf.max_connection = 4; // default 4 (max possible)
+ conf.beacon_interval = 100; // default 100 ms
+
+ // Set config
+ //ETS_UART_INTR_DISABLE();
+ suc = wifi_softap_set_config_current(&conf);
+ //ETS_UART_INTR_ENABLE();
+ if (!suc) {
+ error("[WiFi] AP config set fail!");
+ return;
+ }
+
+ // Set IP
+ info("[WiFi] Configuring SoftAP local IP...");
+ dbg("[WiFi] SoftAP.ip = "IPSTR, GOOD_IP2STR(wificonf->ap_addr.ip.addr));
+ dbg("[WiFi] SoftAP.mask = "IPSTR, GOOD_IP2STR(wificonf->ap_addr.netmask.addr));
+ dbg("[WiFi] SoftAP.gw = "IPSTR, GOOD_IP2STR(wificonf->ap_addr.gw.addr));
+
+ wifi_softap_dhcps_stop();
+
+ // Configure DHCP
+ if (!wifi_set_ip_info(SOFTAP_IF, &wificonf->ap_addr)) {
+ error("[WiFi] IP set fail!");
+ return;
+ }
+
+ info("[WiFi] Configuring SoftAP DHCP server...");
+ dbg("[WiFi] DHCP.start = "IPSTR, GOOD_IP2STR(wificonf->ap_dhcp_range.start_ip.addr));
+ dbg("[WiFi] DHCP.end = "IPSTR, GOOD_IP2STR(wificonf->ap_dhcp_range.end_ip.addr));
+ dbg("[WiFi] DHCP.lease = %d minutes", wificonf->ap_dhcp_time);
+
+ if (!wifi_softap_set_dhcps_lease(&wificonf->ap_dhcp_range)) {
+ error("[WiFi] DHCP address range set fail!");
+ return;
+ }
+
+ if (!wifi_softap_set_dhcps_lease_time(wificonf->ap_dhcp_time)) {
+ error("[WiFi] DHCP lease time set fail!");
+ return;
+ }
+
+ // some weird magic shit about router
+ uint8 mode = 1;
+ wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode);
+
+ if (!wifi_softap_dhcps_start()) {
+ error("[WiFi] Failed to start DHCP server!");
+ return;
+ }
+}
+
+/**
+ * Register the WiFi event listener, cycle WiFi, apply settings
+ */
+void ICACHE_FLASH_ATTR
+wifimgr_apply_settings(void)
+{
+ info("[WiFi] Initializing...");
+
+ // Force wifi cycle
+ // Disconnect - may not be needed?
+ WIFI_MODE opmode = wifi_get_opmode();
+
+ bool is_sta = wificonf->opmode & STATION_MODE;
+ bool is_ap = wificonf->opmode & SOFTAP_MODE;
+
+ if ((wificonf->opmode & STATION_MODE) && !(opmode & STATION_MODE)) {
+ wifi_change_flags.sta = true;
+ }
+
+ if ((wificonf->opmode & SOFTAP_MODE) && !(opmode & SOFTAP_MODE)) {
+ wifi_change_flags.ap = true;
+ }
+
+ if (opmode != wificonf->opmode) {
+ wifi_set_opmode_current(wificonf->opmode);
+ }
+
+ // Configure the client
+ if (is_sta && wifi_change_flags.sta) {
+ configure_station();
+ }
+
+ // Configure the AP
+ if (is_ap && wifi_change_flags.ap) {
+ configure_ap();
+ }
+
+ wifi_change_flags.ap = false;
+ wifi_change_flags.sta = false;
+
+ info("[WiFi] WiFi settings applied.");
+}
diff --git a/user/wifimgr.h b/user/wifimgr.h
new file mode 100644
index 0000000..79a4246
--- /dev/null
+++ b/user/wifimgr.h
@@ -0,0 +1,57 @@
+//
+// Created by MightyPork on 2017/07/08.
+// This module handles all WiFi configuration and is interfaced
+// by the cgi_wifi functions.
+//
+
+#ifndef ESP_VT100_FIRMWARE_WIFI_MANAGER_H
+#define ESP_VT100_FIRMWARE_WIFI_MANAGER_H
+
+#include
+#include "cgi_wifi.h"
+
+#define SSID_LEN 32
+#define PASSWORD_LEN 64
+
+/**
+ * A structure holding all configured WiFi parameters
+ * and the active state.
+ *
+ * This block can be used eg. for WiFi config backup.
+ */
+typedef struct {
+ WIFI_MODE opmode : 8;
+ u8 tpw;
+
+ // --- AP config ---
+ u8 ap_channel;
+ u8 ap_ssid[SSID_LEN];
+ u8 ap_password[PASSWORD_LEN];
+ bool ap_hidden;
+ u16 ap_dhcp_time; // in minutes
+ struct dhcps_lease ap_dhcp_range;
+
+ struct ip_info ap_addr;
+
+ // --- Client config ---
+ u8 sta_ssid[SSID_LEN];
+ u8 sta_password[PASSWORD_LEN];
+ bool sta_dhcp_enable;
+
+ struct ip_info sta_addr;
+} WiFiConfigBundle;
+
+typedef struct {
+ bool sta;
+ bool ap;
+} WiFiConfChangeFlags;
+
+extern WiFiConfChangeFlags wifi_change_flags;
+
+extern WiFiConfigBundle * const wificonf;
+
+void wifimgr_restore_defaults(void);
+
+void wifimgr_apply_settings(void);
+
+#endif //ESP_VT100_FIRMWARE_WIFI_MANAGER_H