summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rwxr-xr-xbuild_static.php96
-rw-r--r--deploy/build_common.sh74
-rwxr-xr-xdeploy/build_css.sh62
-rwxr-xr-xdeploy/build_js.sh31
-rwxr-xr-xdeploy/deploy.sh (renamed from deploy.sh)10
-rwxr-xr-xdeploy/gen_css_diff.js (renamed from dark-theme-diff.js)0
-rwxr-xr-xdeploy/gen_static_config.php57
-rw-r--r--engine/RequestDispatcher.php4
-rw-r--r--engine/logging.php3
-rw-r--r--handler/admin/AdminRequestHandler.php4
-rw-r--r--htdocs/css/admin.css1
-rw-r--r--htdocs/css/admin_dark.css0
-rw-r--r--htdocs/css/common.css1
-rw-r--r--htdocs/css/common_dark.css1
-rw-r--r--htdocs/js.php31
-rw-r--r--htdocs/js/admin/00-common.js1
-rw-r--r--htdocs/js/admin/10-draft.js29
-rw-r--r--htdocs/js/admin/11-write-form.js (renamed from htdocs/js/admin.js)53
-rw-r--r--htdocs/js/admin/12-upload-list.js19
-rw-r--r--htdocs/js/common.js679
-rw-r--r--htdocs/js/common/00-polyfills.js47
-rw-r--r--htdocs/js/common/02-ajax.js118
-rw-r--r--htdocs/js/common/03-dom.js117
-rw-r--r--htdocs/js/common/04-util.js89
-rw-r--r--htdocs/js/common/10-lang.js5
-rw-r--r--htdocs/js/common/20-dynlogo.js60
-rw-r--r--htdocs/js/common/30-static-manager.js30
-rw-r--r--htdocs/js/common/35-theme-switcher.js195
-rw-r--r--htdocs/js/common/90-retina.js9
-rw-r--r--lib/cli.php6
-rw-r--r--skin/base.skin.php63
32 files changed, 1046 insertions, 852 deletions
diff --git a/.gitignore b/.gitignore
index 5ba863a..8ae0b83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
/debug.log
test.php
/.git
-/htdocs/css
/node_modules/
/vendor/
.DS_Store
@@ -10,3 +9,5 @@ test.php
config-static.php
config-local.php
/.idea
+/htdocs/dist-css
+/htdocs/dist-js
diff --git a/build_static.php b/build_static.php
deleted file mode 100755
index 63c6e0e..0000000
--- a/build_static.php
+++ /dev/null
@@ -1,96 +0,0 @@
-#!/usr/bin/env php8.1
-<?php
-
-function gethash(string $path): string {
- return substr(sha1(file_get_contents($path)), 0, 8);
-}
-
-function sassc(string $src_file, string $dst_file): int {
- $cmd = 'sassc -t compressed '.escapeshellarg($src_file).' '.escapeshellarg($dst_file);
- exec($cmd, $output, $code);
- return $code;
-}
-
-function clean_css(string $file) {
- $output = $file.'.out';
- if (file_exists($output))
- unlink($output);
-
- $cmd = ROOT.'/node_modules/clean-css-cli/bin/cleancss -O2 "all:on;mergeSemantically:on;restructureRules:on" '.escapeshellarg($file).' > '.escapeshellarg($output);
- system($cmd);
-
- if (file_exists($output)) {
- unlink($file);
- rename($output, $file);
- } else {
- fwrite(STDERR, "error: could not cleancss $file\n");
- }
-}
-
-function dark_diff(string $light_file, string $dark_file): void {
- $temp_output = $dark_file.'.diff';
- $cmd = ROOT.'/dark-theme-diff.js '.escapeshellarg($light_file).' '.$dark_file.' > '.$temp_output;
- exec($cmd, $output, $code);
- if ($code != 0) {
- fwrite(STDERR, "dark_diff failed with code $code\n");
- return;
- }
-
- unlink($dark_file);
- rename($temp_output, $dark_file);
-}
-
-require __DIR__.'/init.php';
-
-function build_static(): void {
- $css_dir = ROOT.'/htdocs/css';
- $hashes = [];
-
- if (!file_exists($css_dir))
- mkdir($css_dir);
-
- // 1. scss -> css
- $themes = ['light', 'dark'];
- $entries = ['common', 'admin'];
- foreach ($themes as $theme) {
- foreach ($entries as $entry) {
- $input = ROOT.'/htdocs/scss/entries/'.$entry.'/'.$theme.'.scss';
- $output = $css_dir.'/'.$entry.($theme == 'dark' ? '_dark' : '').'.css';
- if (sassc($input, $output) != 0)
- fwrite(STDERR, "error: could not compile entries/$entry/$theme.scss\n");
- }
- }
-
- // 2. generate dark theme diff
- foreach ($entries as $entry) {
- $light_file = $css_dir.'/'.$entry.'.css';
- $dark_file = str_replace('.css', '_dark.css', $light_file);
- dark_diff($light_file, $dark_file);
-
- // 2.1. apply cleancss (must be done _after_ css-patch)
- clean_css($light_file);
- clean_css($dark_file);
- }
-
- // 3. calculate hashes
- foreach (['css', 'js'] as $type) {
- $reldir = ROOT.'/htdocs/';
- $entries = glob_recursive($reldir.$type.'/*.'.$type);
- if (empty($entries)) {
- continue;
- }
- foreach ($entries as $file) {
- $name = preg_replace('/^'.preg_quote($reldir, '/').'/', '', $file);
- $hashes[$name] = gethash($file);
- }
- }
- logInfo($hashes);
-
- // 4. write config-static.php
- $scfg = "<?php\n\n";
- $scfg .= "return ".var_export($hashes, true).";\n";
-
- file_put_contents(ROOT.'/config-static.php', $scfg);
-}
-
-build_static(); \ No newline at end of file
diff --git a/deploy/build_common.sh b/deploy/build_common.sh
new file mode 100644
index 0000000..acac5da
--- /dev/null
+++ b/deploy/build_common.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+
+set -e
+
+INDIR=
+OUTDIR=
+
+error() {
+ >&2 echo "error: $@"
+}
+
+warning() {
+ >&2 echo "warning: $@"
+}
+
+die() {
+ error "$@"
+ exit 1
+}
+
+usage() {
+ local code="$1"
+ cat <<EOF
+usage: $PROGNAME [OPTIONS]
+
+Options:
+ -o output directory
+ -i input directory
+ -h show this help
+EOF
+ exit $code
+}
+
+input_args() {
+ [ -z "$1" ] && usage
+
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ -o)
+ OUTDIR="$2"
+ shift
+ ;;
+ -i)
+ INDIR="$2"
+ shift
+ ;;
+ -h)
+ usage
+ ;;
+ *)
+ die "unexpected argument: $1"
+ ;;
+ esac
+ shift
+ done
+}
+
+check_args() {
+ [ -z "$OUTDIR" ] && {
+ error "output directory not specified"
+ usage 1
+ }
+ [ -z "$INDIR" ] && {
+ error "input directory not specified"
+ usage 1
+ }
+
+ if [ ! -d "$OUTDIR" ]; then
+ mkdir "$OUTDIR"
+ else
+ warning "$OUTDIR already exists, erasing it"
+ rm "$OUTDIR"/*
+ fi
+} \ No newline at end of file
diff --git a/deploy/build_css.sh b/deploy/build_css.sh
new file mode 100755
index 0000000..2129ea2
--- /dev/null
+++ b/deploy/build_css.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+PROGNAME="$0"
+DIR="$( cd "$( dirname "$(readlink -f "${BASH_SOURCE[0]}")" )" && pwd )"
+ROOT="$(realpath "$DIR/../")"
+CLEANCSS="$ROOT"/node_modules/clean-css-cli/bin/cleancss
+
+. $DIR/build_common.sh
+
+build_scss() {
+ local entry_name="$1"
+ local theme="$2"
+
+ local input="$INDIR/entries/$entry_name/$theme.scss"
+ local output="$OUTDIR/$entry_name"
+ [ "$theme" = "dark" ] && output="${output}_dark"
+ output="${output}.css"
+
+ sassc -t compressed "$input" "$output"
+}
+
+cleancss() {
+ local entry_name="$1"
+ local theme="$2"
+
+ local file="$OUTDIR/$entry_name"
+ [ "$theme" = "dark" ] && file="${file}_dark"
+ file="${file}.css"
+
+ $CLEANCSS -O2 "all:on;mergeSemantically:on;restructureRules:on" "$file" > "$file.tmp"
+ rm "$file"
+ mv "$file.tmp" "$file"
+}
+
+create_dark_patch() {
+ local entry_name="$1"
+ local light_file="$OUTDIR/$entry_name.css"
+ local dark_file="$OUTDIR/${entry_name}_dark.css"
+
+ "$DIR"/gen_css_diff.js "$light_file" "$dark_file" > "$dark_file.diff"
+ rm "$dark_file"
+ mv "$dark_file.diff" "$dark_file"
+}
+
+THEMES="light dark"
+TARGETS="common admin"
+
+input_args "$@"
+check_args
+
+[ -x "$CLEANCSS" ] || die "cleancss is not found"
+
+for theme in $THEMES; do
+ for target in $TARGETS; do
+ build_scss "$target" "$theme"
+ done
+done
+
+for target in $TARGETS; do
+ create_dark_patch "$target"
+ for theme in $THEMES; do cleancss "$target" "$theme"; done
+done \ No newline at end of file
diff --git a/deploy/build_js.sh b/deploy/build_js.sh
new file mode 100755
index 0000000..b1019f7
--- /dev/null
+++ b/deploy/build_js.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+PROGNAME="$0"
+DIR="$( cd "$( dirname "$(readlink -f "${BASH_SOURCE[0]}")" )" && pwd )"
+
+. $DIR/build_common.sh
+
+# suckless version of webpack
+# watch and learn, bitches!
+build_chunk() {
+ local name="$1"
+ local output="$OUTDIR/$name.js"
+ local not_first=0
+ for file in "$INDIR/$name"/*.js; do
+ # insert newline before out comment
+ [ "$not_first" = "1" ] && echo "" >> "$output"
+ echo "/* $(basename "$file") */" >> "$output"
+
+ cat "$file" >> "$output"
+ not_first=1
+ done
+}
+
+TARGETS="common admin"
+
+input_args "$@"
+check_args
+
+for f in $TARGETS; do
+ build_chunk "$f"
+done \ No newline at end of file
diff --git a/deploy.sh b/deploy/deploy.sh
index 9e418e4..7ef28a3 100755
--- a/deploy.sh
+++ b/deploy/deploy.sh
@@ -2,9 +2,9 @@
set -e
-DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+DIR="$( cd "$( dirname "$(readlink -f "${BASH_SOURCE[0]}")" )" && pwd )"
-DEV_DIR="${DIR}"
+DEV_DIR="$(realpath "$DIR/../")"
STAGING_DIR="$HOME/staging"
PROD_DIR="$HOME/prod"
PHP=/usr/bin/php8.1
@@ -33,7 +33,9 @@ fi
cp "$DEV_DIR/config-local.php" .
sed -i '/is_dev/d' ./config-local.php
-$PHP build_static.php
+"$DIR"/build_js.sh -i "$DEV_DIR/htdocs/js" -o "$STAGING_DIR/htdocs/dist-js" || die "build_js failed"
+"$DIR"/build_css.sh -i "$DEV_DIR/htdocs/scss" -o "$STAGING_DIR/htdocs/dist-css" || die "build_css failed"
+$PHP "$DIR"/gen_static_config.php > "$STAGING_DIR/config-static.php" || die "gen_static_config failed"
popd
@@ -43,6 +45,8 @@ rsync -a --delete --delete-excluded --info=progress2 "$STAGING_DIR/" "$PROD_DIR/
--exclude debug.log \
--exclude='/composer.*' \
--exclude='/htdocs/scss' \
+ --exclude='/htdocs/js' \
--exclude='/htdocs/sass.php' \
+ --exclude='/htdocs/js.php' \
--exclude='*.sh' \
--exclude='*.sql'
diff --git a/dark-theme-diff.js b/deploy/gen_css_diff.js
index 5ca1945..5ca1945 100755
--- a/dark-theme-diff.js
+++ b/deploy/gen_css_diff.js
diff --git a/deploy/gen_static_config.php b/deploy/gen_static_config.php
new file mode 100755
index 0000000..09058ad
--- /dev/null
+++ b/deploy/gen_static_config.php
@@ -0,0 +1,57 @@
+#!/usr/bin/env php8.1
+<?php
+
+require __DIR__.'/../init.php';
+
+if ($argc <= 1) {
+ usage();
+ exit(1);
+}
+
+$input_dir = null;
+
+array_shift($argv);
+while (count($argv) > 0) {
+ switch ($argv[0]) {
+ case '-i':
+ array_shift($argv);
+ $input_dir = array_shift($argv);
+ break;
+
+ default:
+ cli::die('unsupported argument: '.$argv[0]);
+ }
+}
+
+if (is_null($input_dir))
+ cli::die("input directory has not been specified");
+
+$hashes = [];
+foreach (['css', 'js'] as $type) {
+ $entries = glob_recursive($input_dir.'/dist-'.$type.'/*.'.$type);
+ if (empty($entries)) {
+ cli::error("warning: no files found in $input_dir/dist-$type");
+ continue;
+ }
+
+ foreach ($entries as $file)
+ $hashes[$type.'/'.basename($file)] = get_hash($file);
+}
+
+echo "<?php\n\n";
+echo "return ".var_export($hashes, true).";\n";
+
+function usage(): void {
+ global $argv;
+ echo <<<EOF
+usage: {$argv[0]} [OPTIONS]
+
+Options:
+ -i input htdocs directory
+
+EOF;
+}
+
+function get_hash(string $path): string {
+ return substr(sha1(file_get_contents($path)), 0, 8);
+} \ No newline at end of file
diff --git a/engine/RequestDispatcher.php b/engine/RequestDispatcher.php
index 3c3f684..ec31516 100644
--- a/engine/RequestDispatcher.php
+++ b/engine/RequestDispatcher.php
@@ -39,8 +39,8 @@ class RequestDispatcher {
}
$skin = new Skin();
- $skin->static[] = '/css/common.css';
- $skin->static[] = '/js/common.js';
+ $skin->static[] = 'css/common.css';
+ $skin->static[] = 'js/common.js';
$lang = LangData::getInstance();
$skin->addLangKeys($lang->search('/^theme_/'));
diff --git a/engine/logging.php b/engine/logging.php
index 1bc946b..24803cf 100644
--- a/engine/logging.php
+++ b/engine/logging.php
@@ -72,6 +72,9 @@ class logging {
}
public static function logCustom(LogLevel $level, ...$args): void {
+ global $config;
+ if (!$config['is_dev'] && $level == LogLevel::DEBUG)
+ return;
self::write($level, self::strVars($args));
}
diff --git a/handler/admin/AdminRequestHandler.php b/handler/admin/AdminRequestHandler.php
index 04b7cde..5a6bd12 100644
--- a/handler/admin/AdminRequestHandler.php
+++ b/handler/admin/AdminRequestHandler.php
@@ -8,8 +8,8 @@ use Response;
class AdminRequestHandler extends \RequestHandler {
public function beforeDispatch(): ?Response {
- $this->skin->static[] = '/css/admin.css';
- $this->skin->static[] = '/js/admin.js';
+ $this->skin->static[] = 'css/admin.css';
+ $this->skin->static[] = 'js/admin.js';
if (!($this instanceof Login) && !admin::isAdmin())
throw new \ForbiddenException('looks like you are not admin');
diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css
new file mode 100644
index 0000000..286fb72
--- /dev/null
+++ b/htdocs/css/admin.css
@@ -0,0 +1 @@
+.admin-page{line-height:155%} \ No newline at end of file
diff --git a/htdocs/css/admin_dark.css b/htdocs/css/admin_dark.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/htdocs/css/admin_dark.css
diff --git a/htdocs/css/common.css b/htdocs/css/common.css
new file mode 100644
index 0000000..4a0a74b
--- /dev/null
+++ b/htdocs/css/common.css
@@ -0,0 +1 @@
+body,input[type=password],input[type=text],textarea{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;font-size:16px}.pt h3,body{font-size:16px}.md-file-attach-icon,.pt a,.pt h3{top:1px;position:relative}.empty,.md-image.align-center{text-align:center}.contact-img,.head-logo,.md-file-attach-icon,.md-image-wrap,.pt h3{display:inline-block}.head-logo,a.head-item>span>span>span.moon-icon{left:0;position:absolute}.blog-post-text table.table-100,.blog-write-options-table,.blog-write-table{table-layout:fixed;border-collapse:collapse}.blog-post-text table.table-100 td,.blog-write-table>tbody>tr>td{vertical-align:top;text-align:left}.blog-list-table,.blog-write-options-table,.blog-write-table,table.contacts{border-collapse:collapse}.clearfix:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}body,html{padding:0;margin:0;border:0;background-color:#fff;color:#222;height:100%;min-height:100%}.base-width{max-width:900px;margin:0 auto;position:relative}.md-image,.md-image-wrap,.md-image-wrap>a,.md-images,.md-images>span,.md-video video{max-width:100%}body.full-width .base-width{max-width:100%;margin-left:auto;margin-right:auto}input[type=password],input[type=text],textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;border:1px solid #e0e0e0;border-radius:3px;background-color:#f7f7f7;color:#222;padding:6px;outline:0;-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px}a,table.contacts div.note>a:hover{color:#116fd4;text-decoration:none}input[type=password]:focus,input[type=text]:focus,textarea:focus{border-color:#e0e0e0}textarea{resize:vertical}a{outline:0}a:hover{text-decoration:underline}p,p code{line-height:150%}.unicode{font-family:sans-serif}.blog-post-text code,.blog-post-text pre,.blog-write-form textarea.form-field-input,.ff_ms,.head-logo,table.contacts td.value span{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}.blog-upload-item-actions,.fl_r{float:right}.fl_l{float:left}.pos_rel{position:relative}.pos_abs{position:absolute}.pos_fxd{position:fixed}.head,.page-content{padding:0 25px}.page-content-inner{padding:18px 0}table.contacts{border:0;margin:8px auto 0}table.contacts td{white-space:nowrap;padding-bottom:15px;vertical-align:top}table.contacts td.label{text-align:right;width:30%;color:#777}table.contacts td.value{text-align:left;padding-left:8px}table.contacts td.value span{background:#f1f1f1;padding:3px 7px 4px;border-radius:3px;color:#222;font-size:15px}table.contacts td b{font-weight:600}table.contacts td pre{padding:0;margin:0;font-size:12px}table.contacts div.note{font-size:13px;padding-top:2px;color:#777}table.contacts div.note>a{color:#777;border-bottom:1px solid #e0e0e0}.empty,.head-logo>a:hover .head-logo-path:not(.alwayshover),.pt{color:#999}table.contacts div.note>a:hover{border-bottom-color:#116fd4}.pt{margin:5px 0 20px;padding-bottom:7px;border-bottom:2px solid rgba(255,255,255,.12)}.pt h3{margin:0;font-weight:700;color:#222}.blog-post-date>a,.page-edit-links>a,.pt a:not(:first-child),.pt h3:not(:first-child){margin-left:5px}.pt a{margin-right:5px}.blog-post-text h1:first-child,.blog-post-text h2:first-child,.pt_r{margin-top:5px}.empty{padding:40px 20px;-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-color:#f7f7f7}.md-file-attach,.md-image,.md-images{padding:3px 0}.blog-post-text pre,.head-logo-path-mapped{-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px}.contact-img{width:77px;height:12px;background:url(/img/contact.gif?1) 0 0/77px 12px no-repeat}@media (-o-min-device-pixel-ratio:3/2),(-webkit-min-device-pixel-ratio:1.5),(min--moz-device-pixel-ratio:1.5),(min-resolution:1.5dppx){.contact-img{background-image:url(/img/contact@2x.gif?1)}}.md-file-attach-icon{width:14px;height:14px;background:url(/img/attachment.svg) center center/14px 14px no-repeat;margin-right:5px}.md-file-attach-size{color:#888;margin-left:2px}.md-file-attach-note{color:#222;margin-left:2px}.md-image{line-height:0}.md-images{margin-bottom:-8px}.md-images .md-image{padding-top:0;padding-bottom:0}.md-images>span{display:block;float:left;margin:0 8px 8px 0}.md-image.align-left,td.blog-item-title-cell{text-align:left}.md-image.align-right{text-align:right}.md-image-wrap{overflow:hidden}.md-image-wrap>a{display:block}.md-image-note{line-height:150%;color:#777;padding:2px 0 4px}.language-ascii{line-height:125%!important}.head-inner{position:relative;border-bottom:2px solid #e0e0e0}.head-logo{font-size:15px;padding:16px 0;background-color:#fff}body:not(.theme-changing) .head-logo{transition:background-color .03s linear;-webkit-transition:background-color .03s linear;-moz-transition:background-color .03s linear}.head-logo:after{content:'';display:block;width:40px;position:absolute;right:-40px;top:0;bottom:0;border-left:8px solid #fff;box-sizing:border-box;background:linear-gradient(to left,rgba(255,255,255,0) 0,#fff 100%)}.head-logo>a{color:#222;font-size:14px}.head-logo>a:hover,a.head-item:hover{text-decoration:none}.head-logo-enter{background:#f3f3f3;color:#333;display:inline;opacity:0;font-size:11px;position:relative;padding:2px 5px;font-weight:400;vertical-align:middle;top:-1px}body:not(.theme-changing) .head-logo-enter{transition:opacity .03s linear;-webkit-transition:opacity .03s linear;-moz-transition:opacity .03s linear}.head-logo-enter-icon{width:12px;height:7px;display:inline-block;margin-right:5px;position:relative;top:1px}.head-logo-enter-icon>svg path{fill:#333}.head-logo>a:hover .head-logo-enter{opacity:1}.head-logo-path{color:#222;font-weight:700;-webkit-font-smoothing:antialiased}body:not(.theme-changing) .head-logo-path{transition:color .03s linear;-webkit-transition:color .03s linear;-moz-transition:color .03s linear}.head-logo-path:not(.neverhover):hover{color:#222!important}.head-logo-dolsign{color:#0bad19;font-weight:400}.head-logo-dolsign.is_root{color:#ce1a1a}.head-logo-cd{display:none}.head-logo>a:hover .head-logo-cd{display:inline}.head-logo-path-mapped{padding:3px 5px;background:#f1f1f1;pointer-events:none;border-radius:3px;margin:0 2px}.head-items{float:right;color:#777}a.head-item{color:#222;font-size:15px;display:block;float:left;padding:16px 0}a.head-item>span{position:relative;padding:0 12px;border-right:1px solid #d0d0d0}a.head-item>span>span{padding:2px 0}a.head-item>span>span>span.moon-icon>svg path{fill:#222}a.head-item.is-theme-switcher>span{padding-left:20px}a.head-item:last-child>span{border-right:0;padding-right:1px}a.head-item:hover>span>span{border-bottom:1px solid #d0d0d0}.blog-write-link-wrap{margin-bottom:18px}.blog-write-table{border:0;width:100%}.blog-write-table>tbody>tr>td:first-child{padding-right:8px;width:45%}.blog-write-table>tbody>tr>td:last-child{padding-left:8px}.blog-write-form .form-field-input{width:100%}.blog-write-form textarea.form-field-input{height:400px;font-size:12px}.blog-write-form textarea.form-field-input.nowrap{white-space:pre;overflow-wrap:normal}.blog-write-options-table{width:100%}.blog-write-options-table td{padding-top:12px}.blog-write-options-table td:first-child{width:70%}.blog-write-options-table td:nth-child(2){width:30%;padding-left:10px}.blog-item-row-year:first-child td,.blog-write-options-table tr:first-child td,.form-layout-v .form-field-wrap:first-child{padding-top:0}.blog-write-options-table button[type=submit]{margin-left:3px}.blog-write-form-toggle-link{margin-top:3px;display:inline-block}.blog-upload-form{padding-bottom:18px}.blog-upload-item{border-top:1px solid #e0e0e0;padding:10px 0}.blog-upload-item-name{font-weight:700;margin-bottom:2px}.blog-upload-item-info{color:#888;font-size:14px}.blog-upload-item-note{padding:0 0 4px}.blog-upload-item-md{margin-top:3px}.blog-post-title{margin:0 0 16px}.blog-post-title h1{font-size:22px;font-weight:700;padding:0;margin:0}.blog-post-date{color:#888;margin-top:5px;font-size:15px}.blog-post-tags{margin-top:16px;margin-bottom:-1px}.blog-post-tags>a{display:block;float:left;font-size:15px;margin-right:8px;cursor:pointer}.blog-post-tags>a:last-child{margin-right:0}.blog-post-tags>a>span{opacity:.5}.blog-post-text li{margin:13px 0}.blog-post-text p{margin-top:13px;margin-bottom:13px}.blog-post-text h3:first-child,.blog-post-text h4:first-child,.blog-post-text h5:first-child,.blog-post-text h6:first-child,.blog-post-text p:first-child,.blog-post-text td>pre:first-child{margin-top:0}.blog-post-text p:last-child,.blog-post-text td>pre:last-child{margin-bottom:0}.blog-post-text pre{background-color:#f3f3f3;overflow:auto;border-radius:3px}.blog-post-text code,.form-error{-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px}.blog-post-text code{background-color:#f1f1f1;font-size:85%;padding:3px 5px;border-radius:3px}.blog-post-text pre code{display:block;padding:12px;line-height:145%;background-color:#f3f3f3}.blog-post-text pre code span.term-prompt{color:#999;-moz-user-select:none;-webkit-user-select:none;user-select:none}.blog-post-text blockquote{border-left:3px solid #e0e0e0;margin-left:0;padding:5px 0 5px 12px;color:#888}.blog-post-text table.table-100{border:0;margin:0;width:100%}.blog-post-text table.table-100 td{padding:0 4px;border:0}.blog-post-text table.table-100 td:first-child{padding-left:0}.blog-post-text table.table-100 td:last-child{padding-right:0}.blog-post-text h1{margin:40px 0 16px;font-weight:600;font-size:30px;border-bottom:1px solid #e0e0e0;padding-bottom:8px}.blog-post-text h2{margin:35px 0 16px;font-weight:500;font-size:25px;border-bottom:1px solid #e0e0e0;padding-bottom:6px}.blog-post-text h3{margin:27px 0 16px;font-size:24px;font-weight:500}.blog-post-text h4{font-size:18px;margin:24px 0 16px}.blog-post-text h5{font-size:15px;margin:24px 0 16px}.blog-post-text h6{font-size:13px;margin:24px 0 16px;color:#666}.blog-post-text hr{height:1px;border:0;background:#e0e0e0;margin:17px 0}.blog-post-comments{margin-top:18px;padding:12px 15px;border:1px solid #e0e0e0;-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.blog-post-comments img{vertical-align:middle;position:relative;top:-1px;margin-left:2px}.index-blog-block{margin-top:23px}.blog-list.withtags{margin-right:211px}.blog-list-title{font-size:22px;margin-bottom:15px}.blog-list-title>span{margin-left:2px}.blog-list-title>span>a{font-size:16px;margin-left:2px}.blog-list-table-wrap{padding:5px 0}.blog-list-table td{vertical-align:top;padding:0 0 13px}.blog-list-table tr:last-child td,.form-layout-v .form-field-wrap:last-child{padding-bottom:0}td.blog-item-date-cell{width:1px;white-space:nowrap;text-align:right;padding-right:10px}.blog-item-date{color:#888}.blog-item-row{font-size:16px;line-height:140%}.blog-item-row.ishidden a.blog-item-title,.blog-tag-item>a{color:#222}.blog-item-row-year td{padding-top:10px;text-align:right;font-size:20px;letter-spacing:-.5px}.blog-tags{float:right;width:175px;padding-left:8px;border-left:1px solid #e0e0e0}.blog-tags-title{margin-bottom:15px;font-size:22px;padding:0 7px}.blog-tag-item{padding:6px 10px;font-size:15px}.form-error,.form-success{padding:10px 13px;margin-bottom:18px}.blog-tag-item-count{color:#aaa;margin-left:6px;text-decoration:none!important}form{display:block;margin:0}.form-layout-h .form-field-wrap{padding:8px 0}.form-layout-h .form-field-label{float:left;width:120px;text-align:right;padding:7px 0 0}.form-layout-h .form-field{margin-left:130px}.form-layout-v .form-field-wrap{padding:6px 0}.form-layout-v .form-field-label{padding:0 0 4px 4px;font-weight:700;font-size:12px;letter-spacing:.5px;text-transform:uppercase;color:#888}.form-error{background-color:#f9eeee;color:#d13d3d;border-radius:3px}.form-success{background-color:#eff5f0;color:#2a6f34;-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.page-edit-links{display:none;float:right;font-size:15px}.hljs,.page-content-inner:hover .page-edit-links{display:block}.hljs{overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-meta,.hljs-strong{font-weight:700}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}@media screen and (max-width:600px){textarea{-webkit-overflow-scrolling:touch}.head-logo{position:static;display:block;overflow:hidden;white-space:nowrap;padding-bottom:0}.blog-tags,.head-logo::after{display:none}.head-items{float:none}a.head-item:active,a.head-item:hover{-webkit-tap-highlight-color:transparent!important}a.head-item:last-child>span{border-right:0;padding-right:12px}a.head-item:first-child>span{padding-left:1px}.blog-list.withtags{margin-right:0}} \ No newline at end of file
diff --git a/htdocs/css/common_dark.css b/htdocs/css/common_dark.css
new file mode 100644
index 0000000..a7b4a4e
--- /dev/null
+++ b/htdocs/css/common_dark.css
@@ -0,0 +1 @@
+.head-logo,body,html{background-color:#222}.blog-item-date,.blog-post-date,.blog-post-text blockquote,.blog-post-text pre code span.term-prompt,.blog-upload-item-info,.form-layout-v .form-field-label,.head-logo>a:hover .head-logo-path:not(.alwayshover),.md-file-attach-size,.md-image-note,table.contacts div.note,table.contacts td.label{color:#798086}.blog-item-row.ishidden a.blog-item-title,.blog-tag-item>a,.head-logo-path,.head-logo>a,.md-file-attach-note,.pt h3,a.head-item,body,html{color:#eee}.blog-post-comments{border:1px solid #48535a}.blog-post-text code,.blog-post-text pre,.blog-post-text pre code{background-color:#394146}.blog-post-text h1,.blog-post-text h2{border-bottom:1px solid #48535a}.blog-post-text hr{background:#48535a}.blog-tags{border-left:1px solid #48535a}.blog-upload-item{border-top:1px solid #48535a}.head-inner{border-bottom:2px solid #48535a}.head-logo-dolsign.is_root{color:#e23636}.head-logo-enter{background:#394146;color:#cdd3d8}.head-logo-enter-icon>svg path{fill:#CDD3D8}.head-logo-path:not(.neverhover):hover{color:#eee!important}.head-logo:after{background:linear-gradient(to left,rgba(34,34,34,0) 0,#222 100%);border-left:8px solid #222}.hljs{background:#2b2b2d;color:#cdd3d8}.hljs-addition{background:#144212}.hljs-built_in,.hljs-builtin-name,.hljs-bullet,.hljs-symbol{color:#c792ea}.hljs-comment,.hljs-quote{color:#6272a4}.hljs-deletion{background:#e6e1dc}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#cdd3d8}.hljs-meta,.hljs-section,.hljs-selector-id,.hljs-title{color:#75a5ff}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:#bd93f9}.hljs-link,.hljs-regexp{color:#f77669}.hljs-doctag,.hljs-string{color:#f1fa8c}.hljs-attribute,.hljs-name,.hljs-tag{color:#abb2bf}.hljs-class .hljs-title,.hljs-type{color:#da4939}a{color:#71abe5}a.head-item:hover>span>span{border-bottom:1px solid #5e6264}a.head-item>span{border-right:1px solid #5e6264}a.head-item>span>span>span.moon-icon>svg path{fill:#eee}input[type=password],input[type=text],textarea{background-color:#30373b;border:1px solid #48535a;color:#eee}input[type=password]:focus,input[type=text]:focus,textarea:focus{border-color:#48535a}table.contacts div.note>a{border-bottom:1px solid #48535a;color:#798086}table.contacts div.note>a:hover{border-bottom-color:#71abe5;color:#71abe5}table.contacts td.value span{background:#394146;color:#eee} \ No newline at end of file
diff --git a/htdocs/js.php b/htdocs/js.php
new file mode 100644
index 0000000..c9939fe
--- /dev/null
+++ b/htdocs/js.php
@@ -0,0 +1,31 @@
+<?php
+
+require __DIR__.'/../init.php';
+global $config;
+
+$name = $_REQUEST['name'] ?? '';
+
+if (!$config['is_dev'] || !$name || !is_dir($path = ROOT.'/htdocs/js/'.$name)) {
+ http_response_code(403);
+ exit;
+}
+
+header('Content-Type: application/javascript');
+header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
+header("Cache-Control: post-check=0, pre-check=0", false);
+header("Pragma: no-cache");
+
+$files = scandir($path, SCANDIR_SORT_ASCENDING);
+$first = true;
+foreach ($files as $file) {
+ if ($file == '.' || $file == '..')
+ continue;
+ // logDebug(__FILE__.': reading '.$path.'/'.$file);
+ if (!$first)
+ echo "\n";
+ else
+ $first = false;
+ echo "/* $file */\n";
+ if (readfile($path.'/'.$file) === false)
+ logError(__FILE__.': failed to readfile('.$path.'/'.$file.')');
+}
diff --git a/htdocs/js/admin/00-common.js b/htdocs/js/admin/00-common.js
new file mode 100644
index 0000000..999099d
--- /dev/null
+++ b/htdocs/js/admin/00-common.js
@@ -0,0 +1 @@
+var LS = window.localStorage;
diff --git a/htdocs/js/admin/10-draft.js b/htdocs/js/admin/10-draft.js
new file mode 100644
index 0000000..99106f2
--- /dev/null
+++ b/htdocs/js/admin/10-draft.js
@@ -0,0 +1,29 @@
+var Draft = {
+ get: function() {
+ if (!LS) return null;
+
+ var title = LS.getItem('draft_title') || null;
+ var text = LS.getItem('draft_text') || null;
+
+ return {
+ title: title,
+ text: text
+ };
+ },
+
+ setTitle: function(text) {
+ if (!LS) return null;
+ LS.setItem('draft_title', text);
+ },
+
+ setText: function(text) {
+ if (!LS) return null;
+ LS.setItem('draft_text', text);
+ },
+
+ reset: function() {
+ if (!LS) return;
+ LS.removeItem('draft_title');
+ LS.removeItem('draft_text');
+ }
+}; \ No newline at end of file
diff --git a/htdocs/js/admin.js b/htdocs/js/admin/11-write-form.js
index a717d5c..b49a523 100644
--- a/htdocs/js/admin.js
+++ b/htdocs/js/admin/11-write-form.js
@@ -1,35 +1,3 @@
-var LS = window.localStorage;
-
-var Draft = {
- get: function() {
- if (!LS) return null;
-
- var title = LS.getItem('draft_title') || null;
- var text = LS.getItem('draft_text') || null;
-
- return {
- title: title,
- text: text
- };
- },
-
- setTitle: function(text) {
- if (!LS) return null;
- LS.setItem('draft_title', text);
- },
-
- setText: function(text) {
- if (!LS) return null;
- LS.setItem('draft_text', text);
- },
-
- reset: function() {
- if (!LS) return;
- LS.removeItem('draft_title');
- LS.removeItem('draft_text');
- }
-};
-
var AdminWriteForm = {
form: null,
previewTimeout: null,
@@ -170,24 +138,5 @@ var AdminWriteForm = {
}
}
};
-bindEventHandlers(AdminWriteForm);
-
-var BlogUploadList = {
- submitNoteEdit: function(action, note) {
- if (note === null)
- return;
-
- var form = document.createElement('form');
- form.setAttribute('method', 'post');
- form.setAttribute('action', action);
- var input = document.createElement('input');
- input.setAttribute('type', 'hidden');
- input.setAttribute('name', 'note');
- input.setAttribute('value', note);
-
- form.appendChild(input);
- document.body.appendChild(form);
- form.submit();
- }
-};
+bindEventHandlers(AdminWriteForm);
diff --git a/htdocs/js/admin/12-upload-list.js b/htdocs/js/admin/12-upload-list.js
new file mode 100644
index 0000000..5b496f6
--- /dev/null
+++ b/htdocs/js/admin/12-upload-list.js
@@ -0,0 +1,19 @@
+var BlogUploadList = {
+ submitNoteEdit: function(action, note) {
+ if (note === null)
+ return;
+
+ var form = document.createElement('form');
+ form.setAttribute('method', 'post');
+ form.setAttribute('action', action);
+
+ var input = document.createElement('input');
+ input.setAttribute('type', 'hidden');
+ input.setAttribute('name', 'note');
+ input.setAttribute('value', note);
+
+ form.appendChild(input);
+ document.body.appendChild(form);
+ form.submit();
+ }
+};
diff --git a/htdocs/js/common.js b/htdocs/js/common.js
deleted file mode 100644
index 959b672..0000000
--- a/htdocs/js/common.js
+++ /dev/null
@@ -1,679 +0,0 @@
-if (!String.prototype.startsWith) {
- String.prototype.startsWith = function(search, pos) {
- pos = !pos || pos < 0 ? 0 : +pos;
- return this.substring(pos, pos + search.length) === search;
- };
-}
-
-if (!String.prototype.endsWith) {
- String.prototype.endsWith = function(search, this_len) {
- if (this_len === undefined || this_len > this.length) {
- this_len = this.length;
- }
- return this.substring(this_len - search.length, this_len) === search;
- };
-}
-
-if (!Object.assign) {
- Object.defineProperty(Object, 'assign', {
- enumerable: false,
- configurable: true,
- writable: true,
- value: function(target, firstSource) {
- 'use strict';
- if (target === undefined || target === null) {
- throw new TypeError('Cannot convert first argument to object');
- }
-
- var to = Object(target);
- for (var i = 1; i < arguments.length; i++) {
- var nextSource = arguments[i];
- if (nextSource === undefined || nextSource === null) {
- continue;
- }
-
- var keysArray = Object.keys(Object(nextSource));
- for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
- var nextKey = keysArray[nextIndex];
- var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
- if (desc !== undefined && desc.enumerable) {
- to[nextKey] = nextSource[nextKey];
- }
- }
- }
- return to;
- }
- });
-}
-
-
-//
-// AJAX
-//
-(function() {
-
- var defaultOpts = {
- json: true
- };
-
- function createXMLHttpRequest() {
- if (window.XMLHttpRequest) {
- return new XMLHttpRequest();
- }
-
- var xhr;
- try {
- xhr = new ActiveXObject('Msxml2.XMLHTTP');
- } catch (e) {
- try {
- xhr = new ActiveXObject('Microsoft.XMLHTTP');
- } catch (e) {}
- }
- if (!xhr) {
- console.error('Your browser doesn\'t support XMLHttpRequest.');
- }
- return xhr;
- }
-
- function request(method, url, data, optarg1, optarg2) {
- data = data || null;
-
- var opts, callback;
- if (optarg2 !== undefined) {
- opts = optarg1;
- callback = optarg2;
- } else {
- callback = optarg1;
- }
-
- opts = opts || {};
-
- if (typeof callback != 'function') {
- throw new Error('callback must be a function');
- }
-
- if (!url) {
- throw new Error('no url specified');
- }
-
- switch (method) {
- case 'GET':
- if (isObject(data)) {
- for (var k in data) {
- if (data.hasOwnProperty(k)) {
- url += (url.indexOf('?') == -1 ? '?' : '&')+encodeURIComponent(k)+'='+encodeURIComponent(data[k])
- }
- }
- }
- break;
-
- case 'POST':
- if (isObject(data)) {
- var sdata = [];
- for (var k in data) {
- if (data.hasOwnProperty(k)) {
- sdata.push(encodeURIComponent(k)+'='+encodeURIComponent(data[k]));
- }
- }
- data = sdata.join('&');
- }
- break;
- }
-
- opts = Object.assign({}, defaultOpts, opts);
-
- var xhr = createXMLHttpRequest();
- xhr.open(method, url);
-
- xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
- if (method == 'POST') {
- xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
- }
-
- xhr.onreadystatechange = function() {
- if (xhr.readyState == 4) {
- if ('status' in xhr && !/^2|1223/.test(xhr.status)) {
- throw new Error('http code '+xhr.status)
- }
- if (opts.json) {
- var resp = JSON.parse(xhr.responseText)
- if (!isObject(resp)) {
- throw new Error('ajax: object expected')
- }
- if (resp.error) {
- throw new Error(resp.error)
- }
- callback(null, resp.response);
- } else {
- callback(null, xhr.responseText);
- }
- }
- };
-
- xhr.onerror = function(e) {
- callback(e);
- };
-
- xhr.send(method == 'GET' ? null : data);
-
- return xhr;
- }
-
- window.ajax = {
- get: request.bind(request, 'GET'),
- post: request.bind(request, 'POST')
- }
-
-})();
-
-function bindEventHandlers(obj) {
- for (var k in obj) {
- if (obj.hasOwnProperty(k)
- && typeof obj[k] == 'function'
- && k.length > 2
- && k.startsWith('on')
- && k[2].charCodeAt(0) >= 65
- && k[2].charCodeAt(0) <= 90) {
- obj[k] = obj[k].bind(obj)
- }
- }
-}
-
-//
-// DOM helpers
-//
-function ge(id) {
- return document.getElementById(id)
-}
-
-function hasClass(el, name) {
- return el && el.nodeType === 1 && (" " + el.className + " ").replace(/[\t\r\n\f]/g, " ").indexOf(" " + name + " ") >= 0
-}
-
-function addClass(el, name) {
- if (!el) {
- return console.warn('addClass: el is', el)
- }
- if (!hasClass(el, name)) {
- el.className = (el.className ? el.className + ' ' : '') + name
- }
-}
-
-function removeClass(el, name) {
- if (!el) {
- return console.warn('removeClass: el is', el)
- }
- if (isArray(name)) {
- for (var i = 0; i < name.length; i++) {
- removeClass(el, name[i]);
- }
- return;
- }
- el.className = ((el.className || '').replace((new RegExp('(\\s|^)' + name + '(\\s|$)')), ' ')).trim()
-}
-
-function addEvent(el, type, f, useCapture) {
- if (!el) {
- return console.warn('addEvent: el is', el, stackTrace())
- }
-
- if (isArray(type)) {
- for (var i = 0; i < type.length; i++) {
- addEvent(el, type[i], f, useCapture);
- }
- return;
- }
-
- if (el.addEventListener) {
- el.addEventListener(type, f, useCapture || false);
- return true;
- } else if (el.attachEvent) {
- return el.attachEvent('on' + type, f);
- }
-
- return false;
-}
-
-function removeEvent(el, type, f, useCapture) {
- if (isArray(type)) {
- for (var i = 0; i < type.length; i++) {
- var t = type[i];
- removeEvent(el, type[i], f, useCapture);
- }
- return;
- }
-
- if (el.removeEventListener) {
- el.removeEventListener(type, f, useCapture || false);
- } else if (el.detachEvent) {
- return el.detachEvent('on' + type, f);
- }
-
- return false;
-}
-
-function cancelEvent(evt) {
- if (!evt) {
- return console.warn('cancelEvent: event is', evt)
- }
-
- if (evt.preventDefault) evt.preventDefault();
- if (evt.stopPropagation) evt.stopPropagation();
-
- evt.cancelBubble = true;
- evt.returnValue = false;
-
- return false;
-}
-
-
-//
-// Cookies
-//
-function setCookie(name, value, days) {
- var expires = "";
- if (days) {
- var date = new Date();
- date.setTime(date.getTime() + (days*24*60*60*1000));
- expires = "; expires=" + date.toUTCString();
- }
- document.cookie = name + "=" + (value || "") + expires + "; domain=" + window.appConfig.cookieHost + "; path=/";
-}
-
-function unsetCookie(name) {
- document.cookie = name + '=; Max-Age=-99999999; domain=' + window.appConfig.cookieHost + "; path=/";
-}
-
-function getCookie(name) {
- var nameEQ = name + "=";
- var ca = document.cookie.split(';');
- for (var i = 0; i < ca.length; i++) {
- var c = ca[i];
- while (c.charAt(0) === ' ')
- c = c.substring(1, c.length);
- if (c.indexOf(nameEQ) === 0)
- return c.substring(nameEQ.length, c.length);
- }
- return null;
-}
-
-//
-// Misc
-//
-function isObject(o) {
- return Object.prototype.toString.call(o) === '[object Object]';
-}
-
-function isArray(a) {
- return Object.prototype.toString.call(a) === '[object Array]';
-}
-
-function extend(dst, src) {
- if (!isObject(dst)) {
- return console.error('extend: dst is not an object');
- }
- if (!isObject(src)) {
- return console.error('extend: src is not an object');
- }
- for (var key in src) {
- dst[key] = src[key];
- }
- return dst;
-}
-
-function stackTrace(split) {
- if (split === undefined) {
- split = true;
- }
- try {
- o.lo.lo += 0;
- } catch(e) {
- if (e.stack) {
- var stack = split ? e.stack.split('\n') : e.stack;
- stack.shift();
- stack.shift();
- return stack.join('\n');
- }
- }
- return null;
-}
-
-function escape(str) {
- var pre = document.createElement('pre');
- var text = document.createTextNode(str);
- pre.appendChild(text);
- return pre.innerHTML;
-}
-
-function parseUrl(uri) {
- var parser = document.createElement('a');
- parser.href = uri;
-
- return {
- protocol: parser.protocol, // => "http:"
- host: parser.host, // => "example.com:3000"
- hostname: parser.hostname, // => "example.com"
- port: parser.port, // => "3000"
- pathname: parser.pathname, // => "/pathname/"
- hash: parser.hash, // => "#hash"
- search: parser.search, // => "?search=test"
- origin: parser.origin, // => "http://example.com:3000"
- path: (parser.pathname || '') + (parser.search || '')
- }
-}
-
-function once(fn, context) {
- var result;
- return function() {
- if (fn) {
- result = fn.apply(context || this, arguments);
- fn = null;
- }
- return result;
- };
-}
-
-//
-//
-
-function lang(key) {
- return __lang[key] !== undefined ? __lang[key] : '{'+key+'}';
-}
-
-var DynamicLogo = {
- dynLink: null,
- afr: null,
- afrUrl: null,
-
- init: function() {
- this.dynLink = ge('head_dyn_link');
- this.cdText = ge('head_cd_text');
-
- if (!this.dynLink) {
- return console.warn('DynamicLogo.init: !this.dynLink');
- }
-
- var spans = this.dynLink.querySelectorAll('span.head-logo-path');
- for (var i = 0; i < spans.length; i++) {
- var span = spans[i];
- addEvent(span, 'mouseover', this.onSpanOver);
- addEvent(span, 'mouseout', this.onSpanOut);
- }
- },
-
- setUrl: function(url) {
- if (this.afr !== null) {
- cancelAnimationFrame(this.afr);
- }
- this.afrUrl = url;
- this.afr = requestAnimationFrame(this.onAnimationFrame);
- },
-
- onAnimationFrame: function() {
- var url = this.afrUrl;
-
- // update link
- this.dynLink.setAttribute('href', url);
-
- // update console text
- if (this.afrUrl === '/') {
- url = '~';
- } else {
- url = '~'+url.replace(/\/$/, '');
- }
- this.cdText.innerHTML = escape(url);
-
- this.afr = null;
- },
-
- onSpanOver: function() {
- var span = event.target;
- this.setUrl(span.getAttribute('data-url'));
- cancelEvent(event);
- },
-
- onSpanOut: function() {
- var span = event.target;
- this.setUrl('/');
- cancelEvent(event);
- }
-};
-bindEventHandlers(DynamicLogo);
-
-window.__lang = {};
-
-// set/remove retina cookie
-(function() {
- var isRetina = window.devicePixelRatio >= 1.5;
- if (isRetina) {
- setCookie('is_retina', 1, 365);
- } else {
- unsetCookie('is_retina');
- }
-})();
-
-var StaticManager = {
- loadedStyles: [],
- versions: {},
-
- setStyles: function(list, versions) {
- this.loadedStyles = list;
- this.versions = versions;
- },
-
- loadStyle: function(name, theme, callback) {
- var url;
- if (!window.appConfig.devMode) {
- var filename = name + (theme === 'dark' ? '_dark' : '') + '.css';
- url = '/css/'+filename+'?'+this.versions[filename];
- } else {
- url = '/sass.php?name='+name+'&theme='+theme;
- }
-
- var el = document.createElement('link');
- el.onerror = callback
- el.onload = callback
- el.setAttribute('rel', 'stylesheet');
- el.setAttribute('type', 'text/css');
- el.setAttribute('id', 'style_'+name+'_dark');
- el.setAttribute('href', url);
-
- document.getElementsByTagName('head')[0].appendChild(el);
- }
-};
-
-var ThemeSwitcher = (function() {
- /**
- * @type {string[]}
- */
- var modes = ['auto', 'dark', 'light'];
-
- /**
- * @type {number}
- */
- var currentModeIndex = -1;
-
- /**
- * @type {boolean|null}
- */
- var systemState = null;
-
- /**
- * @returns {boolean}
- */
- function isSystemModeSupported() {
- try {
- // crashes on:
- // Mozilla/5.0 (Windows NT 6.2; ARM; Trident/7.0; Touch; rv:11.0; WPDesktop; Lumia 630 Dual SIM) like Gecko
- // Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1
- // Mozilla/5.0 (iPad; CPU OS 12_5_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1
- //
- // error examples:
- // - window.matchMedia("(prefers-color-scheme: dark)").addEventListener is not a function. (In 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",this.onSystemSettingChange.bind(this))', 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener' is undefined)
- // - Object [object MediaQueryList] has no method 'addEventListener'
- return !!window['matchMedia']
- && typeof window.matchMedia("(prefers-color-scheme: dark)").addEventListener === 'function';
- } catch (e) {
- return false
- }
- }
-
- /**
- * @returns {boolean}
- */
- function isDarkModeApplied() {
- var st = StaticManager.loadedStyles;
- for (var i = 0; i < st.length; i++) {
- var name = st[i];
- if (ge('style_'+name+'_dark'))
- return true;
- }
- return false;
- }
-
- /**
- * @returns {string}
- */
- function getSavedMode() {
- var val = getCookie('theme');
- if (!val)
- return modes[0];
- if (modes.indexOf(val) === -1) {
- console.error('[ThemeSwitcher getSavedMode] invalid cookie value')
- unsetCookie('theme')
- return modes[0]
- }
- return val
- }
-
- /**
- * @param {boolean} dark
- */
- function changeTheme(dark) {
- addClass(document.body, 'theme-changing');
-
- var onDone = function() {
- window.requestAnimationFrame(function() {
- removeClass(document.body, 'theme-changing');
- })
- };
-
- window.requestAnimationFrame(function() {
- if (dark)
- enableDark(onDone);
- else
- disableDark(onDone);
- })
- }
-
- /**
- * @param {function} callback
- */
- function enableDark(callback) {
- var names = [];
- StaticManager.loadedStyles.forEach(function(name) {
- var el = ge('style_'+name+'_dark');
- if (el)
- return;
- names.push(name);
- });
-
- var left = names.length;
- names.forEach(function(name) {
- StaticManager.loadStyle(name, 'dark', once(function(e) {
- left--;
- if (left === 0)
- callback();
- }));
- })
- }
-
- /**
- * @param {function} callback
- */
- function disableDark(callback) {
- StaticManager.loadedStyles.forEach(function(name) {
- var el = ge('style_'+name+'_dark');
- if (el)
- el.remove();
- })
- callback();
- }
-
- /**
- * @param {string} mode
- */
- function setLabel(mode) {
- var labelEl = ge('theme-switcher-label');
- labelEl.innerHTML = escape(lang('theme_'+mode));
- }
-
- return {
- init: function() {
- var cur = getSavedMode();
- currentModeIndex = modes.indexOf(cur);
-
- var systemSupported = isSystemModeSupported();
- if (!systemSupported) {
- if (currentModeIndex === 0) {
- modes.shift(); // remove 'auto' from the list
- currentModeIndex = 1; // set to 'light'
- if (isDarkModeApplied())
- disableDark();
- }
- } else {
- /**
- * @param {boolean} dark
- */
- var onSystemChange = function(dark) {
- var prevSystemState = systemState;
- systemState = dark;
-
- if (modes[currentModeIndex] !== 'auto')
- return;
-
- if (systemState !== prevSystemState)
- changeTheme(systemState);
- };
-
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
- onSystemChange(e.matches === true)
- });
-
- onSystemChange(window.matchMedia('(prefers-color-scheme: dark)').matches === true);
- }
-
- setLabel(modes[currentModeIndex]);
- },
-
- next: function(e) {
- if (hasClass(document.body, 'theme-changing')) {
- console.log('next: theme changing is in progress, ignoring...')
- return;
- }
-
- currentModeIndex = (currentModeIndex + 1) % modes.length;
- switch (modes[currentModeIndex]) {
- case 'auto':
- if (systemState !== null)
- changeTheme(systemState);
- break;
-
- case 'light':
- if (isDarkModeApplied())
- changeTheme(false);
- break;
-
- case 'dark':
- if (!isDarkModeApplied())
- changeTheme(true);
- break;
- }
-
- setLabel(modes[currentModeIndex]);
- setCookie('theme', modes[currentModeIndex]);
-
- return cancelEvent(e);
- }
- };
-})(); \ No newline at end of file
diff --git a/htdocs/js/common/00-polyfills.js b/htdocs/js/common/00-polyfills.js
new file mode 100644
index 0000000..74ec195
--- /dev/null
+++ b/htdocs/js/common/00-polyfills.js
@@ -0,0 +1,47 @@
+if (!String.prototype.startsWith) {
+ String.prototype.startsWith = function(search, pos) {
+ pos = !pos || pos < 0 ? 0 : +pos;
+ return this.substring(pos, pos + search.length) === search;
+ };
+}
+
+if (!String.prototype.endsWith) {
+ String.prototype.endsWith = function(search, this_len) {
+ if (this_len === undefined || this_len > this.length) {
+ this_len = this.length;
+ }
+ return this.substring(this_len - search.length, this_len) === search;
+ };
+}
+
+if (!Object.assign) {
+ Object.defineProperty(Object, 'assign', {
+ enumerable: false,
+ configurable: true,
+ writable: true,
+ value: function(target, firstSource) {
+ 'use strict';
+ if (target === undefined || target === null) {
+ throw new TypeError('Cannot convert first argument to object');
+ }
+
+ var to = Object(target);
+ for (var i = 1; i < arguments.length; i++) {
+ var nextSource = arguments[i];
+ if (nextSource === undefined || nextSource === null) {
+ continue;
+ }
+
+ var keysArray = Object.keys(Object(nextSource));
+ for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
+ var nextKey = keysArray[nextIndex];
+ var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
+ if (desc !== undefined && desc.enumerable) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ return to;
+ }
+ });
+} \ No newline at end of file
diff --git a/htdocs/js/common/02-ajax.js b/htdocs/js/common/02-ajax.js
new file mode 100644
index 0000000..c432a51
--- /dev/null
+++ b/htdocs/js/common/02-ajax.js
@@ -0,0 +1,118 @@
+//
+// AJAX
+//
+(function() {
+
+ var defaultOpts = {
+ json: true
+ };
+
+ function createXMLHttpRequest() {
+ if (window.XMLHttpRequest) {
+ return new XMLHttpRequest();
+ }
+
+ var xhr;
+ try {
+ xhr = new ActiveXObject('Msxml2.XMLHTTP');
+ } catch (e) {
+ try {
+ xhr = new ActiveXObject('Microsoft.XMLHTTP');
+ } catch (e) {}
+ }
+ if (!xhr) {
+ console.error('Your browser doesn\'t support XMLHttpRequest.');
+ }
+ return xhr;
+ }
+
+ function request(method, url, data, optarg1, optarg2) {
+ data = data || null;
+
+ var opts, callback;
+ if (optarg2 !== undefined) {
+ opts = optarg1;
+ callback = optarg2;
+ } else {
+ callback = optarg1;
+ }
+
+ opts = opts || {};
+
+ if (typeof callback != 'function') {
+ throw new Error('callback must be a function');
+ }
+
+ if (!url) {
+ throw new Error('no url specified');
+ }
+
+ switch (method) {
+ case 'GET':
+ if (isObject(data)) {
+ for (var k in data) {
+ if (data.hasOwnProperty(k)) {
+ url += (url.indexOf('?') == -1 ? '?' : '&')+encodeURIComponent(k)+'='+encodeURIComponent(data[k])
+ }
+ }
+ }
+ break;
+
+ case 'POST':
+ if (isObject(data)) {
+ var sdata = [];
+ for (var k in data) {
+ if (data.hasOwnProperty(k)) {
+ sdata.push(encodeURIComponent(k)+'='+encodeURIComponent(data[k]));
+ }
+ }
+ data = sdata.join('&');
+ }
+ break;
+ }
+
+ opts = Object.assign({}, defaultOpts, opts);
+
+ var xhr = createXMLHttpRequest();
+ xhr.open(method, url);
+
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+ if (method == 'POST') {
+ xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+ }
+
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if ('status' in xhr && !/^2|1223/.test(xhr.status)) {
+ throw new Error('http code '+xhr.status)
+ }
+ if (opts.json) {
+ var resp = JSON.parse(xhr.responseText)
+ if (!isObject(resp)) {
+ throw new Error('ajax: object expected')
+ }
+ if (resp.error) {
+ throw new Error(resp.error)
+ }
+ callback(null, resp.response);
+ } else {
+ callback(null, xhr.responseText);
+ }
+ }
+ };
+
+ xhr.onerror = function(e) {
+ callback(e);
+ };
+
+ xhr.send(method == 'GET' ? null : data);
+
+ return xhr;
+ }
+
+ window.ajax = {
+ get: request.bind(request, 'GET'),
+ post: request.bind(request, 'POST')
+ }
+
+})();
diff --git a/htdocs/js/common/03-dom.js b/htdocs/js/common/03-dom.js
new file mode 100644
index 0000000..d05bcd0
--- /dev/null
+++ b/htdocs/js/common/03-dom.js
@@ -0,0 +1,117 @@
+//
+// DOM helpers
+//
+function ge(id) {
+ return document.getElementById(id)
+}
+
+function hasClass(el, name) {
+ return el && el.nodeType === 1 && (" " + el.className + " ").replace(/[\t\r\n\f]/g, " ").indexOf(" " + name + " ") >= 0
+}
+
+function addClass(el, name) {
+ if (!el) {
+ return console.warn('addClass: el is', el)
+ }
+ if (!hasClass(el, name)) {
+ el.className = (el.className ? el.className + ' ' : '') + name
+ }
+}
+
+function removeClass(el, name) {
+ if (!el) {
+ return console.warn('removeClass: el is', el)
+ }
+ if (isArray(name)) {
+ for (var i = 0; i < name.length; i++) {
+ removeClass(el, name[i]);
+ }
+ return;
+ }
+ el.className = ((el.className || '').replace((new RegExp('(\\s|^)' + name + '(\\s|$)')), ' ')).trim()
+}
+
+function addEvent(el, type, f, useCapture) {
+ if (!el) {
+ return console.warn('addEvent: el is', el, stackTrace())
+ }
+
+ if (isArray(type)) {
+ for (var i = 0; i < type.length; i++) {
+ addEvent(el, type[i], f, useCapture);
+ }
+ return;
+ }
+
+ if (el.addEventListener) {
+ el.addEventListener(type, f, useCapture || false);
+ return true;
+ } else if (el.attachEvent) {
+ return el.attachEvent('on' + type, f);
+ }
+
+ return false;
+}
+
+function removeEvent(el, type, f, useCapture) {
+ if (isArray(type)) {
+ for (var i = 0; i < type.length; i++) {
+ var t = type[i];
+ removeEvent(el, type[i], f, useCapture);
+ }
+ return;
+ }
+
+ if (el.removeEventListener) {
+ el.removeEventListener(type, f, useCapture || false);
+ } else if (el.detachEvent) {
+ return el.detachEvent('on' + type, f);
+ }
+
+ return false;
+}
+
+function cancelEvent(evt) {
+ if (!evt) {
+ return console.warn('cancelEvent: event is', evt)
+ }
+
+ if (evt.preventDefault) evt.preventDefault();
+ if (evt.stopPropagation) evt.stopPropagation();
+
+ evt.cancelBubble = true;
+ evt.returnValue = false;
+
+ return false;
+}
+
+
+//
+// Cookies
+//
+function setCookie(name, value, days) {
+ var expires = "";
+ if (days) {
+ var date = new Date();
+ date.setTime(date.getTime() + (days*24*60*60*1000));
+ expires = "; expires=" + date.toUTCString();
+ }
+ document.cookie = name + "=" + (value || "") + expires + "; domain=" + window.appConfig.cookieHost + "; path=/";
+}
+
+function unsetCookie(name) {
+ document.cookie = name + '=; Max-Age=-99999999; domain=' + window.appConfig.cookieHost + "; path=/";
+}
+
+function getCookie(name) {
+ var nameEQ = name + "=";
+ var ca = document.cookie.split(';');
+ for (var i = 0; i < ca.length; i++) {
+ var c = ca[i];
+ while (c.charAt(0) === ' ')
+ c = c.substring(1, c.length);
+ if (c.indexOf(nameEQ) === 0)
+ return c.substring(nameEQ.length, c.length);
+ }
+ return null;
+}
diff --git a/htdocs/js/common/04-util.js b/htdocs/js/common/04-util.js
new file mode 100644
index 0000000..da95e39
--- /dev/null
+++ b/htdocs/js/common/04-util.js
@@ -0,0 +1,89 @@
+function bindEventHandlers(obj) {
+ for (var k in obj) {
+ if (obj.hasOwnProperty(k)
+ && typeof obj[k] == 'function'
+ && k.length > 2
+ && k.startsWith('on')
+ && k[2].charCodeAt(0) >= 65
+ && k[2].charCodeAt(0) <= 90) {
+ obj[k] = obj[k].bind(obj)
+ }
+ }
+}
+
+function isObject(o) {
+ return Object.prototype.toString.call(o) === '[object Object]';
+}
+
+function isArray(a) {
+ return Object.prototype.toString.call(a) === '[object Array]';
+}
+
+function extend(dst, src) {
+ if (!isObject(dst)) {
+ return console.error('extend: dst is not an object');
+ }
+ if (!isObject(src)) {
+ return console.error('extend: src is not an object');
+ }
+ for (var key in src) {
+ dst[key] = src[key];
+ }
+ return dst;
+}
+
+function timestamp() {
+ return Math.floor(Date.now() / 1000)
+}
+
+function stackTrace(split) {
+ if (split === undefined) {
+ split = true;
+ }
+ try {
+ o.lo.lo += 0;
+ } catch(e) {
+ if (e.stack) {
+ var stack = split ? e.stack.split('\n') : e.stack;
+ stack.shift();
+ stack.shift();
+ return stack.join('\n');
+ }
+ }
+ return null;
+}
+
+function escape(str) {
+ var pre = document.createElement('pre');
+ var text = document.createTextNode(str);
+ pre.appendChild(text);
+ return pre.innerHTML;
+}
+
+function parseUrl(uri) {
+ var parser = document.createElement('a');
+ parser.href = uri;
+
+ return {
+ protocol: parser.protocol, // => "http:"
+ host: parser.host, // => "example.com:3000"
+ hostname: parser.hostname, // => "example.com"
+ port: parser.port, // => "3000"
+ pathname: parser.pathname, // => "/pathname/"
+ hash: parser.hash, // => "#hash"
+ search: parser.search, // => "?search=test"
+ origin: parser.origin, // => "http://example.com:3000"
+ path: (parser.pathname || '') + (parser.search || '')
+ }
+}
+
+function once(fn, context) {
+ var result;
+ return function() {
+ if (fn) {
+ result = fn.apply(context || this, arguments);
+ fn = null;
+ }
+ return result;
+ };
+} \ No newline at end of file
diff --git a/htdocs/js/common/10-lang.js b/htdocs/js/common/10-lang.js
new file mode 100644
index 0000000..bd0d8e7
--- /dev/null
+++ b/htdocs/js/common/10-lang.js
@@ -0,0 +1,5 @@
+function lang(key) {
+ return __lang[key] !== undefined ? __lang[key] : '{'+key+'}';
+}
+
+window.__lang = {}; \ No newline at end of file
diff --git a/htdocs/js/common/20-dynlogo.js b/htdocs/js/common/20-dynlogo.js
new file mode 100644
index 0000000..2d62a27
--- /dev/null
+++ b/htdocs/js/common/20-dynlogo.js
@@ -0,0 +1,60 @@
+var DynamicLogo = {
+ dynLink: null,
+ afr: null,
+ afrUrl: null,
+
+ init: function() {
+ this.dynLink = ge('head_dyn_link');
+ this.cdText = ge('head_cd_text');
+
+ if (!this.dynLink) {
+ return console.warn('DynamicLogo.init: !this.dynLink');
+ }
+
+ var spans = this.dynLink.querySelectorAll('span.head-logo-path');
+ for (var i = 0; i < spans.length; i++) {
+ var span = spans[i];
+ addEvent(span, 'mouseover', this.onSpanOver);
+ addEvent(span, 'mouseout', this.onSpanOut);
+ }
+ },
+
+ setUrl: function(url) {
+ if (this.afr !== null) {
+ cancelAnimationFrame(this.afr);
+ }
+ this.afrUrl = url;
+ this.afr = requestAnimationFrame(this.onAnimationFrame);
+ },
+
+ onAnimationFrame: function() {
+ var url = this.afrUrl;
+
+ // update link
+ this.dynLink.setAttribute('href', url);
+
+ // update console text
+ if (this.afrUrl === '/') {
+ url = '~';
+ } else {
+ url = '~'+url.replace(/\/$/, '');
+ }
+ this.cdText.innerHTML = escape(url);
+
+ this.afr = null;
+ },
+
+ onSpanOver: function() {
+ var span = event.target;
+ this.setUrl(span.getAttribute('data-url'));
+ cancelEvent(event);
+ },
+
+ onSpanOut: function() {
+ var span = event.target;
+ this.setUrl('/');
+ cancelEvent(event);
+ }
+};
+
+bindEventHandlers(DynamicLogo); \ No newline at end of file
diff --git a/htdocs/js/common/30-static-manager.js b/htdocs/js/common/30-static-manager.js
new file mode 100644
index 0000000..772df32
--- /dev/null
+++ b/htdocs/js/common/30-static-manager.js
@@ -0,0 +1,30 @@
+var StaticManager = {
+ loadedStyles: [],
+ versions: {},
+
+ init: function(loadedStyles, versions) {
+ this.loadedStyles = loadedStyles;
+ this.versions = versions;
+ },
+
+ loadStyle: function(name, theme, callback) {
+ var url;
+ if (!window.appConfig.devMode) {
+ if (theme === 'dark')
+ name += '_dark';
+ url = '/css/'+name+'.css?'+this.versions.css[name];
+ } else {
+ url = '/sass.php?name='+name+'&theme='+theme+'&v='+timestamp();
+ }
+
+ var el = document.createElement('link');
+ el.onerror = callback;
+ el.onload = callback;
+ el.setAttribute('rel', 'stylesheet');
+ el.setAttribute('type', 'text/css');
+ el.setAttribute('id', 'style_'+name);
+ el.setAttribute('href', url);
+
+ document.getElementsByTagName('head')[0].appendChild(el);
+ }
+};
diff --git a/htdocs/js/common/35-theme-switcher.js b/htdocs/js/common/35-theme-switcher.js
new file mode 100644
index 0000000..e716e4b
--- /dev/null
+++ b/htdocs/js/common/35-theme-switcher.js
@@ -0,0 +1,195 @@
+var ThemeSwitcher = (function() {
+ /**
+ * @type {string[]}
+ */
+ var modes = ['auto', 'dark', 'light'];
+
+ /**
+ * @type {number}
+ */
+ var currentModeIndex = -1;
+
+ /**
+ * @type {boolean|null}
+ */
+ var systemState = null;
+
+ /**
+ * @returns {boolean}
+ */
+ function isSystemModeSupported() {
+ try {
+ // crashes on:
+ // Mozilla/5.0 (Windows NT 6.2; ARM; Trident/7.0; Touch; rv:11.0; WPDesktop; Lumia 630 Dual SIM) like Gecko
+ // Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1
+ // Mozilla/5.0 (iPad; CPU OS 12_5_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1
+ //
+ // error examples:
+ // - window.matchMedia("(prefers-color-scheme: dark)").addEventListener is not a function. (In 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",this.onSystemSettingChange.bind(this))', 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener' is undefined)
+ // - Object [object MediaQueryList] has no method 'addEventListener'
+ return !!window['matchMedia']
+ && typeof window.matchMedia("(prefers-color-scheme: dark)").addEventListener === 'function';
+ } catch (e) {
+ return false
+ }
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ function isDarkModeApplied() {
+ var st = StaticManager.loadedStyles;
+ for (var i = 0; i < st.length; i++) {
+ var name = st[i];
+ if (ge('style_'+name+'_dark'))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @returns {string}
+ */
+ function getSavedMode() {
+ var val = getCookie('theme');
+ if (!val)
+ return modes[0];
+ if (modes.indexOf(val) === -1) {
+ console.error('[ThemeSwitcher getSavedMode] invalid cookie value')
+ unsetCookie('theme')
+ return modes[0]
+ }
+ return val
+ }
+
+ /**
+ * @param {boolean} dark
+ */
+ function changeTheme(dark) {
+ addClass(document.body, 'theme-changing');
+
+ var onDone = function() {
+ window.requestAnimationFrame(function() {
+ removeClass(document.body, 'theme-changing');
+ })
+ };
+
+ window.requestAnimationFrame(function() {
+ if (dark)
+ enableDark(onDone);
+ else
+ disableDark(onDone);
+ })
+ }
+
+ /**
+ * @param {function} callback
+ */
+ function enableDark(callback) {
+ var names = [];
+ StaticManager.loadedStyles.forEach(function(name) {
+ var el = ge('style_'+name+'_dark');
+ if (el)
+ return;
+ names.push(name);
+ });
+
+ var left = names.length;
+ names.forEach(function(name) {
+ StaticManager.loadStyle(name, 'dark', once(function(e) {
+ left--;
+ if (left === 0)
+ callback();
+ }));
+ })
+ }
+
+ /**
+ * @param {function} callback
+ */
+ function disableDark(callback) {
+ StaticManager.loadedStyles.forEach(function(name) {
+ var el = ge('style_'+name+'_dark');
+ if (el)
+ el.remove();
+ })
+ callback();
+ }
+
+ /**
+ * @param {string} mode
+ */
+ function setLabel(mode) {
+ var labelEl = ge('theme-switcher-label');
+ labelEl.innerHTML = escape(lang('theme_'+mode));
+ }
+
+ return {
+ init: function() {
+ var cur = getSavedMode();
+ currentModeIndex = modes.indexOf(cur);
+
+ var systemSupported = isSystemModeSupported();
+ if (!systemSupported) {
+ if (currentModeIndex === 0) {
+ modes.shift(); // remove 'auto' from the list
+ currentModeIndex = 1; // set to 'light'
+ if (isDarkModeApplied())
+ disableDark();
+ }
+ } else {
+ /**
+ * @param {boolean} dark
+ */
+ var onSystemChange = function(dark) {
+ var prevSystemState = systemState;
+ systemState = dark;
+
+ if (modes[currentModeIndex] !== 'auto')
+ return;
+
+ if (systemState !== prevSystemState)
+ changeTheme(systemState);
+ };
+
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
+ onSystemChange(e.matches === true)
+ });
+
+ onSystemChange(window.matchMedia('(prefers-color-scheme: dark)').matches === true);
+ }
+
+ setLabel(modes[currentModeIndex]);
+ },
+
+ next: function(e) {
+ if (hasClass(document.body, 'theme-changing')) {
+ console.log('next: theme changing is in progress, ignoring...')
+ return;
+ }
+
+ currentModeIndex = (currentModeIndex + 1) % modes.length;
+ switch (modes[currentModeIndex]) {
+ case 'auto':
+ if (systemState !== null)
+ changeTheme(systemState);
+ break;
+
+ case 'light':
+ if (isDarkModeApplied())
+ changeTheme(false);
+ break;
+
+ case 'dark':
+ if (!isDarkModeApplied())
+ changeTheme(true);
+ break;
+ }
+
+ setLabel(modes[currentModeIndex]);
+ setCookie('theme', modes[currentModeIndex]);
+
+ return cancelEvent(e);
+ }
+ };
+})(); \ No newline at end of file
diff --git a/htdocs/js/common/90-retina.js b/htdocs/js/common/90-retina.js
new file mode 100644
index 0000000..46b9f17
--- /dev/null
+++ b/htdocs/js/common/90-retina.js
@@ -0,0 +1,9 @@
+// set/remove retina cookie
+(function() {
+ var isRetina = window.devicePixelRatio >= 1.5;
+ if (isRetina) {
+ setCookie('is_retina', 1, 365);
+ } else {
+ unsetCookie('is_retina');
+ }
+})(); \ No newline at end of file
diff --git a/lib/cli.php b/lib/cli.php
index 3bda20f..a860871 100644
--- a/lib/cli.php
+++ b/lib/cli.php
@@ -63,8 +63,12 @@ class cli {
}
public static function die($error): void {
- fwrite(STDERR, "error: {$error}\n");
+ self::error($error);
exit(1);
}
+ public static function error($error): void {
+ fwrite(STDERR, "error: {$error}\n");
+ }
+
} \ No newline at end of file
diff --git a/skin/base.skin.php b/skin/base.skin.php
index 2d71312..616e1b3 100644
--- a/skin/base.skin.php
+++ b/skin/base.skin.php
@@ -45,11 +45,20 @@ function renderScript($ctx, $unsafe_js, $unsafe_lang, $enable_dynlogo) {
global $config;
$styles = json_encode($ctx->styleNames);
-$versions = !$config['is_dev'] ? json_encode($config['static']) : '{}';
+if ($config['is_dev'])
+ $versions = '{}';
+else {
+ $versions = [];
+ foreach ($config['static'] as $name => $v) {
+ list($type, $bname) = getStaticNameParts($name);
+ $versions[$type][$bname] = $v;
+ }
+ $versions = json_encode($versions);
+}
return <<<HTML
<script type="text/javascript">
-StaticManager.setStyles({$styles}, {$versions});
+StaticManager.init({$styles}, {$versions});
{$ctx->if_true($unsafe_js, '(function(){'.$unsafe_js.'})();')}
{$ctx->if_true($unsafe_lang, 'extend(__lang, '.$unsafe_lang.');')}
{$ctx->if_true($enable_dynlogo, 'DynamicLogo.init();')}
@@ -77,40 +86,46 @@ function renderStatic($ctx, $static, $theme) {
$ctx->styleNames = [];
foreach ($static as $name) {
// javascript
- if (str_ends_with($name, '.js'))
+ if (str_starts_with($name, 'js/'))
$html[] = jsLink($name);
- // cs
- else if (str_ends_with($name, '.css')) {
+ // css
+ else if (str_starts_with($name, 'css/')) {
$html[] = cssLink($name, 'light', $style_name);
$ctx->styleNames[] = $style_name;
if ($dark)
$html[] = cssLink($name, 'dark', $style_name);
else if (!$config['is_dev'])
- $html[] = cssPrefetchLink(str_replace('.css', '_dark.css', $name));
+ $html[] = cssPrefetchLink($style_name.'_dark');
}
+ else
+ logError(__FUNCTION__.': unexpected static entry: '.$name);
}
return implode("\n", $html);
}
function jsLink(string $name): string {
- return '<script src="'.$name.'?'.getStaticVersion($name).'" type="text/javascript"></script>';
+ global $config;
+ list (, $bname) = getStaticNameParts($name);
+ if ($config['is_dev']) {
+ $href = '/js.php?name='.urlencode($bname).'&amp;v='.time();
+ } else {
+ $href = '/dist-js/'.$bname.'.js?'.getStaticVersion($name);
+ }
+ return '<script src="'.$href.'" type="text/javascript"></script>';
}
function cssLink(string $name, string $theme, &$bname = null): string {
global $config;
- $dname = dirname($name);
- $bname = basename($name);
- if (($pos = strrpos($bname, '.')))
- $bname = substr($bname, 0, $pos);
+ list(, $bname) = getStaticNameParts($name);
if ($config['is_dev']) {
$href = '/sass.php?name='.urlencode($bname).'&amp;theme='.$theme.'&amp;v='.time();
} else {
$version = getStaticVersion('css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css');
- $href = $dname.'/'.$bname.($theme == 'dark' ? '_dark' : '').'.css?'.$version;
+ $href = '/dist-css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css?'.$version;
}
$id = 'style_'.$bname;
@@ -121,17 +136,33 @@ function cssLink(string $name, string $theme, &$bname = null): string {
}
function cssPrefetchLink(string $name): string {
- $url = $name.'?'.getStaticVersion($name);
- return <<<HTML
+$url = '/dist-css/'.$name.'.css?'.getStaticVersion('css/'.$name.'.css');
+return <<<HTML
<link rel="prefetch" href="{$url}" />
HTML;
}
+function getStaticNameParts(string $name): array {
+ $dname = dirname($name);
+ $bname = basename($name);
+ if (($pos = strrpos($bname, '.'))) {
+ $ext = substr($bname, $pos+1);
+ $bname = substr($bname, 0, $pos);
+ } else {
+ $ext = '';
+ }
+ return [$dname, $bname, $ext];
+}
+
function getStaticVersion(string $name): string {
global $config;
- if (str_starts_with($name, '/'))
+ if ($config['is_dev'])
+ return time();
+ if (str_starts_with($name, '/')) {
+ logWarning(__FUNCTION__.': '.$name.' starts with /');
$name = substr($name, 1);
- return $config['is_dev'] ? time() : $config['static'][$name] ?? 'notfound';
+ }
+ return $config['static'][$name] ?? 'notfound';
}
function renderHeader($ctx, $theme, $unsafe_logo_html) {