articles and books interface, search in wff archive
This commit is contained in:
parent
dca4a9f9fc
commit
fed70b8045
@ -3,6 +3,7 @@
|
||||
"gch1p/parsedown-highlight": "master",
|
||||
"gch1p/parsedown-highlight-extended": "dev-main",
|
||||
"erusev/parsedown": "1.8.0-beta-7",
|
||||
"gigablah/sphinxphp": "2.0.*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-mysqli": "*",
|
||||
|
60
composer.lock
generated
60
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ca8ca355a9f6ce85170f473238a57d6b",
|
||||
"content-hash": "dba6710f07144861b58159926e3c0cc7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "erusev/parsedown",
|
||||
@ -210,6 +210,61 @@
|
||||
],
|
||||
"time": "2023-03-01T22:30:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "gigablah/sphinxphp",
|
||||
"version": "2.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/gigablah/sphinxphp.git",
|
||||
"reference": "6d5e97fdd33c1129ca372203d1330827c1cbc46c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/gigablah/sphinxphp/zipball/6d5e97fdd33c1129ca372203d1330827c1cbc46c",
|
||||
"reference": "6d5e97fdd33c1129ca372203d1330827c1cbc46c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "3.7.*",
|
||||
"satooshi/php-coveralls": "dev-master"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Sphinx": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"GPL-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Andrew Aksyonoff",
|
||||
"homepage": "http://sphinxsearch.com/"
|
||||
}
|
||||
],
|
||||
"description": "Sphinx Search PHP API",
|
||||
"homepage": "http://sphinxsearch.com/",
|
||||
"keywords": [
|
||||
"api",
|
||||
"search",
|
||||
"sphinx"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/gigablah/sphinxphp/issues",
|
||||
"source": "https://github.com/gigablah/sphinxphp/tree/2.0.x"
|
||||
},
|
||||
"time": "2013-08-22T08:05:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "scrivo/highlight.php",
|
||||
"version": "v9.18.1.10",
|
||||
@ -301,7 +356,8 @@
|
||||
"ext-mbstring": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-mysqli": "*",
|
||||
"ext-json": "*"
|
||||
"ext-json": "*",
|
||||
"ext-yaml": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.3.0"
|
||||
|
@ -14,6 +14,8 @@ mysql:
|
||||
log: false
|
||||
log_stat: false
|
||||
|
||||
sphinx:
|
||||
host: "127.0.0.1"
|
||||
|
||||
umask: 0002
|
||||
group: www-data
|
||||
@ -25,6 +27,7 @@ password_salt: "456"
|
||||
|
||||
uploads_dir: /home/user/files.example.org
|
||||
uploads_path: /uploads
|
||||
files_domain: files.example.org
|
||||
|
||||
# deploy config
|
||||
git_repo: git@github.com:example/example_org.git
|
||||
|
@ -24,6 +24,8 @@ class mysql {
|
||||
$arg_val = $args[$i];
|
||||
if (is_null($arg_val)) {
|
||||
$v = 'NULL';
|
||||
} elseif ($arg_val instanceof BackedEnum) {
|
||||
$v = '\''.$this->escape($arg_val->value).'\'';
|
||||
} else {
|
||||
$v = '\''.$this->escape($arg_val).'\'';
|
||||
}
|
||||
@ -230,8 +232,6 @@ class mysql_bitfield {
|
||||
if ($bit < 0 || $bit >= $this->size)
|
||||
throw new Exception('invalid bit '.$bit.', allowed range: [0..'.$this->size.')');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function DB(): mysql|null {
|
||||
|
@ -58,7 +58,7 @@ function http_error(HTTPCode $http_code, string $message = ''): void {
|
||||
$data['message'] = $message;
|
||||
ajax_error((object)$data, $http_code->value);
|
||||
} else {
|
||||
$ctx = new SkinContext('\\skin\\error');
|
||||
$ctx = skin('error');
|
||||
$http_message = preg_replace('/(?<!^)([A-Z])/', ' $1', $http_code->name);
|
||||
$html = $ctx->http_error($http_code->value, $http_message, $message);
|
||||
http_response_code($http_code->value);
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
const ROUTER_VERSION = 2;
|
||||
const ROUTER_VERSION = 3;
|
||||
const ROUTER_MC_KEY = '4in1/routes';
|
||||
|
||||
$RouterInput = [];
|
||||
|
105
engine/skin.php
105
engine/skin.php
@ -9,7 +9,6 @@ $SkinState = new class {
|
||||
public array $options = [
|
||||
'full_width' => false,
|
||||
'wide' => false,
|
||||
'dynlogo_enabled' => true,
|
||||
'logo_path_map' => [],
|
||||
'logo_link_map' => [],
|
||||
'is_index' => false,
|
||||
@ -17,13 +16,15 @@ $SkinState = new class {
|
||||
'articles_lang' => null,
|
||||
];
|
||||
public array $static = [];
|
||||
public array $svg_defs = [];
|
||||
};
|
||||
|
||||
function render($f, ...$vars): void {
|
||||
global $SkinState, $config;
|
||||
|
||||
$f = '\\skin\\'.str_replace('/', '\\', $f);
|
||||
$ctx = new SkinContext(substr($f, 0, ($pos = strrpos($f, '\\'))));
|
||||
add_skin_strings(['4in1']);
|
||||
|
||||
$ctx = skin(substr($f, 0, ($pos = strrpos(str_replace('/', '\\', $f), '\\'))));
|
||||
$body = call_user_func_array([$ctx, substr($f, $pos + 1)], $vars);
|
||||
if (is_array($body))
|
||||
list($body, $js) = $body;
|
||||
@ -34,12 +35,12 @@ function render($f, ...$vars): void {
|
||||
if ($theme != 'auto' && !themeExists($theme))
|
||||
$theme = 'auto';
|
||||
|
||||
$layout_ctx = new SkinContext('\\skin\\base');
|
||||
$layout_ctx = skin('base');
|
||||
|
||||
$lang = [];
|
||||
foreach ($SkinState->lang as $key)
|
||||
$lang[$key] = lang($key);
|
||||
$lang = !empty($lang) ? jsonEncode($lang, JSON_UNESCAPED_UNICODE) : '';
|
||||
$lang = !empty($lang) ? jsonEncode($lang) : '';
|
||||
|
||||
$title = $SkinState->title;
|
||||
if (!$SkinState->options['is_index'])
|
||||
@ -56,6 +57,7 @@ function render($f, ...$vars): void {
|
||||
unsafe_body: $body,
|
||||
exec_time: exectime(),
|
||||
admin_email: $config['admin_email'],
|
||||
svg_defs: $SkinState->svg_defs
|
||||
);
|
||||
echo $html;
|
||||
exit;
|
||||
@ -127,6 +129,9 @@ class SkinContext {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($plain_args && !isset($arguments[$key]))
|
||||
break;
|
||||
|
||||
if (is_string($arguments[$key]) || $arguments[$key] instanceof SkinString) {
|
||||
if (is_string($arguments[$key]))
|
||||
$arguments[$key] = new SkinString($arguments[$key]);
|
||||
@ -189,8 +194,10 @@ class SkinContext {
|
||||
return csrf_get($key);
|
||||
}
|
||||
|
||||
function bc(array $items, ?string $style = null): string {
|
||||
$buf = implode(array_map(function(array $i): string {
|
||||
function bc(array $items, ?string $style = null, bool $mt = false): string {
|
||||
static $chevron = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.47 4.217a.75.75 0 0 0 0 1.06L12.185 10 7.469 14.72a.75.75 0 1 0 1.062 1.06l5.245-5.25a.75.75 0 0 0 0-1.061L8.531 4.218a.75.75 0 0 0-1.061-.001z" fill="currentColor"/></svg>';
|
||||
|
||||
$buf = implode(array_map(function(array $i) use ($chevron): string {
|
||||
$buf = '';
|
||||
$has_url = array_key_exists('url', $i);
|
||||
|
||||
@ -201,13 +208,16 @@ class SkinContext {
|
||||
$buf .= htmlescape($i['text']);
|
||||
|
||||
if ($has_url)
|
||||
$buf .= ' <span class="bc-arrow">›</span></a>';
|
||||
$buf .= ' <span class="bc-arrow">'.$chevron.'</span></a>';
|
||||
else
|
||||
$buf .= '</span>';
|
||||
|
||||
return $buf;
|
||||
}, $items));
|
||||
return '<div class="bc"'.($style ? ' style="'.$style.'"' : '').'>'.$buf.'</div>';
|
||||
$class = 'bc';
|
||||
if ($mt)
|
||||
$class .= ' mt';
|
||||
return '<div class="'.$class.'"'.($style ? ' style="'.$style.'"' : '').'>'.$buf.'</div>';
|
||||
}
|
||||
|
||||
protected function _if_condition($condition, $callback, ...$args) {
|
||||
@ -236,6 +246,10 @@ class SkinContext {
|
||||
return htmlescape($this->langRaw(...$args));
|
||||
}
|
||||
|
||||
function lang_num(...$args): string {
|
||||
return htmlescape(lang_num(...$args));
|
||||
}
|
||||
|
||||
function langRaw(string $key, ...$args) {
|
||||
$val = lang($key);
|
||||
return empty($args) ? $val : sprintf($val, ...$args);
|
||||
@ -243,6 +257,79 @@ class SkinContext {
|
||||
|
||||
}
|
||||
|
||||
class SVGSkinContext extends SkinContext {
|
||||
|
||||
function __construct() {
|
||||
parent::__construct('\\skin\\icons');
|
||||
}
|
||||
|
||||
function __call($name, array $arguments) {
|
||||
global $SkinState;
|
||||
|
||||
$already_defined = isset($SkinState->svg_defs[$name]);
|
||||
if (!array_is_list($arguments)) {
|
||||
$in_place = isset($arguments['in_place']) && $arguments['in_place'] === true;
|
||||
$preload_symbol = isset($arguments['preload_symbol']) && $arguments['preload_symbol'] === true;
|
||||
} else {
|
||||
$in_place = false;
|
||||
$preload_symbol = false;
|
||||
}
|
||||
|
||||
if ($already_defined && $preload_symbol)
|
||||
return null;
|
||||
|
||||
if ($in_place || !$already_defined) {
|
||||
if (!preg_match_all('/\d+/', $name, $matches))
|
||||
throw new InvalidArgumentException('icon name '.$name.' is invalid, it should follow following pattern: $name_$size[_$size]');
|
||||
$size = array_slice($matches[0], -2);
|
||||
$width = $size[0];
|
||||
$height = $size[1] ?? $size[0];
|
||||
}
|
||||
|
||||
if (!$in_place && (!$already_defined || $preload_symbol)) {
|
||||
$SkinState->svg_defs[$name] = [
|
||||
'svg' => parent::__call($name, !$preload_symbol ? $arguments : []),
|
||||
'width' => $width,
|
||||
'height' => $height
|
||||
];
|
||||
}
|
||||
|
||||
if ($preload_symbol)
|
||||
return null;
|
||||
|
||||
if ($already_defined && !isset($width)) {
|
||||
$width = $SkinState->svg_defs[$name]['width'];
|
||||
$height = $SkinState->svg_defs[$name]['height'];
|
||||
}
|
||||
|
||||
if ($in_place) {
|
||||
$content = parent::__call($name, []);
|
||||
return <<<SVG
|
||||
<svg id="svgicon_{$name}" width="{$width}" height="{$height}" fill="currentColor" viewBox="0 0 {$width} {$height}">{$content}</svg>
|
||||
SVG;
|
||||
} else {
|
||||
return <<<SVG
|
||||
<svg width="{$width}" height="{$height}"><use xlink:href="#svgicon_{$name}"></use></svg>
|
||||
SVG;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function skin($name): SkinContext {
|
||||
static $cache = [];
|
||||
if (!isset($cache[$name])) {
|
||||
$cache[$name] = new SkinContext('\\skin\\'.$name);
|
||||
}
|
||||
return $cache[$name];
|
||||
}
|
||||
|
||||
function svg(): SVGSkinContext {
|
||||
static $svg = null;
|
||||
if ($svg === null)
|
||||
$svg = new SVGSkinContext();
|
||||
return $svg;
|
||||
}
|
||||
|
||||
enum SkinStringModificationType {
|
||||
case RAW;
|
||||
|
104
engine/sphinx.php
Normal file
104
engine/sphinx.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
function sphinx_execute(string $sql) {
|
||||
$link = _sphinxql_link();
|
||||
|
||||
if (func_num_args() > 1) {
|
||||
$mark_count = substr_count($sql, '?');
|
||||
$positions = array();
|
||||
$last_pos = -1;
|
||||
for ($i = 0; $i < $mark_count; $i++) {
|
||||
$last_pos = strpos($sql, '?', $last_pos + 1);
|
||||
$positions[] = $last_pos;
|
||||
}
|
||||
for ($i = $mark_count - 1; $i >= 0; $i--) {
|
||||
$arg = func_get_arg($i + 1);
|
||||
if (is_string($arg))
|
||||
$arg = _sphinx_normalize($arg);
|
||||
$v = '\''.$link->real_escape_string($arg).'\'';
|
||||
$sql = substr_replace($sql, $v, $positions[$i], 1);
|
||||
}
|
||||
}
|
||||
|
||||
$q = $link->query($sql);
|
||||
|
||||
$error = sphinx_error();
|
||||
if ($error)
|
||||
logError(__FUNCTION__, $error);
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
function sphinx_error() {
|
||||
$link = _sphinxql_link(auto_create: false);
|
||||
return $link?->error;
|
||||
}
|
||||
|
||||
function sphinx_mkquery($q, array $opts = []) {
|
||||
$defaults = [
|
||||
'any_word' => false,
|
||||
'star' => false,
|
||||
'and' => false,
|
||||
'exact_first' => false
|
||||
];
|
||||
$opts = array_merge($defaults, $opts);
|
||||
$q = preg_replace('/\s+/', ' ', $q);
|
||||
$q = _sphinx_normalize($q);
|
||||
$q = trim($q);
|
||||
$q = sphinx_client()->escapeString($q);
|
||||
if ($opts['star']) {
|
||||
$words = explode(' ', $q);
|
||||
$words = array_map(fn($word) => $word.'*', $words);
|
||||
$q = implode(' ', $words);
|
||||
}
|
||||
if ($opts['any_word']) {
|
||||
$q = str_replace(' ', ' | ', $q);
|
||||
} else if ($opts['and']) {
|
||||
$q = str_replace(' ', ' AND ', $q);
|
||||
}
|
||||
if ($opts['exact_first']) {
|
||||
$q = '"^'.$q.'$" | "'.$q.'" | ('.$q.')';
|
||||
}
|
||||
return $q;
|
||||
}
|
||||
|
||||
function sphinx_client(): Sphinx\SphinxClient {
|
||||
static $cl = null;
|
||||
if (!is_null($cl))
|
||||
return $cl;
|
||||
return $cl = new Sphinx\SphinxClient;
|
||||
}
|
||||
|
||||
function _sphinx_normalize(string $origstr): string {
|
||||
$buf = preg_replace('/[Ёё]/iu', 'е', $origstr);
|
||||
if (!pcre_check_error($buf, no_error: true)) {
|
||||
$origstr = mb_convert_encoding($origstr, 'UTF-8', 'UTF-8');
|
||||
$buf = preg_replace('/[Ёё]/iu', 'е', $origstr);
|
||||
pcre_check_error($buf);
|
||||
}
|
||||
if ($buf === null) {
|
||||
logError(__METHOD__.': preg_replace() failed with error: '.preg_last_error().': '.preg_last_error_msg());
|
||||
$buf = $origstr;
|
||||
}
|
||||
return preg_replace('/[!\?]/', '', $buf);
|
||||
}
|
||||
|
||||
function _sphinxql_link($auto_create = true) {
|
||||
global $config;
|
||||
|
||||
/** @var ?mysqli $link */
|
||||
static $link = null;
|
||||
if (!is_null($link) || !$auto_create)
|
||||
return $link;
|
||||
|
||||
$link = new mysqli();
|
||||
$link->real_connect(
|
||||
$config['sphinx']['host'],
|
||||
ini_get('mysql.default_user'),
|
||||
ini_get('mysql.default_password'),
|
||||
null,
|
||||
9306);
|
||||
$link->set_charset('utf8');
|
||||
|
||||
return $link;
|
||||
}
|
@ -37,16 +37,6 @@ function htmlescape(string|array $s): string|array {
|
||||
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function strtrim(string $str, int $len, bool &$trimmed): string {
|
||||
if (mb_strlen($str) > $len) {
|
||||
$str = mb_substr($str, 0, $len);
|
||||
$trimmed = true;
|
||||
} else {
|
||||
$trimmed = false;
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
function sizeString(int $size): string {
|
||||
$ks = array('B', 'KiB', 'MiB', 'GiB');
|
||||
foreach ($ks as $i => $k) {
|
||||
@ -295,6 +285,10 @@ function lang() {
|
||||
global $__lang;
|
||||
return call_user_func_array([$__lang, 'get'], func_get_args());
|
||||
}
|
||||
function lang_num() {
|
||||
global $__lang;
|
||||
return call_user_func_array([$__lang, 'num'], func_get_args());
|
||||
}
|
||||
|
||||
function is_dev(): bool { global $config; return $config['is_dev']; }
|
||||
function is_cli(): bool { return PHP_SAPI == 'cli'; };
|
||||
@ -302,3 +296,70 @@ function is_retina(): bool { return isset($_COOKIE['is_retina']) && $_COOKIE['is
|
||||
|
||||
function jsonEncode($obj): ?string { return json_encode($obj, JSON_UNESCAPED_UNICODE) ?: null; }
|
||||
function jsonDecode($json) { return json_decode($json, true); }
|
||||
|
||||
function pcre_check_error(mixed &$result, bool $no_error = false): bool {
|
||||
if ($result === null) {
|
||||
if (preg_last_error() !== PREG_NO_ERROR) {
|
||||
if (!$no_error)
|
||||
logError('an error occurred while PCRE regex execution: '.preg_last_error_msg());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $s
|
||||
* @param string|string[]|null $keywords
|
||||
* @return string
|
||||
*/
|
||||
function hl_matched(string $s, string|array|null $keywords = []): string {
|
||||
if (is_null($keywords))
|
||||
return htmlescape($s);
|
||||
|
||||
if (is_string($keywords))
|
||||
$keywords = preg_split('/\s+/', $keywords);
|
||||
|
||||
$charset = 'utf-8';
|
||||
$s_len = mb_strlen($s, $charset);
|
||||
|
||||
usort($keywords, function($a, $b) use (&$charset) {
|
||||
return mb_strlen($b, $charset) - mb_strlen($a, $charset);
|
||||
});
|
||||
|
||||
$all = [];
|
||||
foreach ($keywords as $kw) {
|
||||
$kw_len = mb_strlen($kw, $charset);
|
||||
$offset = 0;
|
||||
while (($pos = mb_stripos($s, $kw, $offset, $charset)) !== false) {
|
||||
$offset = $pos + 1;
|
||||
$all[] = [$pos, $kw_len];
|
||||
}
|
||||
}
|
||||
|
||||
usort($all, function($a, $b) {
|
||||
return $a[0] - $b[0];
|
||||
});
|
||||
|
||||
$last_index = 0;
|
||||
$buf = '';
|
||||
foreach ($all as $range) {
|
||||
list($start_pos, $len) = $range;
|
||||
if ($start_pos < $last_index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($start_pos > $last_index) {
|
||||
$buf .= htmlescape(mb_substr($s, $last_index, $start_pos - $last_index, $charset));
|
||||
}
|
||||
|
||||
$buf .= '<span class="matched">'.htmlescape(mb_substr($s, $start_pos, $len, $charset)).'</span>';
|
||||
$last_index = $start_pos + $len;
|
||||
}
|
||||
|
||||
if ($last_index < $s_len) {
|
||||
$buf .= htmlescape(mb_substr($s, $last_index, $s_len - $last_index, $charset));
|
||||
}
|
||||
|
||||
return $buf;
|
||||
}
|
@ -132,7 +132,7 @@ class AdminHandler extends request_handler {
|
||||
ensure_xhr();
|
||||
list($md, $title, $use_image_previews) = input('md, title, b:use_image_previews');
|
||||
$html = markup::markdownToHtml($md, $use_image_previews);
|
||||
$ctx = new SkinContext('\\skin\\admin');
|
||||
$ctx = skin('admin');
|
||||
$html = $ctx->markdownPreview(
|
||||
unsafe_html: $html,
|
||||
title: $title
|
||||
@ -480,7 +480,11 @@ class AdminHandler extends request_handler {
|
||||
|
||||
admin_log(new \AdminActions\PostEdit($post->id));
|
||||
ajax_ok(['url' => $post->getUrl().'edit/?saved=1&lang='.$lang->value]);
|
||||
}
|
||||
|
||||
function GET_books() {
|
||||
set_title('$admin_books');
|
||||
render('admin/books');
|
||||
}
|
||||
|
||||
protected static function _postEditValidateCommonData($date) {
|
||||
|
133
handler/FilesHandler.php
Normal file
133
handler/FilesHandler.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
require_once 'lib/files.php';
|
||||
|
||||
class FilesHandler extends request_handler {
|
||||
|
||||
const SEARCH_RESULTS_PER_PAGE = 50;
|
||||
const SEARCH_MIN_QUERY_LENGTH = 3;
|
||||
|
||||
function GET_files() {
|
||||
set_title('$files');
|
||||
set_skin_opts(['head_section' => 'files']);
|
||||
$collections = array_map(fn(FilesCollection $c) => new CollectionItem($c), FilesCollection::cases());
|
||||
$books = books_get();
|
||||
$misc = books_get(category: BookCategory::MISC);
|
||||
render('files/index',
|
||||
collections: $collections,
|
||||
books: $books,
|
||||
misc: $misc);
|
||||
}
|
||||
|
||||
function GET_folder() {
|
||||
list($folder_id) = input('i:folder_id');
|
||||
$folder = books_get_folder($folder_id);
|
||||
if (!$folder)
|
||||
not_found();
|
||||
$files = books_get($folder_id);
|
||||
set_title(lang('files').' - '.$folder->title);
|
||||
render('files/folder',
|
||||
folder: $folder,
|
||||
files: $files);
|
||||
}
|
||||
|
||||
function GET_collection() {
|
||||
list($collection, $folder_id, $query, $offset) = input('collection, i:folder_id, q, i:offset');
|
||||
$collection = FilesCollection::from($collection);
|
||||
$files = [];
|
||||
$parents = null;
|
||||
|
||||
$query = trim($query);
|
||||
if (!$query)
|
||||
$query = null;
|
||||
|
||||
add_skin_strings_re('/^files_(.*?)_collection$/');
|
||||
add_skin_strings([
|
||||
'files_search_results_count'
|
||||
]);
|
||||
|
||||
$vars = [];
|
||||
$text_excerpts = null;
|
||||
|
||||
switch ($collection) {
|
||||
case FilesCollection::WilliamFriedman:
|
||||
if ($query !== null) {
|
||||
$files = wff_search($query, $offset, self::SEARCH_RESULTS_PER_PAGE);
|
||||
$vars += [
|
||||
'search_count' => $files['count'],
|
||||
'search_query' => $query
|
||||
];
|
||||
|
||||
/** @var WFFCollectionItem[] $files */
|
||||
$files = $files['items'];
|
||||
|
||||
$query_words = array_map('mb_strtolower', preg_split('/\s+/', $query));
|
||||
$found = [];
|
||||
$result_ids = [];
|
||||
foreach ($files as $file) {
|
||||
if ($file->isFolder())
|
||||
continue;
|
||||
$result_ids[] = $file->id;
|
||||
|
||||
foreach ([
|
||||
mb_strtolower($file->getTitle()),
|
||||
strtolower($file->documentId)
|
||||
] as $haystack) {
|
||||
foreach ($query_words as $qw) {
|
||||
if (mb_strpos($haystack, $qw) !== false) {
|
||||
$found[$file->id] = true;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$found = array_map('intval', array_keys($found));
|
||||
$not_found = array_diff($result_ids, $found);
|
||||
if (!empty($not_found))
|
||||
$text_excerpts = wff_get_text_excerpts($not_found, $query_words);
|
||||
|
||||
if (is_xhr_request()) {
|
||||
ajax_ok([
|
||||
...$vars,
|
||||
'new_offset' => $offset + count($files),
|
||||
'html' => skin('files')->collection_files($files, $query, $text_excerpts)
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if ($folder_id) {
|
||||
$parents = wff_get_folder($folder_id, true);
|
||||
if (!$parents)
|
||||
not_found();
|
||||
if (count($parents) > 1)
|
||||
$parents = array_reverse($parents);
|
||||
}
|
||||
$files = wff_get($folder_id);
|
||||
}
|
||||
|
||||
$title = lang('files_wff_collection');
|
||||
if ($folder_id)
|
||||
$title .= ' - '.htmlescape($parents[count($parents)-1]->getTitle());
|
||||
if ($query)
|
||||
$title .= ' - '.htmlescape($query);
|
||||
set_title($title);
|
||||
|
||||
break;
|
||||
|
||||
case FilesCollection::MercureDeFrance:
|
||||
$files = mdf_get();
|
||||
set_title('$files_mdf_collection');
|
||||
break;
|
||||
}
|
||||
|
||||
render('files/collection',
|
||||
...$vars,
|
||||
collection: $collection,
|
||||
files: $files,
|
||||
parents: $parents,
|
||||
search_results_per_page: self::SEARCH_RESULTS_PER_PAGE,
|
||||
search_min_query_length: self::SEARCH_MIN_QUERY_LENGTH,
|
||||
text_excerpts: $text_excerpts);
|
||||
}
|
||||
|
||||
}
|
@ -123,7 +123,7 @@ class MainHandler extends request_handler {
|
||||
];
|
||||
}, is_admin() ? posts::getList(0, 20, filter_by_lang: $lang) : []);
|
||||
|
||||
$ctx = new SkinContext('\\skin\\rss');
|
||||
$ctx = skin('rss');
|
||||
$body = $ctx->atom(
|
||||
title: lang('site_title'),
|
||||
link: 'https://'.$config['domain'],
|
||||
|
@ -1,2 +0,0 @@
|
||||
var LS = window.localStorage;
|
||||
window.cur = {};
|
@ -1,32 +1,49 @@
|
||||
class Draft {
|
||||
constructor(id, lang = 'en') {
|
||||
this.id = id
|
||||
this.lang = lang
|
||||
}
|
||||
function Draft(id, lang) {
|
||||
if (!lang)
|
||||
lang = 'en'
|
||||
this.id = id
|
||||
this.lang = lang
|
||||
}
|
||||
|
||||
setLang(lang) {
|
||||
extend(Draft.prototype, {
|
||||
setLang: function(lang) {
|
||||
this.lang = lang
|
||||
}
|
||||
},
|
||||
|
||||
getForLang(lang, what) {
|
||||
getForLang: function(lang, what) {
|
||||
return LS.getItem(this.key(what, lang)) || ''
|
||||
}
|
||||
},
|
||||
|
||||
key(what, lang = null) {
|
||||
key: function(what, lang) {
|
||||
if (!lang)
|
||||
lang = null
|
||||
if (lang === null)
|
||||
lang = this.lang
|
||||
return `draft_${this.id}_${what}__${lang}`
|
||||
}
|
||||
return 'draft_'+this.id+'_'+what+'__'+lang
|
||||
},
|
||||
|
||||
reset(langs) {
|
||||
for (const what of ['title', 'text']) {
|
||||
for (const l of langs)
|
||||
LS.removeItem(this.key(what, l))
|
||||
reset: function(langs) {
|
||||
var types = ['title', 'text'];
|
||||
for (var i = 0; i < types.length; i++) {
|
||||
var what = types[i];
|
||||
for (var j = 0; j < langs.length; j++)
|
||||
LS.removeItem(this.key(what, langs[i]));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get title() { return LS.getItem(this.key('title')) || '' }
|
||||
get text() { return LS.getItem(this.key('text')) || '' }
|
||||
set title(val) { LS.setItem(this.key('title'), val) }
|
||||
set text(val) { LS.setItem(this.key('text'), val) }
|
||||
}
|
||||
get: function(what) {
|
||||
return what === 'title' ? this.getTitle() : this.getText()
|
||||
},
|
||||
|
||||
set: function(what, val) {
|
||||
if (what === 'title')
|
||||
this.setTitle(val)
|
||||
else
|
||||
this.setText(val)
|
||||
},
|
||||
|
||||
getTitle: function() { return LS.getItem(this.key('title')) || '' },
|
||||
getText: function() { return LS.getItem(this.key('text')) || '' },
|
||||
setTitle: function(val) { LS.setItem(this.key('title'), val) },
|
||||
setText: function(val) { LS.setItem(this.key('text'), val) }
|
||||
});
|
@ -1,105 +1,101 @@
|
||||
class AdminWriteEditForm {
|
||||
constructor(opts = {}) {
|
||||
this.opts = opts
|
||||
this.form = document.forms[this.isPage() ? 'pageForm' : 'postForm']
|
||||
this.isFixed = false
|
||||
this.previewTimeout = null
|
||||
this.previewRequest = null
|
||||
this.tocByLang = {}
|
||||
function AdminWriteEditForm(opts) {
|
||||
bindEvents(this);
|
||||
opts = opts || {};
|
||||
|
||||
if (!this.isEditing() && this.isPost()) {
|
||||
for (const l of opts.langs) {
|
||||
this.tocByLang[l] = false
|
||||
}
|
||||
this.opts = opts;
|
||||
this.form = document.forms[this.isPage() ? 'pageForm' : 'postForm'];
|
||||
this.previewTimeout = null;
|
||||
this.previewRequest = null;
|
||||
this.tocByLang = {};
|
||||
|
||||
if (!this.isEditing() && this.isPost()) {
|
||||
for (var i = 0; i < opts.langs.length; i++) {
|
||||
this.tocByLang[opts.langs[i]] = false;
|
||||
}
|
||||
|
||||
this.form.addEventListener('submit', this.onSubmit)
|
||||
this.form.title.addEventListener('input', this.onInput)
|
||||
this.form.text.addEventListener('input', this.onInput)
|
||||
ge('toggle_wrap').addEventListener('click', this.onToggleWrapClick)
|
||||
|
||||
if (this.isPost()) {
|
||||
ge('toc_cb').addEventListener('change', (e) => {
|
||||
this.tocByLang[this.getCurrentLang()] = e.target.checked
|
||||
})
|
||||
}
|
||||
|
||||
let lang = 'en'
|
||||
if (this.isPost()) {
|
||||
lang = this.getCurrentLang()
|
||||
this.form.lang.addEventListener('change', (e) => {
|
||||
let newLang = e.target.options[e.target.selectedIndex].value
|
||||
this.draft.setLang(newLang)
|
||||
this.fillFromDraft({applyEvenEmpty: true})
|
||||
ge('toc_cb').checked = this.tocByLang[newLang]
|
||||
})
|
||||
}
|
||||
|
||||
let draftId, draftType = !this.isPost() ? 'page' : 'post'
|
||||
if (this.isEditing()) {
|
||||
draftId = `edit_${draftType}${this.opts.id}`
|
||||
} else {
|
||||
draftId = `new_${draftType}`
|
||||
}
|
||||
|
||||
this.draft = new Draft(draftId, lang);
|
||||
if (this.isEditing()) {
|
||||
if (this.isPost()) {
|
||||
for (let l in opts.texts) {
|
||||
this.draft.setLang(l)
|
||||
this.draft.title = opts.texts[l].title
|
||||
this.draft.text = opts.texts[l].md
|
||||
this.tocByLang[l] = opts.texts[l].toc
|
||||
}
|
||||
this.draft.setLang(lang)
|
||||
} else {
|
||||
this.draft.title = opts.text.title
|
||||
this.draft.text = opts.text.text
|
||||
}
|
||||
this.showPreview()
|
||||
} else {
|
||||
this.fillFromDraft()
|
||||
}
|
||||
|
||||
// window.addEventListener('scroll', this.onScroll)
|
||||
// window.addEventListener('resize', this.onResize)
|
||||
}
|
||||
|
||||
getCurrentLang() { return this.form.lang.options[this.form.lang.selectedIndex].value }
|
||||
isPost() { return !this.opts.pages }
|
||||
isPage() { return !!this.opts.pages }
|
||||
isEditing() { return !!this.opts.edit }
|
||||
this.form.addEventListener('submit', this.onSubmit);
|
||||
this.form.title.addEventListener('input', this.onInput);
|
||||
this.form.text.addEventListener('input', this.onInput);
|
||||
ge('toggle_wrap').addEventListener('click', this.onToggleWrapClick);
|
||||
|
||||
fillFromDraft({applyEvenEmpty} = {applyEvenEmpty: false}) {
|
||||
for (const what of ['title', 'text']) {
|
||||
if (this.draft[what] !== '' || applyEvenEmpty)
|
||||
this.form[what].value = this.draft[what]
|
||||
}
|
||||
|
||||
if (this.form.text.value !== '' || applyEvenEmpty)
|
||||
this.showPreview()
|
||||
if (this.isPost()) {
|
||||
ge('toc_cb').addEventListener('change', this.onToCCheckboxChange);
|
||||
}
|
||||
|
||||
showPreview() {
|
||||
var lang = 'en';
|
||||
if (this.isPost()) {
|
||||
lang = this.getCurrentLang();
|
||||
this.form.lang.addEventListener('change', this.onLangChanged);
|
||||
}
|
||||
|
||||
var draftId, draftType = !this.isPost() ? 'page' : 'post';
|
||||
if (this.isEditing()) {
|
||||
draftId = 'edit_'+draftType+this.opts.id;
|
||||
} else {
|
||||
draftId = 'new_'+draftType;
|
||||
}
|
||||
|
||||
this.draft = new Draft(draftId, lang);
|
||||
if (this.isEditing()) {
|
||||
if (this.isPost()) {
|
||||
for (var l in opts.texts) {
|
||||
this.draft.setLang(l)
|
||||
this.draft.setTitle(opts.texts[l].title)
|
||||
this.draft.setText(opts.texts[l].md)
|
||||
this.tocByLang[l] = opts.texts[l].toc
|
||||
}
|
||||
this.draft.setLang(lang)
|
||||
} else {
|
||||
this.draft.setTitle(opts.text.title)
|
||||
this.draft.setText(opts.text.text)
|
||||
}
|
||||
this.showPreview()
|
||||
} else {
|
||||
this.fillFromDraft()
|
||||
}
|
||||
}
|
||||
|
||||
extend(AdminWriteEditForm.prototype, {
|
||||
getCurrentLang: function () {
|
||||
return this.form.lang.options[this.form.lang.selectedIndex].value;
|
||||
},
|
||||
isPost: function() { return !this.opts.pages },
|
||||
isPage: function() { return !!this.opts.pages },
|
||||
isEditing: function() { return !!this.opts.edit },
|
||||
|
||||
fillFromDraft: function(opts) {
|
||||
opts = opts || {applyEventEmpty: false};
|
||||
var whats = ['title', 'text'];
|
||||
for (var i = 0; i < whats.length; i++) {
|
||||
var what = whats[i];
|
||||
if (this.draft.get(what) !== '' || opts.applyEvenEmpty)
|
||||
this.form[what].value = this.draft.get(what);
|
||||
}
|
||||
if (this.form.text.value !== '' || opts.applyEvenEmpty)
|
||||
this.showPreview();
|
||||
},
|
||||
|
||||
showPreview: function() {
|
||||
if (this.previewRequest !== null)
|
||||
this.previewRequest.abort()
|
||||
const params = {
|
||||
this.previewRequest.abort();
|
||||
var params = {
|
||||
md: this.form.elements.text.value,
|
||||
use_image_previews: this.isPage() ? 1 : 0
|
||||
}
|
||||
};
|
||||
if (this.isPost())
|
||||
params.title = this.form.elements.title.value
|
||||
this.previewRequest = ajax.post('/admin/markdown-preview.ajax', params, (err, response) => {
|
||||
params.title = this.form.elements.title.value;
|
||||
this.previewRequest = ajax.post('/admin/markdown-preview.ajax', params, function(err, response) {
|
||||
if (err)
|
||||
return console.error(err)
|
||||
ge('preview_html').innerHTML = response.html
|
||||
})
|
||||
}
|
||||
return console.error(err);
|
||||
ge('preview_html').innerHTML = response.html;
|
||||
});
|
||||
},
|
||||
|
||||
showError(code, message) {
|
||||
showError: function(code, message) {
|
||||
if (code) {
|
||||
let el = ge('form-error')
|
||||
let label = escape(lang(`err_blog_${code}`))
|
||||
var el = ge('form-error')
|
||||
var label = escape(lang('err_blog_'+code))
|
||||
if (message)
|
||||
label += ' (' + message + ')'
|
||||
el.innerHTML = label
|
||||
@ -107,165 +103,129 @@ class AdminWriteEditForm {
|
||||
} else if (message) {
|
||||
alert(lang('error')+': '+message)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
hideError() {
|
||||
let el = ge('form-error')
|
||||
hideError: function() {
|
||||
var el = ge('form-error')
|
||||
el.style.display = 'none'
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit = (evt) => {
|
||||
const fields = []
|
||||
onSubmit: function(evt) {
|
||||
var fields = []
|
||||
try {
|
||||
if (this.isEditing()) {
|
||||
fields.push('new_short_name');
|
||||
} else {
|
||||
fields.push('short_name');
|
||||
}
|
||||
for (const field of fields) {
|
||||
fields.forEach(function(field) {
|
||||
if (evt.target.elements[field].value.trim() === '')
|
||||
throw 'no_'+field
|
||||
}
|
||||
})
|
||||
|
||||
const fd = new FormData()
|
||||
for (const f of fields) {
|
||||
console.log(`field: ${f}`)
|
||||
fd.append(f, evt.target[f].value.trim())
|
||||
}
|
||||
var fd = new FormData();
|
||||
fields.forEach(function(f) {
|
||||
console.log('field: ' + f)
|
||||
fd.append(f, evt.target[f].value.trim());
|
||||
})
|
||||
|
||||
// fd.append('lang', this.getCurrentLang())
|
||||
if (this.isPost() || this.isEditing())
|
||||
fd.append('visible', ge('visible_cb').checked ? 1 : 0)
|
||||
fd.append('visible', ge('visible_cb').checked ? 1 : 0);
|
||||
|
||||
// text-specific fields
|
||||
let atLeastOneLangIsWritten = false
|
||||
const writtenLangs = []
|
||||
var atLeastOneLangIsWritten = false;
|
||||
var writtenLangs = [];
|
||||
if (this.isPost()) {
|
||||
for (const l of this.opts.langs) {
|
||||
const title = this.draft.getForLang(l, 'title')
|
||||
const text = this.draft.getForLang(l, 'text')
|
||||
console.log(`lang: ${l}`, title, text)
|
||||
if (title !== '' && text !== '')
|
||||
atLeastOneLangIsWritten = true
|
||||
fd.append(`title:${l}`, title)
|
||||
fd.append(`text:${l}`, text)
|
||||
fd.append(`toc:${l}`, this.tocByLang[l] ? 1 : 0)
|
||||
writtenLangs.push(l)
|
||||
}
|
||||
this.opts.langs.forEach(function(l) {
|
||||
var title = this.draft.getForLang(l, 'title');
|
||||
var text = this.draft.getForLang(l, 'text');
|
||||
if (title !== '' && text !== '') {
|
||||
atLeastOneLangIsWritten = true;
|
||||
fd.append('title:' + l, title);
|
||||
fd.append('text:' + l, text);
|
||||
fd.append('toc:' + l, this.tocByLang[l] ? 1 : 0);
|
||||
writtenLangs.push(l);
|
||||
}
|
||||
}.bind(this))
|
||||
} else {
|
||||
const title = this.draft.title
|
||||
const text = this.draft.text
|
||||
var title = this.draft.getTitle()
|
||||
var text = this.draft.getText()
|
||||
if (title !== '' && text !== '') {
|
||||
atLeastOneLangIsWritten = true
|
||||
fd.append('title', title)
|
||||
fd.append('text', text)
|
||||
atLeastOneLangIsWritten = true;
|
||||
fd.append('title', title);
|
||||
fd.append('text', text);
|
||||
}
|
||||
}
|
||||
|
||||
if (!atLeastOneLangIsWritten)
|
||||
throw 'no_text'
|
||||
|
||||
fd.append('langs', writtenLangs.join(','))
|
||||
fd.append('langs', writtenLangs.join(','));
|
||||
|
||||
// date field
|
||||
if (this.isPost()) {
|
||||
const dateInput = evt.target.elements.date;
|
||||
var dateInput = evt.target.elements.date;
|
||||
if (!dateInput.value)
|
||||
throw 'no_date'
|
||||
fd.append('date', dateInput.value)
|
||||
fd.append('date', dateInput.value);
|
||||
}
|
||||
|
||||
fd.append('token', this.opts.token)
|
||||
cancelEvent(evt)
|
||||
fd.append('token', this.opts.token);
|
||||
cancelEvent(evt);
|
||||
|
||||
this.hideError();
|
||||
|
||||
ajax.post(evt.target.action, fd, (error, response) => {
|
||||
ajax.post(evt.target.action, fd, function(error, response) {
|
||||
if (error) {
|
||||
this.showError(error.code, error.message)
|
||||
this.showError(error.code, error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.url) {
|
||||
this.draft.reset(this.opts.langs);
|
||||
window.location = response.url
|
||||
window.location = response.url;
|
||||
}
|
||||
})
|
||||
}.bind(this));
|
||||
} catch (e) {
|
||||
const errorText = typeof e == 'string' ? lang('error')+': '+lang((this.isPage() ? 'err_pages_' : 'err_blog_')+e) : e.message;
|
||||
var errorText = typeof e == 'string' ? lang('error')+': '+lang((this.isPage() ? 'err_pages_' : 'err_blog_')+e) : e.message;
|
||||
alert(errorText);
|
||||
console.error(e);
|
||||
return cancelEvent(evt);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onToggleWrapClick = (e) => {
|
||||
const textarea = this.form.elements.text
|
||||
onToggleWrapClick: function(e) {
|
||||
var textarea = this.form.elements.text
|
||||
if (!hasClass(textarea, 'nowrap')) {
|
||||
addClass(textarea, 'nowrap')
|
||||
addClass(textarea, 'nowrap');
|
||||
} else {
|
||||
removeClass(textarea, 'nowrap')
|
||||
removeClass(textarea, 'nowrap');
|
||||
}
|
||||
return cancelEvent(e)
|
||||
}
|
||||
return cancelEvent(e);
|
||||
},
|
||||
|
||||
onInput = (e) => {
|
||||
onInput: function(e) {
|
||||
if (this.previewTimeout !== null)
|
||||
clearTimeout(this.previewTimeout);
|
||||
|
||||
this.previewTimeout = setTimeout(() => {
|
||||
this.previewTimeout = setTimeout(function() {
|
||||
this.previewTimeout = null;
|
||||
this.showPreview();
|
||||
|
||||
const what = e.target.name === 'title' ? 'title' : 'text'
|
||||
this.draft[what] = e.target.value
|
||||
}, 300)
|
||||
var what = e.target.name === 'title' ? 'title' : 'text';
|
||||
this.draft.set(what, e.target.value);
|
||||
}.bind(this), 300);
|
||||
},
|
||||
|
||||
onLangChanged: function(e) {
|
||||
var newLang = e.target.options[e.target.selectedIndex].value;
|
||||
this.draft.setLang(newLang);
|
||||
this.fillFromDraft({applyEvenEmpty: true});
|
||||
ge('toc_cb').checked = this.tocByLang[newLang];
|
||||
},
|
||||
|
||||
onToCCheckboxChange: function(e) {
|
||||
this.tocByLang[this.getCurrentLang()] = e.target.checked;
|
||||
}
|
||||
|
||||
onScroll = () => {
|
||||
var ANCHOR_TOP = 10;
|
||||
|
||||
var y = window.pageYOffset;
|
||||
var form = this.form;
|
||||
var td = ge('form_first_cell');
|
||||
var ph = ge('form_placeholder');
|
||||
|
||||
var rect = td.getBoundingClientRect();
|
||||
|
||||
if (rect.top <= ANCHOR_TOP && !this.isFixed) {
|
||||
ph.style.height = form.getBoundingClientRect().height+'px';
|
||||
|
||||
var w = (rect.width - (parseInt(getComputedStyle(td).paddingRight, 10) || 0));
|
||||
form.style.display = 'block';
|
||||
form.style.width = w+'px';
|
||||
form.style.position = 'fixed';
|
||||
form.style.top = ANCHOR_TOP+'px';
|
||||
|
||||
this.isFixed = true;
|
||||
} else if (rect.top > ANCHOR_TOP && this.isFixed) {
|
||||
form.style.display = '';
|
||||
form.style.width = '';
|
||||
form.style.position = '';
|
||||
form.style.position = '';
|
||||
ph.style.height = '';
|
||||
|
||||
this.isFixed = false;
|
||||
}
|
||||
}
|
||||
|
||||
onResize = () => {
|
||||
if (!this.isFixed)
|
||||
return
|
||||
|
||||
const form = this.form
|
||||
const td = ge('form_first_cell')
|
||||
const ph = ge('form_placeholder')
|
||||
|
||||
const rect = td.getBoundingClientRect()
|
||||
const pr = parseInt(getComputedStyle(td).paddingRight, 10) || 0
|
||||
|
||||
ph.style.height = form.getBoundingClientRect().height+'px'
|
||||
form.style.width = (rect.width - pr) + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
// bindEventHandlers(AdminWriteForm);
|
||||
})
|
||||
|
2
htdocs/js/common/00-common.js
Normal file
2
htdocs/js/common/00-common.js
Normal file
@ -0,0 +1,2 @@
|
||||
window.LS = window.localStorage;
|
||||
window.cur = {};
|
@ -44,4 +44,29 @@ if (!Object.assign) {
|
||||
return to;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(function(window) {
|
||||
var lastTime = 0;
|
||||
var vendors = ['ms', 'moz', 'webkit', 'o'];
|
||||
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
|
||||
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
|
||||
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|
||||
|| window[vendors[x]+'CancelRequestAnimationFrame'];
|
||||
}
|
||||
|
||||
if (!window.requestAnimationFrame)
|
||||
window.requestAnimationFrame = function(callback, element) {
|
||||
var currTime = new Date().getTime();
|
||||
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
||||
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
|
||||
timeToCall);
|
||||
lastTime = currTime + timeToCall;
|
||||
return id;
|
||||
};
|
||||
|
||||
if (!window.cancelAnimationFrame)
|
||||
window.cancelAnimationFrame = function(id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}(window));
|
37
htdocs/js/common/01-logger.js
Normal file
37
htdocs/js/common/01-logger.js
Normal file
@ -0,0 +1,37 @@
|
||||
function getLogger(moduleName) {
|
||||
var levels = ['log', 'info', 'warning', 'warn', 'debug', 'error'];
|
||||
|
||||
if (!window.console) {
|
||||
var noop = function() {};
|
||||
var result = {func: function() { return this; }};
|
||||
levels.forEach(function(level) {
|
||||
result[level] = noop;
|
||||
})
|
||||
}
|
||||
|
||||
function logWithPrefix(prefix, method, args) {
|
||||
var message = Array.prototype.slice.call(args);
|
||||
message.unshift('[' + prefix + ']:');
|
||||
console[method].apply(console, message);
|
||||
}
|
||||
|
||||
var logger = {};
|
||||
|
||||
levels.forEach(function(level) {
|
||||
logger[level] = function() {
|
||||
logWithPrefix(moduleName, level === 'warning' ? 'warn' : level, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
logger.local = function(funcName) {
|
||||
var funcLogger = {};
|
||||
levels.forEach(function(level) {
|
||||
funcLogger[level] = function() {
|
||||
logWithPrefix(moduleName + '/' + funcName, level === 'warning' ? 'warn' : level, arguments);
|
||||
};
|
||||
});
|
||||
return funcLogger;
|
||||
};
|
||||
|
||||
return logger;
|
||||
}
|
@ -73,6 +73,8 @@
|
||||
opts = Object.assign({}, defaultOpts, opts);
|
||||
|
||||
var xhr = createXMLHttpRequest();
|
||||
var aborted = false;
|
||||
|
||||
xhr.open(method, url);
|
||||
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
@ -81,7 +83,7 @@
|
||||
|
||||
var callbackFired = false;
|
||||
xhr.onreadystatechange = function() {
|
||||
if (callbackFired)
|
||||
if (callbackFired || aborted)
|
||||
return
|
||||
|
||||
if (xhr.readyState === 4) {
|
||||
@ -102,12 +104,22 @@
|
||||
};
|
||||
|
||||
xhr.onerror = function(e) {
|
||||
if (aborted)
|
||||
return
|
||||
callback(e, null, 0);
|
||||
};
|
||||
|
||||
xhr.send(method === 'GET' ? null : data);
|
||||
|
||||
return xhr;
|
||||
return {
|
||||
xhr: xhr,
|
||||
abort: function() {
|
||||
aborted = true;
|
||||
try {
|
||||
xhr.abort();
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.ajax = {
|
||||
@ -116,3 +128,41 @@
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
//
|
||||
// History API polyfill
|
||||
//
|
||||
(function() {
|
||||
var supportsHistoryAPI = window.history && window.history.pushState && window.history.replaceState;
|
||||
|
||||
function redirectToHashbangPath() {
|
||||
if (!supportsHistoryAPI) {
|
||||
var hash = window.location.hash;
|
||||
if (hash.startsWith("#!")) {
|
||||
var path = hash.substring(2); // Remove the '#!' to get the path
|
||||
window.location.replace(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!supportsHistoryAPI) {
|
||||
var originalTitle = document.title;
|
||||
window.history.pushState = function(state, title, url) {
|
||||
var path = url.substring(url.indexOf("/", url.indexOf("//") + 2));
|
||||
sessionStorage.setItem('!' + path, JSON.stringify(state));
|
||||
window.location.hash = '!' + path;
|
||||
document.title = title || originalTitle;
|
||||
};
|
||||
window.history.replaceState = function(state, title, url) {
|
||||
var path = url.substring(url.indexOf("/", url.indexOf("//") + 2));
|
||||
sessionStorage.setItem('!' + path, JSON.stringify(state));
|
||||
var currentUrlWithoutHash = window.location.href.split('#')[0];
|
||||
window.location.replace(currentUrlWithoutHash + '#!' + path);
|
||||
document.title = title || originalTitle;
|
||||
};
|
||||
window.addEventListener("hashchange", redirectToHashbangPath, false);
|
||||
}
|
||||
|
||||
// Initial check for redirect only if the browser does not support History API
|
||||
redirectToHashbangPath();
|
||||
})();
|
@ -1,80 +1,96 @@
|
||||
//
|
||||
// DOM helpers
|
||||
//
|
||||
function ge(id) {
|
||||
(function(window) {
|
||||
var logger = getLogger('03-dom.js')
|
||||
|
||||
window.ge = function ge(id) {
|
||||
return document.getElementById(id)
|
||||
}
|
||||
|
||||
function hasClass(el, name) {
|
||||
window.re = function re(el) {
|
||||
if (typeof el === 'string')
|
||||
el = ge(el);
|
||||
if (el && el.parentNode) {
|
||||
el.parentNode.removeChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
window.ce = function ce(tagName, attr, style) {
|
||||
if (arguments.length === 1 && typeof tagName == 'string') {
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = tagName;
|
||||
return div.firstChild;
|
||||
}
|
||||
|
||||
var el = document.createElement(tagName);
|
||||
if (attr) {
|
||||
extend(el, attr);
|
||||
}
|
||||
if (style) {
|
||||
setStyle(el, style);
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
window.insertAfter = function insertAfter(elem, refElem) {
|
||||
return refElem.parentNode.insertBefore(elem, refElem.nextSibling);
|
||||
}
|
||||
|
||||
window.show = function show(el, dsp) {
|
||||
if (typeof el === 'string')
|
||||
el = ge(el);
|
||||
if (!el)
|
||||
return logger.local('show').error('invalid element:', el)
|
||||
if (!dsp)
|
||||
dsp = el.tagName === 'SPAN' || el.tagName === 'A' ? 'inline' : 'block';
|
||||
el.style.display = dsp;
|
||||
}
|
||||
|
||||
window.hide = function hide(el) {
|
||||
if (typeof el === 'string')
|
||||
el = ge(el);
|
||||
if (!el)
|
||||
return logger.local('hide').error('invalid element:', el)
|
||||
el.style.display = 'none';
|
||||
}
|
||||
|
||||
window.visible = function visible(el) {
|
||||
return getStyle(el, 'display') !== 'none';
|
||||
}
|
||||
|
||||
window.toggle = function toggle(el) {
|
||||
visible(el) ? hide(el) : show(el)
|
||||
}
|
||||
|
||||
window.hasClass = function hasClass(el, name) {
|
||||
if (typeof el === 'string')
|
||||
el = ge(el);
|
||||
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)) {
|
||||
window.addClass = function addClass(el, name) {
|
||||
if (typeof el === 'string')
|
||||
el = ge(el);
|
||||
if (!el)
|
||||
return logger.local('addClass').warn('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)
|
||||
}
|
||||
window.removeClass = function removeClass(el, name) {
|
||||
if (typeof el === 'string')
|
||||
el = ge(el);
|
||||
if (!el)
|
||||
return logger.local('removeClass').warn('el is', el)
|
||||
if (isArray(name)) {
|
||||
for (var i = 0; i < name.length; i++) {
|
||||
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)
|
||||
}
|
||||
window.cancelEvent = function cancelEvent(evt) {
|
||||
if (!evt)
|
||||
return logger.local('cancelEvent').warn('cancelEvent: event is', evt)
|
||||
|
||||
if (evt.preventDefault) evt.preventDefault();
|
||||
if (evt.stopPropagation) evt.stopPropagation();
|
||||
@ -85,32 +101,55 @@ function cancelEvent(evt) {
|
||||
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();
|
||||
window.setStyle = function setStyle(el, name, value) {
|
||||
if (typeof el === 'string')
|
||||
el = ge(el);
|
||||
|
||||
if (Array.isArray(el)) {
|
||||
return el.forEach(function(el, i) {
|
||||
setStyle(el, name, value);
|
||||
});
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; domain=" + window.appConfig.cookieHost + "; path=/";
|
||||
|
||||
if (isObject(name)) {
|
||||
for (var k in name) {
|
||||
var v = name[k];
|
||||
setStyle(el, k, v);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === 'number'
|
||||
&& name !== 'z-index'
|
||||
&& ! /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i.test(name))
|
||||
value += 'px';
|
||||
|
||||
el.style[toCamelCase(name)] = value;
|
||||
}
|
||||
|
||||
function unsetCookie(name) {
|
||||
document.cookie = name + '=; Max-Age=-99999999; domain=' + window.appConfig.cookieHost + "; path=/";
|
||||
}
|
||||
/**
|
||||
* @param {HTMLElement} el
|
||||
* @param {String|Array} css
|
||||
* @return
|
||||
*/
|
||||
window.getStyle = function getStyle(el, css) {
|
||||
if (typeof el === 'string')
|
||||
el = ge(el);
|
||||
|
||||
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);
|
||||
if (Array.isArray(css)) {
|
||||
var result = {}
|
||||
for (var i = 0; i < css.length; i++) {
|
||||
var key = css[i]
|
||||
result[key] = getStyle(el, key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
if (window.getComputedStyle) {
|
||||
var compStyle = window.getComputedStyle(el, '')
|
||||
return compStyle.getPropertyValue(css)
|
||||
} else if (el.currentStyle) {
|
||||
return el.currentStyle[camelcase(css)]
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})(window);
|
||||
|
@ -1,14 +1,19 @@
|
||||
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 bindEvents(obj) {
|
||||
try {
|
||||
for (var k in obj) {
|
||||
if ((obj.hasOwnProperty(k) || obj.__proto__.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);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function isObject(o) {
|
||||
@ -20,10 +25,10 @@ function isArray(a) {
|
||||
}
|
||||
|
||||
function extend(dst, src) {
|
||||
if (!isObject(dst)) {
|
||||
if (typeof dst !== 'object') {
|
||||
return console.error('extend: dst is not an object');
|
||||
}
|
||||
if (!isObject(src)) {
|
||||
if (typeof src !== 'object') {
|
||||
return console.error('extend: src is not an object');
|
||||
}
|
||||
for (var key in src) {
|
||||
@ -86,4 +91,49 @@ function once(fn, context) {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function throttle(func, wait) {
|
||||
var timeout = null;
|
||||
var lastFunc;
|
||||
var lastRan;
|
||||
|
||||
return function() {
|
||||
var context = this;
|
||||
var args = arguments;
|
||||
if (!lastRan) {
|
||||
func.apply(context, args);
|
||||
lastRan = Date.now();
|
||||
} else {
|
||||
clearTimeout(lastFunc);
|
||||
lastFunc = setTimeout(function() {
|
||||
if ((Date.now() - lastRan) >= wait) {
|
||||
func.apply(context, args);
|
||||
lastRan = Date.now();
|
||||
}
|
||||
}, wait - (Date.now() - lastRan));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function toCamelCase(s) {
|
||||
return s.split('-').map(function(word, index) {
|
||||
return index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function sprintf() {
|
||||
var args = arguments,
|
||||
format = args[0],
|
||||
i = 1;
|
||||
return format.replace(/%(\d+)?([%s])/g, function(match, number, type) {
|
||||
if (type === "%") {
|
||||
return "%";
|
||||
}
|
||||
var index = number ? parseInt(number, 10) : i++;
|
||||
if (index >= args.length) {
|
||||
return match;
|
||||
}
|
||||
return args[index];
|
||||
});
|
||||
}
|
125
htdocs/js/common/05-slideutils.js
Normal file
125
htdocs/js/common/05-slideutils.js
Normal file
@ -0,0 +1,125 @@
|
||||
(function(window) {
|
||||
var SLIDE_DEFAULT_OPTS = {
|
||||
duration: 350,
|
||||
easing: 'ease-in-out'
|
||||
};
|
||||
|
||||
window.slideutils = {
|
||||
up: function slideUp(node, opts, callback) {
|
||||
opts = Object.assign({}, SLIDE_DEFAULT_OPTS, opts || {});
|
||||
var wrap = null;
|
||||
|
||||
if (node._slideActive) {
|
||||
if (node._slideTimeout) {
|
||||
clearTimeout(node._slideTimeout);
|
||||
delete node._slideTimeout;
|
||||
}
|
||||
wrap = node.parentNode;
|
||||
} else {
|
||||
var height = node.offsetHeight;
|
||||
wrap = ce('div', {});
|
||||
setStyle(wrap, {
|
||||
height: height,
|
||||
transition: 'height '+(opts.duration/1000)+'s '+opts.easing,
|
||||
overflow: 'hidden'
|
||||
});
|
||||
|
||||
insertAfter(wrap, node);
|
||||
wrap.appendChild(node);
|
||||
}
|
||||
|
||||
node._slideActive = 1;
|
||||
|
||||
setTimeout(function() {
|
||||
setStyle(wrap, 'height', '0px');
|
||||
|
||||
node._slideTimeout = setTimeout(function() {
|
||||
hide(node);
|
||||
insertAfter(node, wrap);
|
||||
re(wrap);
|
||||
|
||||
delete node._slideActive;
|
||||
delete node._slideTimeout;
|
||||
|
||||
callback && callback();
|
||||
}, opts.duration);
|
||||
});
|
||||
},
|
||||
|
||||
down: function slideDown(node, opts, callback) {
|
||||
opts = Object.assign({}, SLIDE_DEFAULT_OPTS, opts || {});
|
||||
var wrap = null;
|
||||
|
||||
if (node._slideActive) {
|
||||
if (node._slideTimeout) {
|
||||
clearTimeout(node._slideTimeout);
|
||||
delete node._slideTimeout;
|
||||
}
|
||||
wrap = node.parentNode;
|
||||
} else {
|
||||
wrap = ce('div', {});
|
||||
setStyle(wrap, {
|
||||
height: 0,
|
||||
transition: 'height '+(opts.duration/1000)+'s '+opts.easing,
|
||||
overflow: 'hidden'
|
||||
});
|
||||
|
||||
insertAfter(wrap, node);
|
||||
wrap.appendChild(node);
|
||||
show(node);
|
||||
};
|
||||
|
||||
node._slideActive = 2;
|
||||
|
||||
setTimeout(function() {
|
||||
setStyle(wrap, 'height', node.offsetHeight);
|
||||
node._slideTimeout = setTimeout(function() {
|
||||
insertAfter(node, wrap);
|
||||
re(wrap);
|
||||
|
||||
delete node._slideActive;
|
||||
delete node._slideTimeout;
|
||||
|
||||
callback && callback();
|
||||
}, opts.duration);
|
||||
})
|
||||
},
|
||||
|
||||
toggle: function slideToggle(node, opts, callback) {
|
||||
opts = opts || {};
|
||||
var isVisible = getStyle(node, 'display') !== 'none';
|
||||
if (isVisible && node._slideActive !== 1) {
|
||||
this.up(node, opts, callback);
|
||||
return false;
|
||||
} else {
|
||||
this.down(node, opts, callback);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
custom: function(node, targetHeight, opts, callback) {
|
||||
opts = Object.assign({}, SLIDE_DEFAULT_OPTS, opts || {})
|
||||
|
||||
if (node._slideActive) {
|
||||
if (node._slideTimeout) {
|
||||
clearTimeout(node._slideTimeout)
|
||||
delete node._slideTimeout
|
||||
}
|
||||
}
|
||||
|
||||
setStyle(node, {
|
||||
transition: 'height '+(opts.duration/1000)+'s '+opts.easing
|
||||
});
|
||||
|
||||
node._slideActive = 2;
|
||||
setTimeout(function() {
|
||||
setStyle(node, 'height', targetHeight);
|
||||
node._slideTimeout = setTimeout(function() {
|
||||
delete node._slideActive;
|
||||
delete node._slideTimeout;
|
||||
callback && callback();
|
||||
}, opts.duration)
|
||||
});
|
||||
}
|
||||
};
|
||||
})(window);
|
27
htdocs/js/common/06-cookies.js
Normal file
27
htdocs/js/common/06-cookies.js
Normal file
@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
|
@ -2,4 +2,53 @@ function lang(key) {
|
||||
return __lang[key] !== undefined ? __lang[key] : '{'+key+'}';
|
||||
}
|
||||
|
||||
lang.num = function(s, num, opts) {
|
||||
var defaultOpts = {
|
||||
format: true,
|
||||
formatDelim: ' ',
|
||||
lang: 'ru'
|
||||
};
|
||||
opts = Object.assign({}, defaultOpts, opts || {});
|
||||
|
||||
if (typeof s === 'string')
|
||||
s = get(s);
|
||||
|
||||
var word = 2;
|
||||
switch (opts.lang) {
|
||||
case 'ru':
|
||||
var n = num % 100;
|
||||
if (n > 19)
|
||||
n %= 10;
|
||||
|
||||
if (n === 1) {
|
||||
word = 0;
|
||||
} else if (n >= 2 && n <= 4) {
|
||||
word = 1;
|
||||
} else if (num === 0 && s.length === 4) {
|
||||
word = 3;
|
||||
} else {
|
||||
word = 2;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (num === 0 && s.length === 4) {
|
||||
word = 3;
|
||||
} else {
|
||||
word = num === 1 ? 0 : 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// if zero
|
||||
if (word === 3)
|
||||
return s[3];
|
||||
|
||||
if (typeof opts.format === 'function') {
|
||||
num = opts.format(num)
|
||||
}
|
||||
|
||||
return sprintf(s[word], num)
|
||||
}
|
||||
|
||||
window.__lang = {};
|
@ -1,4 +1,6 @@
|
||||
var ThemeSwitcher = (function() {
|
||||
var logger = getLogger('35-theme-switcher.js');
|
||||
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
@ -60,7 +62,7 @@ var ThemeSwitcher = (function() {
|
||||
if (!val)
|
||||
return modes[0];
|
||||
if (modes.indexOf(val) === -1) {
|
||||
console.error('[ThemeSwitcher getSavedMode] invalid cookie value')
|
||||
logger.local('getSavedMode').error('invalid cookie value')
|
||||
unsetCookie('theme')
|
||||
return modes[0]
|
||||
}
|
||||
@ -80,7 +82,7 @@ var ThemeSwitcher = (function() {
|
||||
try {
|
||||
f(dark)
|
||||
} catch (e) {
|
||||
console.error('[ThemeSwitcher->changeTheme->onDone] error while calling user callback:', e)
|
||||
logger.local('changeTheme').error('error while calling user callback:', e)
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -135,7 +137,7 @@ var ThemeSwitcher = (function() {
|
||||
document.body.setAttribute('data-theme', selectedMode);
|
||||
for (var i = 0; i < modes.length; i++) {
|
||||
var mode = modes[i];
|
||||
document.getElementById('moon_'+mode).style.display = mode === selectedMode ? 'block': 'none';
|
||||
document.getElementById('svgicon_moon_'+mode+'_18').style.display = mode === selectedMode ? 'block': 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +181,7 @@ var ThemeSwitcher = (function() {
|
||||
|
||||
next: function(e) {
|
||||
if (hasClass(document.body, 'theme-changing')) {
|
||||
console.log('next: theme changing is in progress, ignoring...')
|
||||
logger.local('next').log('theme changing is in progress, ignoring...')
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
var IndexPage = {
|
||||
window.IndexPage = {
|
||||
offsets: {
|
||||
en: 0,
|
||||
ru: 300
|
||||
|
202
htdocs/js/common/41-files-search.js
Normal file
202
htdocs/js/common/41-files-search.js
Normal file
@ -0,0 +1,202 @@
|
||||
function FileSearch(opts) {
|
||||
bindEvents(this);
|
||||
|
||||
this.logger = getLogger('FileSearch');
|
||||
this.req = null;
|
||||
this.initializedWithSearch = opts.inited_with_search;
|
||||
this.collectionName = opts.collection_name;
|
||||
this.baseUrl = opts.base_url;
|
||||
this.inputQuery = opts.query || null;
|
||||
this.resultsCount = opts.count || 0;
|
||||
this.resultsOffset = opts.query ? opts.per_page : 0;
|
||||
this.resultsPerPage = opts.per_page;
|
||||
this.minQueryLength = opts.min_query_length;
|
||||
|
||||
this.refs = {
|
||||
container: ge(opts.container),
|
||||
filesList: ge('files_list'),
|
||||
clearIcon: ge('files_search_clear_icon'),
|
||||
showMore: ge('files_show_more'),
|
||||
input: ge('files_search_input'),
|
||||
info: ge('files_search_info'),
|
||||
infoResultsCount: ge('files_search_info_text')
|
||||
};
|
||||
|
||||
if (this.inputQuery)
|
||||
this.refs.input.value = this.inputQuery;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
extend(FileSearch.prototype, {
|
||||
init: function() {
|
||||
this.refs.clearIcon.addEventListener('click', this.onClearIconClick);
|
||||
this.refs.input.addEventListener('focus', this.onSearchInputFocus);
|
||||
this.refs.input.addEventListener('blur', this.onSearchInputBlur);
|
||||
this.refs.input.addEventListener('input', this.onSearchInput);
|
||||
this.refs.input.addEventListener('keydown', this.onSearchInputKeyDown);
|
||||
this.refs.showMore.addEventListener('click', this.onShowMoreClick);
|
||||
|
||||
this.searchThrottled = throttle(this.search, 300);
|
||||
|
||||
this.refs.input.focus();
|
||||
},
|
||||
|
||||
abortRequestIfNeeded: function() {
|
||||
if (this.req) {
|
||||
this.req.abort();
|
||||
this.req = null;
|
||||
}
|
||||
},
|
||||
|
||||
search: function(query) {
|
||||
var queryChanged = this.inputQuery !== query;
|
||||
if (queryChanged) {
|
||||
this.resultsOffset = 0;
|
||||
this.resultsCount = 0;
|
||||
}
|
||||
|
||||
this.abortRequestIfNeeded();
|
||||
this.inputQuery = query;
|
||||
|
||||
this.showProgress();
|
||||
|
||||
this.req = ajax.get(this.baseUrl, {
|
||||
q: query,
|
||||
offset: this.resultsOffset
|
||||
}, function(error, response) {
|
||||
this.hideProgress();
|
||||
if (error) {
|
||||
alert('error!');
|
||||
console.error(error);
|
||||
} else {
|
||||
if (!this.initializedWithSearch) {
|
||||
var filesListEl = this.refs.filesList;
|
||||
var filesListHiddenEl = ge('files_list_hidden');
|
||||
if (!filesListHiddenEl.hasAttribute('data-non-empty')) {
|
||||
filesListHiddenEl.innerHTML = filesListEl.innerHTML;
|
||||
filesListHiddenEl.setAttribute('data-non-empty', '1');
|
||||
filesListEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (queryChanged) {
|
||||
this.refs.filesList.innerHTML = response.html
|
||||
} else {
|
||||
this.refs.filesList.innerHTML += response.html;
|
||||
}
|
||||
|
||||
this.resultsCount = response.search_count;
|
||||
this.resultsOffset = response.new_offset;
|
||||
|
||||
this.updateLocation();
|
||||
this.showResultsCount();
|
||||
|
||||
if (this.canLoadMore()) {
|
||||
show(this.refs.showMore)
|
||||
} else {
|
||||
hide(this.refs.showMore)
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
canLoadMore: function() {
|
||||
return this.resultsOffset < this.resultsCount;
|
||||
},
|
||||
|
||||
showProgress: function () {
|
||||
addClass(this.refs.showMore, 'is-loading');
|
||||
|
||||
if (!visible(this.refs.info))
|
||||
slideutils.down(this.refs.info);
|
||||
|
||||
addClass(this.refs.info, 'is-loading');
|
||||
show(this.refs.clearIcon);
|
||||
},
|
||||
|
||||
hideProgress: function() {
|
||||
removeClass(this.refs.showMore, 'is-loading');
|
||||
removeClass(this.refs.info, 'is-loading');
|
||||
},
|
||||
|
||||
hideSearch: function() {
|
||||
hide(this.refs.showMore);
|
||||
|
||||
var filesListEl = ge('files_list');
|
||||
var filesListHiddenEl = ge('files_list_hidden');
|
||||
var nonEmpty = filesListHiddenEl.getAttribute('data-non-empty') === '1';
|
||||
if (!nonEmpty)
|
||||
window.location = this.baseUrl;
|
||||
else {
|
||||
filesListEl.innerHTML = filesListHiddenEl.innerHTML
|
||||
filesListHiddenEl.innerHTML = ''
|
||||
filesListHiddenEl.removeAttribute('data-non-empty')
|
||||
}
|
||||
|
||||
this.abortRequestIfNeeded();
|
||||
|
||||
this.inputQuery = null;
|
||||
this.resultsCount = 0;
|
||||
this.resultsOffset = 0;
|
||||
|
||||
this.updateLocation();
|
||||
|
||||
hide(this.refs.clearIcon);
|
||||
this.hideProgress();
|
||||
if (visible(this.refs.info))
|
||||
slideutils.up(this.refs.info);
|
||||
},
|
||||
|
||||
updateLocation: function() {
|
||||
var title = lang('4in1') + ' - ' + lang('files_'+this.collectionName+'_collection');
|
||||
if (this.inputQuery !== null)
|
||||
title += ' - ' + this.inputQuery;
|
||||
var url = this.baseUrl;
|
||||
if (this.inputQuery !== null)
|
||||
url += '?q='+encodeURIComponent(this.inputQuery);
|
||||
window.history.replaceState(null, title, url);
|
||||
document.title = title;
|
||||
},
|
||||
|
||||
showResultsCount: function() {
|
||||
this.refs.infoResultsCount.innerText = lang.num(lang('files_search_results_count'), this.resultsCount);
|
||||
},
|
||||
|
||||
onSearchInput: function(evt) {
|
||||
var q = evt.target.value.trim();
|
||||
if (q.length >= this.minQueryLength)
|
||||
this.searchThrottled(q);
|
||||
else if (q === '')
|
||||
this.hideSearch();
|
||||
},
|
||||
|
||||
onSearchInputKeyDown: function(evt) {
|
||||
if (evt.keyCode === 10 || evt.keyCode === 13) {
|
||||
var query = this.refs.input.value.trim();
|
||||
if (query === '') {
|
||||
this.hideSearch();
|
||||
} else {
|
||||
this.searchThrottled(query);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onSearchInputFocus: function(evt) {
|
||||
addClass('files_search', 'is-focused');
|
||||
},
|
||||
|
||||
onSearchInputBlur: function(evt) {
|
||||
removeClass('files_search', 'is-focused');
|
||||
},
|
||||
|
||||
onClearIconClick: function(evt) {
|
||||
this.refs.input.value = '';
|
||||
this.refs.input.focus();
|
||||
this.hideSearch();
|
||||
},
|
||||
|
||||
onShowMoreClick: function(evt) {
|
||||
this.search(this.inputQuery);
|
||||
}
|
||||
});
|
@ -66,13 +66,22 @@
|
||||
}
|
||||
|
||||
.blog-upload-form {
|
||||
margin-top: $base-padding;
|
||||
padding-top: $base-padding;
|
||||
padding-bottom: $base-padding;
|
||||
background-color: $hover-hl;
|
||||
@include radius(4px);
|
||||
}
|
||||
|
||||
.blog-upload-list {}
|
||||
.blog-upload-list {
|
||||
padding-top: $base-padding;
|
||||
}
|
||||
.blog-upload-item {
|
||||
border-top: 1px $border-color solid;
|
||||
padding: 10px 0;
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
.blog-upload-item-actions {
|
||||
float: right;
|
||||
@ -171,7 +180,7 @@ body.wide .blog-post {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
|
||||
.blog-post-date {
|
||||
@ -383,7 +392,7 @@ body.wide .blog-post {
|
||||
td.blog-item-date-cell {
|
||||
width: 1px;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
//text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.blog-item-date {
|
||||
@ -406,9 +415,9 @@ td.blog-item-title-cell {
|
||||
.blog-item-row-year {
|
||||
td {
|
||||
padding-top: 10px;
|
||||
text-align: right;
|
||||
font-size: 20px;
|
||||
letter-spacing: -0.5px;
|
||||
//text-align: right;
|
||||
font-size: $fs + 2px;
|
||||
//letter-spacing: -0.5px;
|
||||
}
|
||||
&:first-child td {
|
||||
padding-top: 0;
|
||||
|
@ -23,6 +23,16 @@ body {
|
||||
font-size: $fs;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Old versions of Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently
|
||||
supported by Chrome, Edge, Opera and Firefox */
|
||||
}
|
||||
|
||||
.base-width {
|
||||
max-width: $base-width;
|
||||
margin: 0 auto;
|
||||
@ -43,6 +53,13 @@ body.full-width .base-width {
|
||||
}
|
||||
}
|
||||
|
||||
.matched {
|
||||
background: $hl_matched_bg;
|
||||
color: $hl_matched_fg;
|
||||
padding: 0 1px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
@ -358,15 +375,20 @@ a.index-dl-line {
|
||||
}
|
||||
|
||||
.bc {
|
||||
padding-bottom: 15px;
|
||||
&.mt {
|
||||
margin-top: $base-padding;
|
||||
}
|
||||
|
||||
> a.bc-item, > span.bc-item {
|
||||
font-size: $fs + 2px;
|
||||
line-height: $fs + 6px;
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
padding: 4px 9px;
|
||||
vertical-align: middle;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: 10px;
|
||||
margin-bottom: 8px;
|
||||
&:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -375,8 +397,10 @@ a.index-dl-line {
|
||||
border-radius: 4px;
|
||||
}
|
||||
> span.bc-item {
|
||||
padding: 5px;
|
||||
font-weight: 600;
|
||||
padding: 4px;
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> a.bc-item:hover {
|
||||
@ -387,6 +411,15 @@ a.index-dl-line {
|
||||
|
||||
// arrow
|
||||
span.bc-arrow {
|
||||
color: $grey !important;
|
||||
color: $light-grey;
|
||||
vertical-align: middle;
|
||||
opacity: .7;
|
||||
> svg {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin-left: -6px;
|
||||
margin-right: -4px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
196
htdocs/scss/app/files.scss
Normal file
196
htdocs/scss/app/files.scss
Normal file
@ -0,0 +1,196 @@
|
||||
.files-title {
|
||||
padding: 5px 0;
|
||||
font-size: 20px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.files-list {}
|
||||
.files-list-item {
|
||||
display: block;
|
||||
padding: 5px 8px;
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
|
||||
&.is-disabled {
|
||||
color: $dark-grey;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
float: left;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
&-info {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
//&-label {
|
||||
// margin-right: 4px;
|
||||
//}
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
color: $grey;
|
||||
}
|
||||
}
|
||||
.files-list-item-title-label-external-icon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
color: $grey;
|
||||
}
|
||||
|
||||
.files-list-item-title-label + .files-list-item-meta-item {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.files-list-item-meta {
|
||||
padding: 2px 0;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.files-list-item-meta-item {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
font-size: $fs - 5px;
|
||||
background-color: $hover-hl;
|
||||
color: transparentize($fg, 0.4);
|
||||
padding: 3px 4px;
|
||||
@include radius(4px);
|
||||
&:last-child { margin-right: 0px; }
|
||||
}
|
||||
|
||||
.files-list-item:hover {
|
||||
text-decoration: none;
|
||||
background-color: $hover-hl;
|
||||
@include radius(3px);
|
||||
|
||||
.files-list-item-meta-item {
|
||||
background-color: darken($hover-hl, 7%);
|
||||
}
|
||||
}
|
||||
|
||||
.files-list-item-text-excerpt {
|
||||
font-size: $fs - 4px;
|
||||
font-family: monospace;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 2px;
|
||||
color: $dark-grey;
|
||||
.matched {
|
||||
background-color: darken($hover-hl, 4%);
|
||||
@include radius(2px);
|
||||
color: $dark-grey;
|
||||
}
|
||||
}
|
||||
|
||||
.files-search {
|
||||
position: relative;
|
||||
border: 1px $input-border solid;
|
||||
background-color: $input-bg;
|
||||
@include radius(4px);
|
||||
|
||||
&.is-focused {
|
||||
border-color: $input-border-focused;
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
> input[type="text"] {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 8px 8px 8px 34px;
|
||||
}
|
||||
|
||||
&-wrap {
|
||||
padding-top: 6px;
|
||||
padding-bottom: $base-padding;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
color: $grey;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 6px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&-clear-icon {
|
||||
color: $light-grey;
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
> svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
// since we know that icon is 16x16, we can just use -8px here
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-results-info {
|
||||
padding-top: 8px;
|
||||
|
||||
&-inner {
|
||||
position: relative;
|
||||
background-color: $hover-hl;
|
||||
color: $fg;
|
||||
font-size: $fs - 2px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 8px;
|
||||
@include radius(4px);
|
||||
}
|
||||
|
||||
&-spinner {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 1px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
&.is-loading &-spinner { display: block; }
|
||||
}
|
||||
}
|
||||
|
||||
.files-list-show-more {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
@include radius(4px);
|
||||
background-color: $hover-hl;
|
||||
margin-top: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: $hover-hl-darker;
|
||||
}
|
||||
&:active {
|
||||
background-color: darken($hover-hl-darker, 4%);
|
||||
}
|
||||
|
||||
> .spinner {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
&.is-loading > .spinner { display: block; }
|
||||
&.is-loading > &-label { visibility: hidden; }
|
||||
}
|
@ -117,3 +117,10 @@ body a.head-item.is-theme-switcher svg path,
|
||||
body a.head-item.is-settings svg path {
|
||||
fill: $fg;
|
||||
}
|
||||
|
||||
|
||||
#svgicon_moon_light_18,
|
||||
#svgicon_moon_dark_18,
|
||||
#svgicon_moon_auto_18 {
|
||||
display: none;
|
||||
}
|
30
htdocs/scss/app/widgets.scss
Normal file
30
htdocs/scss/app/widgets.scss
Normal file
@ -0,0 +1,30 @@
|
||||
.spinner {
|
||||
text-align: center;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 4px;
|
||||
margin-right: 2px;
|
||||
animation: highlight .6s infinite linear;
|
||||
|
||||
&:last-child { margin-right: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0%, 100% {
|
||||
background-color: desaturate(lighten($link-color, 40%), 80%);
|
||||
}
|
||||
33% {
|
||||
background-color: lighten($link-color, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner > span:nth-child(1) {
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
|
||||
.spinner > span:nth-child(2) {
|
||||
animation-delay: -0.2s;
|
||||
}
|
@ -4,7 +4,9 @@
|
||||
@import "./app/blog";
|
||||
@import "./app/form";
|
||||
@import "./app/pages";
|
||||
@import "./app/files";
|
||||
@import "./hljs/github.scss";
|
||||
@import "./app/widgets";
|
||||
|
||||
@media screen and (max-width: 880px) {
|
||||
@import "./app/mobile";
|
||||
|
@ -13,6 +13,9 @@ $light-grey: $grey;
|
||||
$fg: #eee;
|
||||
$bg: #333;
|
||||
|
||||
$hl_matched_bg: $link-color;
|
||||
$hl_matched_fg: #fff;
|
||||
|
||||
$code-block-bg: #394146;
|
||||
$inline-code-block-bg: #394146;
|
||||
|
||||
|
@ -13,6 +13,9 @@ $light-grey: #999;
|
||||
$fg: #222;
|
||||
$bg: #fff;
|
||||
|
||||
$hl_matched_bg: #207cdf;
|
||||
$hl_matched_fg: #fff;
|
||||
|
||||
$code-block-bg: #f3f3f3;
|
||||
$inline-code-block-bg: #f1f1f1;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
$fs: 16px;
|
||||
$fsMono: 85%;
|
||||
$ff: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
|
||||
$ff: -apple-system, BlinkMacSystemFont, Segoe UI, 'Liberation Sans', Helvetica, Arial, sans-serif;
|
||||
$ffMono: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace;
|
||||
|
||||
$base-width: 800px;
|
||||
|
@ -251,7 +251,7 @@ class Logger {
|
||||
continue;
|
||||
|
||||
$class_name = substr($f, 0, strpos($f, '.'));
|
||||
$class = '\\knigavuhe\\AdminActions\\'.$class_name;
|
||||
$class = '\\AdminActions\\'.$class_name;
|
||||
|
||||
if (interface_exists($class) || !class_exists($class)) {
|
||||
// logError(__METHOD__.': class '.$class.' not found');
|
||||
|
@ -214,7 +214,7 @@ class MyParsedown extends ParsedownExtended {
|
||||
}
|
||||
|
||||
protected static function getSkinContext(): SkinContext {
|
||||
return new SkinContext('\\skin\\markdown');
|
||||
return skin('markdown');
|
||||
}
|
||||
|
||||
}
|
||||
|
525
lib/files.php
Normal file
525
lib/files.php
Normal file
@ -0,0 +1,525 @@
|
||||
<?php
|
||||
|
||||
require_once 'engine/sphinx.php';
|
||||
|
||||
const WFF_ARCHIVE_SPHINX_RTINDEX = 'wff_collection';
|
||||
//const MDF_ARCHIVE_SPHINX/**/_RTINDEX = 'mdf_archive';
|
||||
//const BACONIANA_ARCHIVE_SPHINX_RTINDEX = 'baconiana_archive';
|
||||
|
||||
enum FilesCollection: string {
|
||||
case WilliamFriedman = 'wff';
|
||||
case MercureDeFrance = 'mdf';
|
||||
case Baconiana = 'baconiana';
|
||||
|
||||
public function isSearchSupported(): bool {
|
||||
return $this == FilesCollection::WilliamFriedman;
|
||||
}
|
||||
}
|
||||
|
||||
enum FilesItemType: string {
|
||||
case FILE = 'file';
|
||||
case FOLDER = 'folder';
|
||||
}
|
||||
|
||||
enum BookFileType: string {
|
||||
case NONE = 'none';
|
||||
case BOOK = 'book';
|
||||
case ARTICLE = 'article';
|
||||
}
|
||||
|
||||
enum BookCategory: string {
|
||||
case BOOKS = 'books';
|
||||
case MISC = 'misc';
|
||||
}
|
||||
|
||||
interface FilesItemInterface {
|
||||
public function getId(): string;
|
||||
public function isFolder(): bool;
|
||||
public function isFile(): bool;
|
||||
public function getUrl(): string;
|
||||
public function getSize(): ?int;
|
||||
public function getTitle(): string;
|
||||
public function getTitleHtml(): ?string;
|
||||
public function getMeta(?string $hl_matched = null): array;
|
||||
public function isAvailable(): bool;
|
||||
public function isTargetBlank(): bool;
|
||||
public function getSubtitle(): ?string;
|
||||
}
|
||||
|
||||
trait FilesItemTypeTrait {
|
||||
public FilesItemType $type;
|
||||
public function isFolder(): bool { return $this->type == FilesItemType::FOLDER; }
|
||||
public function isFile(): bool { return $this->type == FilesItemType::FILE; }
|
||||
}
|
||||
|
||||
trait FilesItemSizeTrait {
|
||||
public int $size;
|
||||
public function getSize(): ?int { return $this->isFile() ? $this->size : null; }
|
||||
}
|
||||
|
||||
class CollectionItem implements FilesItemInterface {
|
||||
|
||||
public function __construct(
|
||||
protected FilesCollection $collection
|
||||
) {}
|
||||
|
||||
public function getTitleHtml(): ?string { return null; }
|
||||
public function getId(): string { return $this->collection->value; }
|
||||
public function isFolder(): bool { return true; }
|
||||
public function isFile(): bool { return false; }
|
||||
public function isAvailable(): bool { return true; }
|
||||
public function getUrl(): string {
|
||||
global $config;
|
||||
switch ($this->collection) {
|
||||
case FilesCollection::MercureDeFrance:
|
||||
case FilesCollection::WilliamFriedman:
|
||||
return '/files/'.$this->collection->value.'/';
|
||||
case FilesCollection::Baconiana:
|
||||
return 'https://'.$config['files_domain'].'/Baconiana/';
|
||||
}
|
||||
}
|
||||
public function getSize(): ?int { return null; }
|
||||
public function getTitle(): string { return lang("files_{$this->collection->value}_collection"); }
|
||||
public function getMeta(?string $hl_matched = null): array { return []; }
|
||||
public function isTargetBlank(): bool { return $this->collection === FilesCollection::Baconiana; }
|
||||
public function getSubtitle(): ?string { return null; }
|
||||
}
|
||||
|
||||
class WFFCollectionItem extends model implements FilesItemInterface {
|
||||
|
||||
const DB_TABLE = 'wff_collection';
|
||||
|
||||
use FilesItemTypeTrait;
|
||||
use FilesItemSizeTrait;
|
||||
|
||||
public int $id;
|
||||
public int $parentId;
|
||||
public string $title;
|
||||
public string $documentId;
|
||||
public string $path;
|
||||
public int $filesCount;
|
||||
|
||||
public function getTitleHtml(): ?string { return null; }
|
||||
public function getId(): string { return (string)$this->id; }
|
||||
public function isAvailable(): bool { return true; }
|
||||
public function getTitle(): string { return $this->title; }
|
||||
public function getDocumentId(): string { return $this->isFolder() ? str_replace('_', ' ', basename($this->path)) : $this->documentId; }
|
||||
public function isTargetBlank(): bool { return $this->isFile(); }
|
||||
public function getSubtitle(): ?string { return null; }
|
||||
|
||||
public function getUrl(): string {
|
||||
global $config;
|
||||
return $this->isFolder()
|
||||
? "/files/wff/{$this->id}/"
|
||||
: "https://{$config['files_domain']}/NSA Friedman Documents/{$this->path}";
|
||||
}
|
||||
|
||||
public function getMeta(?string $hl_matched = null): array {
|
||||
if ($this->isFolder()) {
|
||||
if (!$this->parentId)
|
||||
return [];
|
||||
return [
|
||||
'items' => [
|
||||
hl_matched($this->getDocumentId(), $hl_matched),
|
||||
lang_num('files_count', $this->filesCount)
|
||||
]
|
||||
];
|
||||
}
|
||||
return [
|
||||
'inline' => false,
|
||||
'items' => [
|
||||
hl_matched('Document '.$this->documentId),
|
||||
sizeString($this->size),
|
||||
'PDF'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MDFCollectionItem extends model implements FilesItemInterface {
|
||||
|
||||
const DB_TABLE = 'mdf_collection';
|
||||
|
||||
use FilesItemTypeTrait;
|
||||
use FilesItemSizeTrait;
|
||||
|
||||
public int $id;
|
||||
public int $issue;
|
||||
public string $path;
|
||||
public string $date;
|
||||
public int $volume;
|
||||
public int $pageFrom;
|
||||
public int $pageTo;
|
||||
public int $pdfPages;
|
||||
public int $size;
|
||||
|
||||
public function isAvailable(): bool { return true; }
|
||||
|
||||
public function getTitleHtml(): ?string { return null; }
|
||||
|
||||
public function getTitle(): string {
|
||||
return "№{$this->issue}, {$this->getHumanFriendlyDate()}";
|
||||
}
|
||||
|
||||
protected function getHumanFriendlyDate(): string {
|
||||
$dt = new DateTime($this->date);
|
||||
return $dt->format('j M Y');
|
||||
}
|
||||
|
||||
public function isTargetBlank(): bool { return true; }
|
||||
public function getId(): string { return (string)$this->issue; }
|
||||
public function getUrl(): string {
|
||||
global $config;
|
||||
return 'https://'.$config['files_domain'].'/Mercure-de-France-OCR/'.$this->path;
|
||||
}
|
||||
|
||||
public function getMeta(?string $hl_matched = null): array {
|
||||
return [
|
||||
'inline' => true,
|
||||
'items' => [
|
||||
'Vol. '.$this->getRomanVolume(),
|
||||
'pp. '.$this->pageFrom.'-'.$this->pageTo,
|
||||
sizeString($this->size),
|
||||
'PDF'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getRomanVolume(): string {
|
||||
return _arabic_to_roman($this->volume);
|
||||
}
|
||||
|
||||
public function getSubtitle(): ?string {
|
||||
return null;
|
||||
//return 'Vol. '.$this->getRomanVolume().', pp. '.$this->pageFrom.'-'.$this->pageTo;
|
||||
}
|
||||
}
|
||||
|
||||
class BookItem extends model implements FilesItemInterface {
|
||||
|
||||
const DB_TABLE = 'books';
|
||||
|
||||
public int $id;
|
||||
public int $parentId;
|
||||
public string $author;
|
||||
public string $title;
|
||||
public int $year;
|
||||
public int $size;
|
||||
public FilesItemType $type;
|
||||
public BookFileType $fileType;
|
||||
public string $path;
|
||||
public bool $external;
|
||||
|
||||
use FilesItemSizeTrait;
|
||||
use FilesItemTypeTrait;
|
||||
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUrl(): string {
|
||||
if ($this->isFolder() && !$this->external)
|
||||
return '/files/'.$this->id.'/';
|
||||
global $config;
|
||||
$buf = 'https://'.$config['files_domain'];
|
||||
if (!str_starts_with($this->path, '/'))
|
||||
$buf .= '/';
|
||||
$buf .= $this->path;
|
||||
return $buf;
|
||||
}
|
||||
|
||||
public function getTitleHtml(): ?string {
|
||||
if ($this->isFolder() || !$this->author)
|
||||
return null;
|
||||
$buf = '<b>'.htmlescape($this->author).'</b>';
|
||||
if (!str_ends_with($this->author, '.'))
|
||||
$buf .= '.';
|
||||
$buf .= ' '.htmlescape($this->title);
|
||||
return $buf;
|
||||
}
|
||||
|
||||
public function getTitle(): string {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getMeta(?string $hl_matched = null): array {
|
||||
if ($this->isFolder())
|
||||
return [];
|
||||
|
||||
$items = [
|
||||
sizeString($this->size),
|
||||
strtoupper($this->getExtension())
|
||||
];
|
||||
|
||||
return [
|
||||
'inline' => false,
|
||||
'items' => $items
|
||||
];
|
||||
}
|
||||
|
||||
protected function getExtension(): string {
|
||||
return extension(basename($this->path));
|
||||
}
|
||||
|
||||
public function isAvailable(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isTargetBlank(): bool {
|
||||
return $this->isFile() || $this->external;
|
||||
}
|
||||
|
||||
public function getSubtitle(): ?string {
|
||||
if (!$this->year)
|
||||
return null;
|
||||
return '('.$this->year.')';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $folder_id
|
||||
* @param bool $with_parents
|
||||
* @return WFFCollectionItem|WFFCollectionItem[]|null
|
||||
*/
|
||||
function wff_get_folder(int $folder_id, bool $with_parents = false): WFFCollectionItem|array|null {
|
||||
$db = DB();
|
||||
$q = $db->query("SELECT * FROM wff_collection WHERE id=?", $folder_id);
|
||||
if (!$db->numRows($q))
|
||||
return null;
|
||||
$item = new WFFCollectionItem($db->fetch($q));
|
||||
if (!$item->isFolder())
|
||||
return null;
|
||||
if ($with_parents) {
|
||||
$items = [$item];
|
||||
if ($item->parentId) {
|
||||
$parents = wff_get_folder($item->parentId, true);
|
||||
if ($parents !== null)
|
||||
$items = array_merge($items, $parents);
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|int[]|null $parent_id
|
||||
* @return array
|
||||
*/
|
||||
function wff_get(int|array|null $parent_id = null) {
|
||||
$db = DB();
|
||||
|
||||
$where = [];
|
||||
$args = [];
|
||||
|
||||
if (!is_null($parent_id)) {
|
||||
if (is_int($parent_id)) {
|
||||
$where[] = "parent_id=?";
|
||||
$args[] = $parent_id;
|
||||
} else {
|
||||
$where[] = "parent_id IN (".implode(", ", $parent_id).")";
|
||||
}
|
||||
}
|
||||
$sql = "SELECT * FROM wff_collection";
|
||||
if (!empty($where))
|
||||
$sql .= " WHERE ".implode(" AND ", $where);
|
||||
$sql .= " ORDER BY title";
|
||||
$q = $db->query($sql, ...$args);
|
||||
|
||||
return array_map('WFFCollectionItem::create_instance', $db->fetchAll($q));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $ids
|
||||
* @return WFFCollectionItem[]
|
||||
*/
|
||||
function wff_get_by_id(array $ids): array {
|
||||
$db = DB();
|
||||
$q = $db->query("SELECT * FROM wff_collection WHERE id IN (".implode(',', $ids).")");
|
||||
return array_map('WFFCollectionItem::create_instance', $db->fetchAll($q));
|
||||
}
|
||||
|
||||
function wff_search(string $q, int $offset = 0, int $count = 0): array {
|
||||
$query_filtered = sphinx_mkquery($q, [
|
||||
'star' => false,
|
||||
]);
|
||||
|
||||
$cl = sphinx_client();
|
||||
$cl->setLimits($offset, $count);
|
||||
$cl->setMatchMode(Sphinx\SphinxClient::SPH_MATCH_EXTENDED);
|
||||
$cl->setFieldWeights([
|
||||
'title' => 50,
|
||||
'document_id' => 60,
|
||||
]);
|
||||
|
||||
$cl->setRankingMode(Sphinx\SphinxClient::SPH_RANK_PROXIMITY_BM25);
|
||||
$cl->setSortMode(Sphinx\SphinxClient::SPH_SORT_EXTENDED, '@relevance DESC, is_folder DESC');
|
||||
|
||||
// run search
|
||||
$final_query = "$query_filtered";
|
||||
$result = $cl->query($final_query, WFF_ARCHIVE_SPHINX_RTINDEX);
|
||||
$error = $cl->getLastError();
|
||||
$warning = $cl->getLastWarning();
|
||||
if ($error)
|
||||
logError(__FUNCTION__, $error);
|
||||
if ($warning)
|
||||
logWarning(__FUNCTION__, $warning);
|
||||
if ($result === false)
|
||||
return ['count' => 0, 'items' => []];
|
||||
|
||||
$total_found = (int)$result['total_found'];
|
||||
|
||||
$items = [];
|
||||
if (!empty($result['matches']))
|
||||
$items = wff_get_by_id(array_keys($result['matches']));
|
||||
|
||||
return ['count' => $total_found, 'items' => $items];
|
||||
}
|
||||
|
||||
function wff_reindex(): void {
|
||||
sphinx_execute("TRUNCATE RTINDEX ".WFF_ARCHIVE_SPHINX_RTINDEX);
|
||||
$db = DB();
|
||||
$q = $db->query("SELECT * FROM wff_collection");
|
||||
while ($row = $db->fetch($q)) {
|
||||
$item = new WFFCollectionItem($row);
|
||||
if ($item->isFile()) {
|
||||
$txt = file_get_contents('/home/user/nsa/txt/'.str_replace('.pdf', '.txt', basename($item->path)));
|
||||
} else {
|
||||
$txt = '';
|
||||
}
|
||||
sphinx_execute("INSERT INTO ".WFF_ARCHIVE_SPHINX_RTINDEX." (id, document_id, title, text, is_folder, parent_id) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
$item->id, $item->getDocumentId(), $item->title, $txt, (int)$item->isFolder(), $item->parentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $ids
|
||||
* @param string[] $keywords Must already be lower-cased
|
||||
* @param int $before
|
||||
* @param int $after
|
||||
* @return array
|
||||
*/
|
||||
function wff_get_text_excerpts(array $ids,
|
||||
array $keywords,
|
||||
int $before = 50,
|
||||
int $after = 40): array {
|
||||
$results = [];
|
||||
foreach ($ids as $id)
|
||||
$results[$id] = null;
|
||||
|
||||
$db = DB();
|
||||
|
||||
$dynamic_sql_parts = [];
|
||||
$combined_parts = [];
|
||||
foreach ($keywords as $keyword) {
|
||||
$part = "LOCATE('".$db->escape($keyword)."', LOWER(text))";
|
||||
$dynamic_sql_parts[] = $part;
|
||||
}
|
||||
if (count($dynamic_sql_parts) > 1) {
|
||||
foreach ($dynamic_sql_parts as $part)
|
||||
$combined_parts[] = "IF({$part} > 0, {$part}, CHAR_LENGTH(text) + 1)";
|
||||
$combined_parts = implode(', ', $combined_parts);
|
||||
$combined_parts = 'LEAST('.$combined_parts.')';
|
||||
} else {
|
||||
$combined_parts = "IF({$dynamic_sql_parts[0]} > 0, {$dynamic_sql_parts[0]}, CHAR_LENGTH(text) + 1)";
|
||||
}
|
||||
|
||||
$total = $before + $after;
|
||||
$sql = "SELECT
|
||||
wff_id AS id,
|
||||
GREATEST(
|
||||
1,
|
||||
{$combined_parts} - {$before}
|
||||
) AS excerpt_start_index,
|
||||
SUBSTRING(
|
||||
text,
|
||||
GREATEST(
|
||||
1,
|
||||
{$combined_parts} - {$before}
|
||||
),
|
||||
LEAST(
|
||||
CHAR_LENGTH(text),
|
||||
{$total} + {$combined_parts} - GREATEST(1, {$combined_parts} - {$before})
|
||||
)
|
||||
) AS excerpt
|
||||
FROM
|
||||
wff_texts
|
||||
WHERE
|
||||
wff_id IN (".implode(',', $ids).")";
|
||||
|
||||
$q = $db->query($sql);
|
||||
while ($row = $db->fetch($q)) {
|
||||
$results[$row['id']] = [
|
||||
'excerpt' => preg_replace('/\s+/', ' ', $row['excerpt']),
|
||||
'index' => (int)$row['excerpt_start_index']
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MDFCollectionItem[]
|
||||
*/
|
||||
function mdf_get(): array {
|
||||
$db = DB();
|
||||
$q = $db->query("SELECT * FROM mdf_collection ORDER BY `date`");
|
||||
return array_map('MDFCollectionItem::create_instance', $db->fetchAll($q));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BookItem[]
|
||||
*/
|
||||
function books_get(int $parent_id = 0,
|
||||
BookCategory $category = BookCategory::BOOKS): array {
|
||||
$db = DB();
|
||||
|
||||
if ($category == BookCategory::BOOKS) {
|
||||
$order_by = "type, ".($parent_id != 0 ? 'year, ': '')."author, title";
|
||||
}
|
||||
else
|
||||
$order_by = "type, title";
|
||||
|
||||
$q = $db->query("SELECT * FROM books WHERE category=? AND parent_id=? ORDER BY $order_by",
|
||||
$category->value, $parent_id);
|
||||
return array_map('BookItem::create_instance', $db->fetchAll($q));
|
||||
}
|
||||
|
||||
function books_get_folder(int $id): ?BookItem {
|
||||
$db = DB();
|
||||
$q = $db->query("SELECT * FROM books WHERE id=?", $id);
|
||||
if (!$db->numRows($q))
|
||||
return null;
|
||||
$item = new BookItem($db->fetch($q));
|
||||
if (!$item->isFolder())
|
||||
return null;
|
||||
return $item;
|
||||
}
|
||||
|
||||
function _arabic_to_roman($number) {
|
||||
$map = [
|
||||
1000 => 'M',
|
||||
900 => 'CM',
|
||||
500 => 'D',
|
||||
400 => 'CD',
|
||||
100 => 'C',
|
||||
90 => 'XC',
|
||||
50 => 'L',
|
||||
40 => 'XL',
|
||||
10 => 'X',
|
||||
9 => 'IX',
|
||||
5 => 'V',
|
||||
4 => 'IV',
|
||||
1 => 'I',
|
||||
];
|
||||
$result = '';
|
||||
|
||||
foreach ($map as $arabic => $roman) {
|
||||
while ($number >= $arabic) {
|
||||
$result .= $roman;
|
||||
$number -= $arabic;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
13
routes.php
13
routes.php
@ -1,6 +1,11 @@
|
||||
<?php
|
||||
|
||||
return (function() {
|
||||
require_once 'lib/files.php';
|
||||
$files_collections = array_map(fn(FilesCollection $fn) => $fn->value, FilesCollection::cases());
|
||||
|
||||
$wff = FilesCollection::WilliamFriedman->value;
|
||||
|
||||
$routes = [
|
||||
'Main' => [
|
||||
'/' => 'index',
|
||||
@ -10,6 +15,12 @@ return (function() {
|
||||
'articles/' => 'articles',
|
||||
'articles/([a-z0-9-]+)/' => 'post name=$(1)',
|
||||
],
|
||||
'Files' => [
|
||||
'files/' => 'files',
|
||||
'files/(\d+)/' => 'folder folder_id=$(1)',
|
||||
'files/{'.implode(',', $files_collections).'}/' => 'collection collection=${1}',
|
||||
'files/'.$wff.'/(\d+)/' => 'collection collection='.$wff.' folder_id=$(1)',
|
||||
],
|
||||
'Admin' => [
|
||||
'admin/' => 'index',
|
||||
'admin/{login,logout,log}/' => '${1}',
|
||||
@ -18,7 +29,7 @@ return (function() {
|
||||
'articles/write/' => 'post_add',
|
||||
'articles/([a-z0-9-]+)/{delete,edit}/' => 'post_${1} short_name=$(1)',
|
||||
'admin/markdown-preview.ajax' => 'ajax_md_preview',
|
||||
'admin/uploads/' => 'uploads',
|
||||
'admin/{uploads}/' => '${1}',
|
||||
'admin/uploads/{edit_note,delete}/(\d+)/' => 'upload_${1} id=$(1)'
|
||||
]
|
||||
];
|
||||
|
@ -52,7 +52,6 @@ function index($ctx, $admin_login) {
|
||||
Authorized as <b>{$admin_login}</b> | <a href="/admin/logout/?token={$ctx->csrf('logout')}">Sign out</a><br>
|
||||
<!--<a href="/admin/log/">Log</a><br/>-->
|
||||
<a href="/admin/uploads/">Uploads</a><br>
|
||||
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
@ -166,7 +165,7 @@ if ($is_edit) {
|
||||
$html = <<<HTML
|
||||
<div class="form-error" id="form-error" style="display:none"></div>
|
||||
{$ctx->if_true($saved, fn() => '<div class="form-success">'.$ctx->lang('info_saved').'</div>')}
|
||||
{$ctx->bc($bc_tree, 'padding-bottom: 20px')}
|
||||
{$ctx->bc($bc_tree, 'padding-bottom: 12px')}
|
||||
<table cellpadding="0" cellspacing="0" class="blog-write-table">
|
||||
<tr>
|
||||
<td id="form_first_cell">
|
||||
@ -280,7 +279,7 @@ $form_url = '/'.$short_name.'/'.($is_edit ? 'edit' : 'create').'/';
|
||||
if ($is_edit) {
|
||||
$bc_html = $ctx->bc([
|
||||
['url' => '/'.$short_name.'/', 'text' => $ctx->lang('view_page')]
|
||||
], 'padding-bottom: 20px');
|
||||
], 'padding-bottom: 12px');
|
||||
} else {
|
||||
$bc_html = '';
|
||||
}
|
||||
@ -408,5 +407,15 @@ return <<<HTML
|
||||
<div class="blog-post-text">{$unsafe_html}</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
|
||||
function books($ctx) {
|
||||
return <<<HTML
|
||||
{$ctx->bc([
|
||||
['text' => $ctx->lang('admin_title'), 'url' => '/admin/'],
|
||||
['text' => $ctx->lang('admin_books')],
|
||||
])}
|
||||
|
||||
HTML;
|
||||
}
|
@ -5,7 +5,7 @@ namespace skin\base;
|
||||
use SkinContext;
|
||||
use Stringable;
|
||||
|
||||
function layout($ctx, $title, $unsafe_body, $static, $meta, $js, $opts, $unsafe_lang, $theme, $exec_time, $admin_email) {
|
||||
function layout($ctx, $title, $unsafe_body, $static, $meta, $js, $opts, $unsafe_lang, $theme, $exec_time, $admin_email, $svg_defs) {
|
||||
global $config;
|
||||
$app_config = jsonEncode([
|
||||
'domain' => $config['domain'],
|
||||
@ -33,6 +33,7 @@ return <<<HTML
|
||||
{$ctx->renderStatic($static, $theme)}
|
||||
</head>
|
||||
<body{$ctx->if_true($body_class, ' class="'.implode(' ', $body_class).'"')}>
|
||||
{$ctx->if_true($svg_defs, fn() => $ctx->renderSVGIcons($svg_defs))}
|
||||
<div class="page-content base-width">
|
||||
{$ctx->renderHeader($theme, $opts['head_section'], $opts['articles_lang'], $opts['is_index'])}
|
||||
<div class="page-content-inner">{$unsafe_body}</div>
|
||||
@ -61,6 +62,19 @@ return <<<HTML
|
||||
HTML;
|
||||
}
|
||||
|
||||
function renderSVGIcons($ctx, $svg_defs) {
|
||||
$buf = '<svg style="display: none">';
|
||||
foreach ($svg_defs as $name => $icon) {
|
||||
$buf .= <<<SVG
|
||||
<symbol id="svgicon_{$name}" viewBox="0 0 {$icon['width']} {$icon['height']}" fill="currentColor">
|
||||
{$icon['svg']}
|
||||
</symbol>
|
||||
SVG;
|
||||
}
|
||||
$buf .= '</svg>';
|
||||
return $buf;
|
||||
}
|
||||
|
||||
function renderScript($ctx, $unsafe_js, $unsafe_lang) {
|
||||
global $config;
|
||||
|
||||
@ -190,16 +204,23 @@ function renderHeader(SkinContext $ctx,
|
||||
?string $section,
|
||||
?string $articles_lang,
|
||||
bool $show_subtitle): string {
|
||||
$icons = svg();
|
||||
$items = [];
|
||||
if (is_admin())
|
||||
if (is_admin()) {
|
||||
$items[] = ['url' => '/articles/'.($articles_lang ? '?lang='.$articles_lang : ''), 'label' => 'articles', 'selected' => $section === 'articles'];
|
||||
array_push($items,
|
||||
['url' => 'https://files.4in1.ws', 'label' => 'files', 'selected' => $section === 'files'],
|
||||
['url' => '/info/', 'label' => 'about', 'selected' => $section === 'about']
|
||||
);
|
||||
}
|
||||
$items[] = ['url' => '/files/', 'label' => 'files', 'selected' => $section === 'files'];
|
||||
$items[] = ['url' => '/info/', 'label' => 'about', 'selected' => $section === 'about'];
|
||||
if (is_admin())
|
||||
$items[] = ['url' => '/admin/', 'label' => $ctx->renderSettingsIcon(), 'type' => 'settings', 'selected' => $section === 'admin'];
|
||||
$items[] = ['url' => 'javascript:void(0)', 'label' => $ctx->renderMoonIcons(), 'type' => 'theme-switcher', 'type_opts' => $theme];
|
||||
$items[] = ['url' => '/admin/', 'label' => $icons->settings_28(in_place: true), 'type' => 'settings', 'selected' => $section === 'admin'];
|
||||
$items[] = [
|
||||
'url' => 'javascript:void(0)',
|
||||
'label' => $icons->moon_auto_18(in_place: true)
|
||||
.$icons->moon_light_18(in_place: true)
|
||||
.$icons->moon_dark_18(in_place: true),
|
||||
'type' => 'theme-switcher',
|
||||
'type_opts' => $theme
|
||||
];
|
||||
|
||||
// here, items are rendered using for_each, so that there are no gaps (whitespaces) between tags
|
||||
|
||||
@ -257,32 +278,6 @@ return <<<HTML
|
||||
HTML;
|
||||
}
|
||||
|
||||
|
||||
function renderMoonIcons(SkinContext $ctx): string {
|
||||
return <<<SVG
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" id="moon_auto" display="none">
|
||||
<path fill-rule="evenodd" d="M14.54 10.37c-4.3 1.548-8.458-2.61-6.91-6.91a.59.59 0 0 0-.74-.75 6.66 6.66 0 0 0-2.47 1.54c-3.028 2.985-2.485 8.012 1.111 10.282 3.596 2.269 8.368.596 9.759-3.422a.59.59 0 0 0-.75-.74Z" />
|
||||
<path d="M13.502 6.513V5.194h-1.389q-.802 0-1.195.346-.392.346-.392 1.06 0 .651.398 1.032.398.38 1.078.38.674 0 1.084-.415.416-.416.416-1.084zm1.078-1.934v3.27h.961v.62h-2.039v-.673q-.357.433-.826.639-.469.205-1.096.205-1.037 0-1.646-.551-.61-.55-.61-1.488 0-.967.698-1.5.697-.534 1.968-.534h1.512V4.14q0-.71-.433-1.096-.428-.393-1.207-.393-.645 0-1.026.293-.38.293-.474.868h-.557v-1.26q.562-.24 1.09-.358.533-.123 1.037-.123 1.295 0 1.969.645.68.638.68 1.863z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" id="moon_light" display="none">
|
||||
<path fill-rule="evenodd" d="M14.54 10.37a5.4 5.4 0 0 1-6.91-6.91.59.59 0 0 0-.74-.75 6.66 6.66 0 0 0-2.47 1.54 6.6 6.6 0 1 0 10.87 6.86.59.59 0 0 0-.75-.74zm-1.61 2.39a5.44 5.44 0 0 1-7.69-7.69 5.58 5.58 0 0 1 1-.76 6.55 6.55 0 0 0 7.47 7.47 5.15 5.15 0 0 1-.78.98z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" id="moon_dark" display="none">
|
||||
<path fill-rule="evenodd" d="M14.54 10.37c-4.3 1.548-8.458-2.61-6.91-6.91a.59.59 0 0 0-.74-.75 6.66 6.66 0 0 0-2.47 1.54c-3.028 2.985-2.485 8.012 1.111 10.282 3.596 2.269 8.368.596 9.759-3.422a.59.59 0 0 0-.75-.74Z"/>
|
||||
</svg>
|
||||
SVG;
|
||||
|
||||
}
|
||||
|
||||
function renderSettingsIcon(SkinContext $ctx): string {
|
||||
return <<<SVG
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
|
||||
<path d="M10.648 5.894c1.465-.84 1.719-1.714 1.894-2.588.194-.973.486-.973.972-.973h.972c.486 0 .68 0 .972.973.263.876.447 1.752 1.903 2.592 1.63.443 2.428.003 3.17-.491.825-.55 1.031-.344 1.375 0l.687.687c.344.344.482.481 0 1.375-.433.805-.923 1.555-.487 3.179.84 1.465 1.714 1.719 2.588 1.894.973.194.973.486.973.972v.972c0 .486 0 .68-.973.972-.876.263-1.752.447-2.592 1.903-.443 1.63-.003 2.428.491 3.17.55.825.344 1.031 0 1.375l-.687.687c-.344.344-.481.482-1.375 0-.805-.433-1.555-.923-3.179-.487-1.465.84-1.719 1.714-1.894 2.588-.194.973-.486.973-.972.973h-.972c-.486 0-.68 0-.972-.973-.263-.876-.447-1.752-1.903-2.592-1.63-.443-2.428-.003-3.17.491-.825.55-1.031.344-1.375 0l-.687-.687c-.344-.344-.482-.481 0-1.375.433-.805.923-1.555.487-3.179-.84-1.465-1.714-1.719-2.588-1.894-.973-.194-.973-.486-.973-.972v-.972c0-.486 0-.68.973-.972.876-.263 1.752-.447 2.592-1.903.443-1.63.003-2.428-.491-3.17-.55-.825-.344-1.031 0-1.375l.687-.687c.344-.344.481-.482 1.375 0 .805.433 1.555.923 3.179.487ZM14 19.502a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11Z"/>
|
||||
</svg>
|
||||
SVG;
|
||||
|
||||
}
|
||||
|
||||
function renderFooter($ctx, $admin_email): string {
|
||||
return <<<HTML
|
||||
<div class="footer">
|
||||
|
249
skin/files.phps
Normal file
249
skin/files.phps
Normal file
@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
namespace skin\files;
|
||||
|
||||
use BookFileType;
|
||||
use BookItem;
|
||||
use FilesCollection;
|
||||
use FilesItemInterface;
|
||||
use SkinContext;
|
||||
use SkinString;
|
||||
use function svg;
|
||||
|
||||
function index($ctx,
|
||||
array $collections,
|
||||
array $books,
|
||||
array $misc) {
|
||||
return <<<HTML
|
||||
{$ctx->bc([
|
||||
['text' => $ctx->lang('files_archives')]
|
||||
])}
|
||||
|
||||
<div class="files-list">
|
||||
{$ctx->for_each($collections,
|
||||
fn(FilesItemInterface $file) => $ctx->file(
|
||||
file: $file,
|
||||
disabled: !$file->isAvailable()))}
|
||||
</div>
|
||||
|
||||
{$ctx->bc([
|
||||
['text' => $ctx->lang('files_books')]
|
||||
], mt: true)}
|
||||
|
||||
<div class="files-list">
|
||||
{$ctx->for_each($books, fn(FilesItemInterface $file) => $ctx->file(file: $file))}
|
||||
</div>
|
||||
|
||||
{$ctx->bc([
|
||||
['text' => $ctx->lang('files_misc')]
|
||||
], mt: true)}
|
||||
|
||||
<div class="files-list">
|
||||
{$ctx->for_each($misc, fn(FilesItemInterface $file) => $ctx->file(file: $file))}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function folder($ctx, BookItem $folder, array $files) {
|
||||
$svg = svg();
|
||||
$svg->folder_20(preload_symbol: true);
|
||||
$svg->file_20(preload_symbol: true);
|
||||
|
||||
return <<<HTML
|
||||
{$ctx->bc([
|
||||
['text' => $ctx->lang('files'), 'url' => '/files/'],
|
||||
['text' => $folder->title]
|
||||
])}
|
||||
<div class="files-list">
|
||||
<div id="files_list">
|
||||
{$ctx->collection_files($files)}
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
}
|
||||
|
||||
function collection(SkinContext $ctx,
|
||||
FilesCollection $collection,
|
||||
array $files,
|
||||
?array $parents,
|
||||
int $search_results_per_page,
|
||||
int $search_min_query_length,
|
||||
?string $search_query = null,
|
||||
?int $search_count = null,
|
||||
?array $text_excerpts = null) {
|
||||
$widgets = skin('widgets');
|
||||
|
||||
$svg = svg();
|
||||
$svg->folder_20(preload_symbol: true);
|
||||
$svg->file_20(preload_symbol: true);
|
||||
|
||||
$bc = [
|
||||
['text' => $ctx->lang('files'), 'url' => '/files/'],
|
||||
];
|
||||
if ($parents) {
|
||||
$bc[] = ['text' => $ctx->lang('files_'.$collection->value.'_collection_short'), 'url' => "/files/{$collection->value}/"];
|
||||
for ($i = 0; $i < count($parents); $i++) {
|
||||
$parent = $parents[$i];
|
||||
$bc_item = ['text' => $parent->getTitle()];
|
||||
if ($i < count($parents)-1)
|
||||
$bc_item['url'] = $parent->getUrl();
|
||||
$bc[] = $bc_item;
|
||||
}
|
||||
} else {
|
||||
$bc[] = ['text' => $ctx->lang('files_'.$collection->value.'_collection')];
|
||||
}
|
||||
|
||||
$do_show_search = empty($parents) && $collection->isSearchSupported();
|
||||
$do_show_more = $search_count > 0 && count($files) < $search_count;
|
||||
|
||||
$html = <<<HTML
|
||||
{$ctx->bc($bc)}
|
||||
{$ctx->if_true($do_show_search, fn() => $ctx->collection_search($search_count, $search_query))}
|
||||
|
||||
<div class="files-list">
|
||||
<div id="files_list">
|
||||
{$ctx->collection_files($files, $search_query, $text_excerpts)}
|
||||
</div>
|
||||
<div class="files-list-show-more no-select" id="files_show_more"{$ctx->if_not($do_show_more, ' style="display: none"')}>
|
||||
<span class="files-list-show-more-label">{$ctx->lang('files_show_more')}</span>
|
||||
{$widgets->spinner('files_show_more_spinner')}
|
||||
</div>
|
||||
</div>
|
||||
<div id="files_list_hidden" style="display: none"></div>
|
||||
HTML;
|
||||
|
||||
if ($do_show_search) {
|
||||
$opts = [
|
||||
'container' => 'files_list',
|
||||
'per_page' => $search_results_per_page,
|
||||
'min_query_length' => $search_min_query_length,
|
||||
'base_url' => "/files/{$collection->value}/",
|
||||
'query' => $search_query,
|
||||
'count' => $search_count,
|
||||
'collection_name' => $collection->value,
|
||||
'inited_with_search' => !!$search_query
|
||||
];
|
||||
$opts = jsonEncode($opts);
|
||||
|
||||
$js = <<<JAVASCRIPT
|
||||
cur.search = new FileSearch({$opts});
|
||||
JAVASCRIPT;
|
||||
|
||||
return [$html, $js];
|
||||
} else {
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
function collection_files($ctx,
|
||||
array $files,
|
||||
?string $search_query = null,
|
||||
?array $text_excerpts = null) {
|
||||
return $ctx->for_each($files, fn(FilesItemInterface $f) => $ctx->file(
|
||||
file: $f,
|
||||
unsafe_query: $search_query,
|
||||
text_excerpts: $text_excerpts));
|
||||
}
|
||||
|
||||
function collection_search(SkinContext $ctx, $count, $query) {
|
||||
$icons = svg();
|
||||
$widgets = skin('widgets');
|
||||
$clear_dsp = $query ? 'block' : 'none';
|
||||
|
||||
return <<<HTML
|
||||
<div class="files-search-wrap">
|
||||
<div class="files-search" id="files_search">
|
||||
<div class="files-search-icon">{$icons->search_20()}</div>
|
||||
<input type="text" value="{$query}" placeholder="{$ctx->lang('files_search_ph')}" id="files_search_input">
|
||||
<div class="files-search-clear-icon" id="files_search_clear_icon" style="display: {$clear_dsp}">{$icons->clear_16()}</div>
|
||||
</div>
|
||||
|
||||
<div class="files-search-results-info" id="files_search_info" style="display: {$clear_dsp}">
|
||||
<div class="files-search-results-info-inner">
|
||||
<div class="files-search-results-info-spinner">{$widgets->spinner()}</div>
|
||||
<span id="files_search_info_text">{$ctx->if_then_else($query, fn() => $ctx->lang_num('files_search_results_count', $count), ' ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
|
||||
function file(SkinContext $ctx,
|
||||
FilesItemInterface $file,
|
||||
?SkinString $unsafe_query = null,
|
||||
bool $disabled = false,
|
||||
?array $text_excerpts = null,) {
|
||||
$icons = svg();
|
||||
if ($file instanceof BookItem && $file->fileType == BookFileType::BOOK)
|
||||
$icon = $icons->book_20();
|
||||
else
|
||||
$icon = $file->isFile() ? $icons->file_20() : $icons->folder_20();
|
||||
|
||||
$class = 'files-list-item clearfix';
|
||||
if ($disabled)
|
||||
$class .= ' is-disabled';
|
||||
|
||||
$mapper = function($s) use ($unsafe_query) {
|
||||
if ($unsafe_query !== null) {
|
||||
return hl_matched($s, [$unsafe_query]);
|
||||
} else {
|
||||
return htmlescape($s);
|
||||
}
|
||||
};
|
||||
|
||||
$title = $file->getTitleHtml();
|
||||
if ($title === null) {
|
||||
// we don't apply $mapper to already htmlescaped string
|
||||
$title = $mapper($file->getTitle());
|
||||
}
|
||||
|
||||
$meta = $file->getMeta($unsafe_query);
|
||||
$meta_is_inline = $meta['inline'] ?? false;
|
||||
$meta_items = $meta['items'] ?? [];
|
||||
$url = htmlescape($file->getUrl());
|
||||
|
||||
$subtitle = $file->getSubtitle();
|
||||
|
||||
return <<<HTML
|
||||
<a href="{$url}" class="{$class}" data-id="{$file->getId()}"{$ctx->if_true($file->isTargetBlank(), ' target="_blank"')}>
|
||||
<div class="files-list-item-icon">{$icon}</div>
|
||||
<div class="files-list-item-info">
|
||||
<div class="files-list-item-title">
|
||||
<span class="files-list-item-title-label">{$title}</span>
|
||||
{$ctx->if_true($file->isFolder() && $file->isTargetBlank(), fn() => '<span class="files-list-item-title-label-external-icon">'.$icons->arrow_up_right_out_square_outline_12().'</span>')}
|
||||
{$ctx->if_true($subtitle, fn() => '<span class="files-list-item-subtitle">'.htmlescape($subtitle).'</span>')}
|
||||
{$ctx->if_true($meta_is_inline, $ctx->for_each($meta_items, fn($s) => '<div class="files-list-item-meta-item">'.$s.'</div>'))}
|
||||
</div>
|
||||
{$ctx->if_true($meta_items && !$meta_is_inline, $ctx->meta($meta_items))}
|
||||
{$ctx->if_true(is_array($text_excerpts) && isset($text_excerpts[$file->getId()]),
|
||||
fn() => $ctx->text_excerpt($text_excerpts[$file->getId()]['excerpt'], $text_excerpts[$file->getId()]['index'], $unsafe_query))}
|
||||
</div>
|
||||
</a>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SkinContext $ctx
|
||||
* @param string[] $meta strings are already html-safe
|
||||
* @return string
|
||||
*/
|
||||
function meta($ctx, array $meta) {
|
||||
return <<<HTML
|
||||
<div class="files-list-item-meta">
|
||||
{$ctx->for_each($meta, fn($s) => '<div class="files-list-item-meta-item">'.$s.'</div>')}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
|
||||
function text_excerpt($ctx, $unsafe_excerpt, $index, $unsafe_query) {
|
||||
if ($index > 0)
|
||||
$unsafe_excerpt = '...'.$unsafe_excerpt;
|
||||
$unsafe_excerpt .= '...';
|
||||
$text = hl_matched($unsafe_excerpt, $unsafe_query);
|
||||
return <<<HTML
|
||||
<div class="files-list-item-text-excerpt">{$text}</div>
|
||||
HTML;
|
||||
}
|
71
skin/icons.phps
Normal file
71
skin/icons.phps
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace skin\icons;
|
||||
|
||||
function folder_20($ctx) {
|
||||
return <<<SVG
|
||||
<path fill-rule="evenodd" d="M7.066 2.058C6.812 2 6.552 2 6.256 2h-.09c-.65 0-1.18 0-1.612.034-.446.035-.85.11-1.23.295A3.25 3.25 0 0 0 1.83 3.824c-.185.38-.26.784-.295 1.23-.034.432-.034.962-.034 1.611v5.616c0 .674 0 1.224.037 1.672.037.463.118.882.317 1.272a3.25 3.25 0 0 0 1.42 1.42c.391.2.81.28 1.273.318.447.037.998.037 1.671.037h7.563c.674 0 1.224 0 1.672-.037.463-.038.882-.118 1.272-.317a3.25 3.25 0 0 0 1.42-1.42c.2-.391.28-.81.318-1.273.037-.448.037-.998.037-1.672V8.85c0-.673 0-1.224-.037-1.672-.037-.463-.118-.881-.317-1.272a3.25 3.25 0 0 0-1.42-1.42c-.391-.2-.81-.28-1.273-.318-.447-.037-.998-.037-1.672-.037h-3.486c-.404 0-.484-.004-.55-.02a.75.75 0 0 1-.218-.091c-.059-.036-.118-.09-.403-.377l-.758-.764c-.208-.21-.391-.395-.611-.534a2.25 2.25 0 0 0-.69-.287ZM6.196 3.5c.39 0 .466.004.53.02a.75.75 0 0 1 .23.095c.056.035.113.086.388.363l.759.766c.216.218.406.41.636.552.202.125.423.217.653.273.263.063.534.063.84.063h3.518c.712 0 1.202 0 1.58.031.371.03.57.086.714.16.33.167.598.435.765.764.074.144.13.343.16.714.005.063.009.129.012.199H3v-.805c0-.686 0-1.157.03-1.523.027-.357.08-.55.147-.69a1.75 1.75 0 0 1 .805-.805c.14-.068.333-.12.69-.148.366-.029.837-.03 1.523-.03ZM3 9v3.25c0 .712 0 1.202.032 1.58.03.371.085.57.159.714.167.33.435.597.764.765.145.074.344.129.714.16.38.03.869.03 1.581.03h7.5c.712 0 1.202 0 1.58-.03.371-.031.57-.086.714-.16a1.75 1.75 0 0 0 .765-.765c.074-.144.13-.343.16-.713.03-.38.031-.869.031-1.581V9H3Z" clip-rule="evenodd"/>
|
||||
SVG;
|
||||
}
|
||||
|
||||
function settings_28($ctx) {
|
||||
return <<<SVG
|
||||
<path d="M10.648 5.894c1.465-.84 1.719-1.714 1.894-2.588.194-.973.486-.973.972-.973h.972c.486 0 .68 0 .972.973.263.876.447 1.752 1.903 2.592 1.63.443 2.428.003 3.17-.491.825-.55 1.031-.344 1.375 0l.687.687c.344.344.482.481 0 1.375-.433.805-.923 1.555-.487 3.179.84 1.465 1.714 1.719 2.588 1.894.973.194.973.486.973.972v.972c0 .486 0 .68-.973.972-.876.263-1.752.447-2.592 1.903-.443 1.63-.003 2.428.491 3.17.55.825.344 1.031 0 1.375l-.687.687c-.344.344-.481.482-1.375 0-.805-.433-1.555-.923-3.179-.487-1.465.84-1.719 1.714-1.894 2.588-.194.973-.486.973-.972.973h-.972c-.486 0-.68 0-.972-.973-.263-.876-.447-1.752-1.903-2.592-1.63-.443-2.428-.003-3.17.491-.825.55-1.031.344-1.375 0l-.687-.687c-.344-.344-.482-.481 0-1.375.433-.805.923-1.555.487-3.179-.84-1.465-1.714-1.719-2.588-1.894-.973-.194-.973-.486-.973-.972v-.972c0-.486 0-.68.973-.972.876-.263 1.752-.447 2.592-1.903.443-1.63.003-2.428-.491-3.17-.55-.825-.344-1.031 0-1.375l.687-.687c.344-.344.481-.482 1.375 0 .805.433 1.555.923 3.179.487ZM14 19.502a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11Z"/>
|
||||
SVG;
|
||||
}
|
||||
|
||||
function moon_auto_18($ctx) {
|
||||
return <<<SVG
|
||||
<path fill-rule="evenodd" d="M14.54 10.37c-4.3 1.548-8.458-2.61-6.91-6.91a.59.59 0 0 0-.74-.75 6.66 6.66 0 0 0-2.47 1.54c-3.028 2.985-2.485 8.012 1.111 10.282 3.596 2.269 8.368.596 9.759-3.422a.59.59 0 0 0-.75-.74Z" />
|
||||
<path d="M13.502 6.513V5.194h-1.389q-.802 0-1.195.346-.392.346-.392 1.06 0 .651.398 1.032.398.38 1.078.38.674 0 1.084-.415.416-.416.416-1.084zm1.078-1.934v3.27h.961v.62h-2.039v-.673q-.357.433-.826.639-.469.205-1.096.205-1.037 0-1.646-.551-.61-.55-.61-1.488 0-.967.698-1.5.697-.534 1.968-.534h1.512V4.14q0-.71-.433-1.096-.428-.393-1.207-.393-.645 0-1.026.293-.38.293-.474.868h-.557v-1.26q.562-.24 1.09-.358.533-.123 1.037-.123 1.295 0 1.969.645.68.638.68 1.863z" />
|
||||
SVG;
|
||||
}
|
||||
|
||||
function moon_light_18($ctx) {
|
||||
return <<<SVG
|
||||
<path fill-rule="evenodd" d="M14.54 10.37a5.4 5.4 0 0 1-6.91-6.91.59.59 0 0 0-.74-.75 6.66 6.66 0 0 0-2.47 1.54 6.6 6.6 0 1 0 10.87 6.86.59.59 0 0 0-.75-.74zm-1.61 2.39a5.44 5.44 0 0 1-7.69-7.69 5.58 5.58 0 0 1 1-.76 6.55 6.55 0 0 0 7.47 7.47 5.15 5.15 0 0 1-.78.98z"/>
|
||||
SVG;
|
||||
}
|
||||
|
||||
function moon_dark_18($ctx) {
|
||||
return <<<SVG
|
||||
<path fill-rule="evenodd" d="M14.54 10.37c-4.3 1.548-8.458-2.61-6.91-6.91a.59.59 0 0 0-.74-.75 6.66 6.66 0 0 0-2.47 1.54c-3.028 2.985-2.485 8.012 1.111 10.282 3.596 2.269 8.368.596 9.759-3.422a.59.59 0 0 0-.75-.74Z"/>
|
||||
SVG;
|
||||
}
|
||||
|
||||
function search_20($ctx) {
|
||||
return <<<SVG
|
||||
<path clip-rule="evenodd" d="m9.5 4.5c-2.76142 0-5 2.23858-5 5 0 2.7614 2.23858 5 5 5 2.7614 0 5-2.2386 5-5 0-2.76142-2.2386-5-5-5zm-6.5 5c0-3.58985 2.91015-6.5 6.5-6.5 3.5899 0 6.5 2.91015 6.5 6.5 0 1.5247-.525 2.9268-1.404 4.0353l3.1843 3.1844c.2929.2929.2929.7677 0 1.0606s-.7677.2929-1.0606 0l-3.1844-3.1843c-1.1085.879-2.5106 1.404-4.0353 1.404-3.58985 0-6.5-2.9101-6.5-6.5z" fill="currentColor" fill-rule="evenodd"/>
|
||||
SVG;
|
||||
}
|
||||
|
||||
function file_20($ctx) {
|
||||
return <<<SVG
|
||||
<path fill-rule="evenodd" d="M10.175 1.5H9c-.806 0-1.465.006-2.01.05-.63.052-1.172.16-1.67.413a4.25 4.25 0 0 0-1.857 1.858c-.253.497-.361 1.04-.413 1.67C3 6.103 3 6.864 3 7.816v4.366c0 .952 0 1.713.05 2.327.052.63.16 1.172.413 1.67a4.25 4.25 0 0 0 1.858 1.857c.497.253 1.04.361 1.67.413.613.05 1.374.05 2.326.05h1.366c.952 0 1.713 0 2.327-.05.63-.052 1.172-.16 1.67-.413a4.251 4.251 0 0 0 1.857-1.857c.253-.498.361-1.04.413-1.67.05-.614.05-1.375.05-2.327V8.325c0-.489 0-.733-.055-.963-.05-.205-.13-.4-.24-.579-.123-.201-.296-.374-.642-.72l-3.626-3.626c-.346-.346-.519-.519-.72-.642a2.001 2.001 0 0 0-.579-.24c-.23-.055-.474-.055-.963-.055ZM15.5 12.15c0 .992 0 1.692-.045 2.238-.044.537-.127.86-.255 1.11A2.751 2.751 0 0 1 14 16.7c-.252.128-.574.21-1.111.255-.546.044-1.245.045-2.238.045h-1.3c-.992 0-1.692 0-2.238-.045-.537-.044-.86-.127-1.11-.255A2.75 2.75 0 0 1 4.8 15.5c-.128-.252-.21-.574-.255-1.111-.044-.546-.045-1.245-.045-2.238v-4.3c0-.992 0-1.692.045-2.238.044-.537.127-.86.255-1.11A2.75 2.75 0 0 1 6.002 3.3c.25-.128.573-.21 1.11-.255C7.658 3.001 8.358 3 9.35 3H10v2.35c0 .409 0 .761.024 1.051.026.306.083.61.238.902.21.398.537.724.935.935.291.155.596.212.902.238.29.024.642.024 1.051.024h2.35v3.65ZM14.879 7 11.5 3.621V5.32c0 .447 0 .736.02.955.017.21.047.288.067.326a.75.75 0 0 0 .312.312c.038.02.116.05.326.068.22.018.508.019.955.019h1.699Zm-8.876 7.248a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Zm.75-3.749a.75.75 0 1 0 0 1.5h4.5a.75.75 0 0 0 0-1.5h-4.5Z" clip-rule="evenodd"/>
|
||||
SVG;
|
||||
}
|
||||
|
||||
function book_20($ctx) {
|
||||
return <<<SVG
|
||||
<path clip-rule="evenodd" d="m6.91957 1.5h.03043 6.1.0304c.5342-.00001.98-.00002 1.3443.02974.3799.03104.7365.09815 1.0738.26999.5174.26366.9381.68435 1.2018 1.2018.1718.33726.2389.69392.27 1.0738.0297.36422.0297.81005.0297 1.34418v.03049 9.3c0 .4142-.3358.75-.75.75h-.1885c-.0335.1873-.0615.4371-.0615.75s.028.5627.0615.75h.1885c.4142 0 .75.3358.75.75s-.3358.75-.75.75h-10.99999c-1.24265 0-2.25001-1.0073-2.25001-2.25v-10.8-.03043c-.00001-.53415-.00002-.98001.02974-1.34424.03104-.37988.09815-.73654.26999-1.0738.26366-.51745.68435-.93814 1.2018-1.2018.33726-.17184.69392-.23895 1.0738-.26999.36423-.02976.81009-.02975 1.34424-.02974zm7.62533 15.5c-.0272-.218-.0449-.4681-.0449-.75s.0177-.532.0449-.75h-9.29489c-.41422 0-.75001.3358-.75001.75s.33579.75.75001.75zm.9551-11.55v8.55h-10.24999c-.26298 0-.51542.0451-.75001.128v-8.678c0-.57243.00058-.95664.02476-1.25252.02346-.28712.06534-.42441.11148-.51497.11984-.2352.31107-.42642.54627-.54627.09056-.04614.22785-.08802.51497-.11148.29588-.02417.68009-.02476 1.25252-.02476h6.1c.5724 0 .9566.00059 1.2525.02476.2871.02346.4244.06534.515.11148.2352.11985.4264.31107.5463.54627.0461.09056.088.22785.1114.51497.0242.29588.0248.68009.0248 1.25252z" fill="currentColor" fill-rule="evenodd"/>
|
||||
SVG;
|
||||
}
|
||||
|
||||
function clear_20($ctx) {
|
||||
return <<<SVG
|
||||
<path clip-rule="evenodd" d="m20 10c0-5.52285-4.4772-10-10-10-5.52285 0-10 4.47715-10 10 0 5.5228 4.47715 10 10 10 5.5228 0 10-4.4772 10-10zm-12.29289-3.70711c-.39053-.39052-1.02369-.39052-1.41422 0-.39052.39053-.39052 1.02369 0 1.41422l2.2929 2.29289-2.2929 2.2929c-.39052.3905-.39052 1.0237 0 1.4142.39053.3905 1.02369.3905 1.41422 0l2.29289-2.2929 2.2929 2.2929c.3905.3905 1.0237.3905 1.4142 0s.3905-1.0237 0-1.4142l-2.2929-2.2929 2.2929-2.29289c.3905-.39053.3905-1.02369 0-1.41422-.3905-.39052-1.0237-.39052-1.4142 0l-2.2929 2.2929z" fill="currentColor" fill-rule="evenodd"/>
|
||||
SVG;
|
||||
}
|
||||
|
||||
function clear_16($ctx) {
|
||||
return <<<SVG
|
||||
<path fill-rule="evenodd" d="M16 8A8 8 0 1 0 0 8a8 8 0 0 0 16 0ZM4.563 4.564a.9.9 0 0 0 0 1.272L6.727 8l-2.164 2.164a.9.9 0 1 0 1.273 1.272L8 9.273l2.164 2.163a.9.9 0 0 0 1.272-1.272L9.273 8l2.163-2.164a.9.9 0 1 0-1.272-1.272L8 6.727 5.836 4.564a.9.9 0 0 0-1.273 0Z" clip-rule="evenodd"/>
|
||||
SVG;
|
||||
}
|
||||
|
||||
function arrow_up_right_out_square_outline_12($ctx) {
|
||||
return <<<SVG
|
||||
<path fill="currentColor" d="M4.25 1a.75.75 0 1 1 0 1.5c-.5006 0-.6414.0036-.7439.024a1.25 1.25 0 0 0-.982.9821c-.0205.1025-.0241.2433-.0241.7439v2.8c0 .5724.0006.9566.0248 1.2525.0234.2871.0653.4244.1114.515a1.25 1.25 0 0 0 .5463.5463c.0906.0461.2279.088.515.1114.2959.0242.68.0248 1.2525.0248h2.8c.5006 0 .6414-.0036.7439-.024a1.25 1.25 0 0 0 .982-.9821c.0205-.1025.0241-.2433.0241-.7439a.75.75 0 0 1 1.5 0c0 .4287.0036.7526-.0528 1.0365a2.7501 2.7501 0 0 1-2.1607 2.1607c-.2675.0532-.5705.053-.9632.0528-.968-.0005-1.9358 0-2.9037 0-.5342 0-.98 0-1.3443-.0297-.3798-.0311-.7365-.0982-1.0738-.27a2.7504 2.7504 0 0 1-1.2018-1.2018c-.1718-.3373-.239-.694-.27-1.0738C1 8.0604 1 7.6146 1 7.0804c0-.9679.0005-1.9358 0-2.9037-.0002-.3927-.0004-.6957.0528-.9632a2.75 2.75 0 0 1 2.1607-2.1607C3.4975.9964 3.8213 1 4.25 1Z M7 .75A.75.75 0 0 1 7.75 0h3.5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0V2.5607L7.2803 5.7803a.75.75 0 0 1-1.0606-1.0606L9.4393 1.5H7.75A.75.75 0 0 1 7 .75Z"/>
|
||||
SVG;
|
||||
|
||||
}
|
14
skin/widgets.phps
Normal file
14
skin/widgets.phps
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace skin\widgets;
|
||||
|
||||
function spinner($ctx,
|
||||
$id = null) {
|
||||
return <<<HTML
|
||||
<div class="spinner"{$ctx->if_true($id, ' id="'.$id.'"')}>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
@ -88,7 +88,36 @@ err_pages_no_id: 'ID not specified'
|
||||
err_pages_no_short_name: 'Short name not specified'
|
||||
err_pages_db_err: 'Database error'
|
||||
|
||||
# admin-switch
|
||||
admin_title: "Admin"
|
||||
admin_password: 'Password'
|
||||
admin_login: "Login"
|
||||
# /admin
|
||||
admin_title: Admin
|
||||
admin_password: Password
|
||||
admin_login: Login
|
||||
admin_books: Books
|
||||
|
||||
# /files
|
||||
files: Files
|
||||
files_collections: Collections
|
||||
files_archives: Archives
|
||||
files_books: Books
|
||||
files_misc: Miscellaneous
|
||||
files_count:
|
||||
- "%s file"
|
||||
- "%s files"
|
||||
- "%s files"
|
||||
- "No files"
|
||||
|
||||
files_wff_collection: William F. Friedman NSA Archive
|
||||
files_mdf_collection: Mercure de France (1920-1940)
|
||||
files_baconiana_collection: Baconiana
|
||||
|
||||
files_wff_collection_short: W.F.F. Archive
|
||||
files_mdf_collection_short: MdF
|
||||
files_baconiana_collection_short: Baconiana
|
||||
|
||||
files_search_ph: Document number, title or text
|
||||
files_search_results_count:
|
||||
- "%s result"
|
||||
- "%s results"
|
||||
- "%s results"
|
||||
- "No results"
|
||||
files_show_more: Show more
|
Loading…
x
Reference in New Issue
Block a user