rewrite css and js assets building
This commit is contained in:
parent
cb13ea239b
commit
864e73cdc7
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,7 +1,6 @@
|
|||||||
/debug.log
|
/debug.log
|
||||||
test.php
|
test.php
|
||||||
/.git
|
/.git
|
||||||
/htdocs/css
|
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/vendor/
|
/vendor/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@ -10,3 +9,5 @@ test.php
|
|||||||
config-static.php
|
config-static.php
|
||||||
config-local.php
|
config-local.php
|
||||||
/.idea
|
/.idea
|
||||||
|
/htdocs/dist-css
|
||||||
|
/htdocs/dist-js
|
||||||
|
@ -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();
|
|
74
deploy/build_common.sh
Normal file
74
deploy/build_common.sh
Normal file
@ -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
|
||||||
|
}
|
62
deploy/build_css.sh
Executable file
62
deploy/build_css.sh
Executable file
@ -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
|
31
deploy/build_js.sh
Executable file
31
deploy/build_js.sh
Executable file
@ -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
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
set -e
|
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"
|
STAGING_DIR="$HOME/staging"
|
||||||
PROD_DIR="$HOME/prod"
|
PROD_DIR="$HOME/prod"
|
||||||
PHP=/usr/bin/php8.1
|
PHP=/usr/bin/php8.1
|
||||||
@ -33,7 +33,9 @@ fi
|
|||||||
cp "$DEV_DIR/config-local.php" .
|
cp "$DEV_DIR/config-local.php" .
|
||||||
sed -i '/is_dev/d' ./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
|
popd
|
||||||
|
|
||||||
@ -43,6 +45,8 @@ rsync -a --delete --delete-excluded --info=progress2 "$STAGING_DIR/" "$PROD_DIR/
|
|||||||
--exclude debug.log \
|
--exclude debug.log \
|
||||||
--exclude='/composer.*' \
|
--exclude='/composer.*' \
|
||||||
--exclude='/htdocs/scss' \
|
--exclude='/htdocs/scss' \
|
||||||
|
--exclude='/htdocs/js' \
|
||||||
--exclude='/htdocs/sass.php' \
|
--exclude='/htdocs/sass.php' \
|
||||||
|
--exclude='/htdocs/js.php' \
|
||||||
--exclude='*.sh' \
|
--exclude='*.sh' \
|
||||||
--exclude='*.sql'
|
--exclude='*.sql'
|
57
deploy/gen_static_config.php
Executable file
57
deploy/gen_static_config.php
Executable file
@ -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);
|
||||||
|
}
|
@ -39,8 +39,8 @@ class RequestDispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$skin = new Skin();
|
$skin = new Skin();
|
||||||
$skin->static[] = '/css/common.css';
|
$skin->static[] = 'css/common.css';
|
||||||
$skin->static[] = '/js/common.js';
|
$skin->static[] = 'js/common.js';
|
||||||
|
|
||||||
$lang = LangData::getInstance();
|
$lang = LangData::getInstance();
|
||||||
$skin->addLangKeys($lang->search('/^theme_/'));
|
$skin->addLangKeys($lang->search('/^theme_/'));
|
||||||
|
@ -72,6 +72,9 @@ class logging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function logCustom(LogLevel $level, ...$args): void {
|
public static function logCustom(LogLevel $level, ...$args): void {
|
||||||
|
global $config;
|
||||||
|
if (!$config['is_dev'] && $level == LogLevel::DEBUG)
|
||||||
|
return;
|
||||||
self::write($level, self::strVars($args));
|
self::write($level, self::strVars($args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ use Response;
|
|||||||
class AdminRequestHandler extends \RequestHandler {
|
class AdminRequestHandler extends \RequestHandler {
|
||||||
|
|
||||||
public function beforeDispatch(): ?Response {
|
public function beforeDispatch(): ?Response {
|
||||||
$this->skin->static[] = '/css/admin.css';
|
$this->skin->static[] = 'css/admin.css';
|
||||||
$this->skin->static[] = '/js/admin.js';
|
$this->skin->static[] = 'js/admin.js';
|
||||||
|
|
||||||
if (!($this instanceof Login) && !admin::isAdmin())
|
if (!($this instanceof Login) && !admin::isAdmin())
|
||||||
throw new \ForbiddenException('looks like you are not admin');
|
throw new \ForbiddenException('looks like you are not admin');
|
||||||
|
1
htdocs/css/admin.css
Normal file
1
htdocs/css/admin.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
.admin-page{line-height:155%}
|
0
htdocs/css/admin_dark.css
Normal file
0
htdocs/css/admin_dark.css
Normal file
1
htdocs/css/common.css
Normal file
1
htdocs/css/common.css
Normal file
File diff suppressed because one or more lines are too long
1
htdocs/css/common_dark.css
Normal file
1
htdocs/css/common_dark.css
Normal file
@ -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}
|
31
htdocs/js.php
Normal file
31
htdocs/js.php
Normal file
@ -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.')');
|
||||||
|
}
|
1
htdocs/js/admin/00-common.js
Normal file
1
htdocs/js/admin/00-common.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
var LS = window.localStorage;
|
29
htdocs/js/admin/10-draft.js
Normal file
29
htdocs/js/admin/10-draft.js
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
};
|
@ -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 = {
|
var AdminWriteForm = {
|
||||||
form: null,
|
form: null,
|
||||||
previewTimeout: null,
|
previewTimeout: null,
|
||||||
@ -170,24 +138,5 @@ var AdminWriteForm = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
bindEventHandlers(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();
|
|
||||||
}
|
|
||||||
};
|
|
19
htdocs/js/admin/12-upload-list.js
Normal file
19
htdocs/js/admin/12-upload-list.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
};
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
47
htdocs/js/common/00-polyfills.js
Normal file
47
htdocs/js/common/00-polyfills.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
118
htdocs/js/common/02-ajax.js
Normal file
118
htdocs/js/common/02-ajax.js
Normal file
@ -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')
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
117
htdocs/js/common/03-dom.js
Normal file
117
htdocs/js/common/03-dom.js
Normal file
@ -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;
|
||||||
|
}
|
89
htdocs/js/common/04-util.js
Normal file
89
htdocs/js/common/04-util.js
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
}
|
5
htdocs/js/common/10-lang.js
Normal file
5
htdocs/js/common/10-lang.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function lang(key) {
|
||||||
|
return __lang[key] !== undefined ? __lang[key] : '{'+key+'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__lang = {};
|
60
htdocs/js/common/20-dynlogo.js
Normal file
60
htdocs/js/common/20-dynlogo.js
Normal file
@ -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);
|
30
htdocs/js/common/30-static-manager.js
Normal file
30
htdocs/js/common/30-static-manager.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
};
|
195
htdocs/js/common/35-theme-switcher.js
Normal file
195
htdocs/js/common/35-theme-switcher.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
9
htdocs/js/common/90-retina.js
Normal file
9
htdocs/js/common/90-retina.js
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
})();
|
@ -63,8 +63,12 @@ class cli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function die($error): void {
|
public static function die($error): void {
|
||||||
fwrite(STDERR, "error: {$error}\n");
|
self::error($error);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function error($error): void {
|
||||||
|
fwrite(STDERR, "error: {$error}\n");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -45,11 +45,20 @@ function renderScript($ctx, $unsafe_js, $unsafe_lang, $enable_dynlogo) {
|
|||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
$styles = json_encode($ctx->styleNames);
|
$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
|
return <<<HTML
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
StaticManager.setStyles({$styles}, {$versions});
|
StaticManager.init({$styles}, {$versions});
|
||||||
{$ctx->if_true($unsafe_js, '(function(){'.$unsafe_js.'})();')}
|
{$ctx->if_true($unsafe_js, '(function(){'.$unsafe_js.'})();')}
|
||||||
{$ctx->if_true($unsafe_lang, 'extend(__lang, '.$unsafe_lang.');')}
|
{$ctx->if_true($unsafe_lang, 'extend(__lang, '.$unsafe_lang.');')}
|
||||||
{$ctx->if_true($enable_dynlogo, 'DynamicLogo.init();')}
|
{$ctx->if_true($enable_dynlogo, 'DynamicLogo.init();')}
|
||||||
@ -77,40 +86,46 @@ function renderStatic($ctx, $static, $theme) {
|
|||||||
$ctx->styleNames = [];
|
$ctx->styleNames = [];
|
||||||
foreach ($static as $name) {
|
foreach ($static as $name) {
|
||||||
// javascript
|
// javascript
|
||||||
if (str_ends_with($name, '.js'))
|
if (str_starts_with($name, 'js/'))
|
||||||
$html[] = jsLink($name);
|
$html[] = jsLink($name);
|
||||||
|
|
||||||
// cs
|
// css
|
||||||
else if (str_ends_with($name, '.css')) {
|
else if (str_starts_with($name, 'css/')) {
|
||||||
$html[] = cssLink($name, 'light', $style_name);
|
$html[] = cssLink($name, 'light', $style_name);
|
||||||
$ctx->styleNames[] = $style_name;
|
$ctx->styleNames[] = $style_name;
|
||||||
|
|
||||||
if ($dark)
|
if ($dark)
|
||||||
$html[] = cssLink($name, 'dark', $style_name);
|
$html[] = cssLink($name, 'dark', $style_name);
|
||||||
else if (!$config['is_dev'])
|
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);
|
return implode("\n", $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
function jsLink(string $name): string {
|
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).'&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 {
|
function cssLink(string $name, string $theme, &$bname = null): string {
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
$dname = dirname($name);
|
list(, $bname) = getStaticNameParts($name);
|
||||||
$bname = basename($name);
|
|
||||||
if (($pos = strrpos($bname, '.')))
|
|
||||||
$bname = substr($bname, 0, $pos);
|
|
||||||
|
|
||||||
if ($config['is_dev']) {
|
if ($config['is_dev']) {
|
||||||
$href = '/sass.php?name='.urlencode($bname).'&theme='.$theme.'&v='.time();
|
$href = '/sass.php?name='.urlencode($bname).'&theme='.$theme.'&v='.time();
|
||||||
} else {
|
} else {
|
||||||
$version = getStaticVersion('css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css');
|
$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;
|
$id = 'style_'.$bname;
|
||||||
@ -121,17 +136,33 @@ function cssLink(string $name, string $theme, &$bname = null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cssPrefetchLink(string $name): string {
|
function cssPrefetchLink(string $name): string {
|
||||||
$url = $name.'?'.getStaticVersion($name);
|
$url = '/dist-css/'.$name.'.css?'.getStaticVersion('css/'.$name.'.css');
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<link rel="prefetch" href="{$url}" />
|
<link rel="prefetch" href="{$url}" />
|
||||||
HTML;
|
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 {
|
function getStaticVersion(string $name): string {
|
||||||
global $config;
|
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);
|
$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) {
|
function renderHeader($ctx, $theme, $unsafe_logo_html) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user