4in1_ws_web/skin/base.phps
2024-03-14 23:33:51 +00:00

298 lines
9.5 KiB
PHP

<?php
namespace skin\base;
use SkinContext;
use Stringable;
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'],
'devMode' => $config['is_dev'],
'cookieHost' => $config['cookie_host'],
]);
$body_class = [];
if ($opts['full_width'])
$body_class[] = 'full-width';
else if ($opts['wide'])
$body_class[] = 'wide';
return <<<HTML
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="alternate" type="application/rss+xml" href="/feed.rss">
<title>{$title}</title>
<script type="text/javascript">window.appConfig = {$app_config};</script>
{$ctx->meta($meta)}
{$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>
{$ctx->if_not($opts['full_width'], fn() => $ctx->renderFooter($admin_email))}
</div>
{$ctx->renderScript($js, $unsafe_lang)}
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(96032069, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/96032069" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>
{$ctx->if_admin(fn() => "<!-- {$exec_time} s -->")}
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;
$styles = jsonEncode($ctx->styleNames);
if ($config['is_dev'])
$versions = '{}';
else {
$versions = [];
foreach ($config['static'] as $name => $v) {
list($type, $bname) = getStaticNameParts($name);
$versions[$type][$bname] = $v;
}
$versions = jsonEncode($versions);
}
return <<<HTML
<script type="text/javascript">
StaticManager.init({$styles}, {$versions});
{$ctx->if_true($unsafe_js, '(function(){try{'.$unsafe_js.'}catch(e){window.console&&console.error("caught exception:",e)}})();')}
{$ctx->if_true($unsafe_lang, 'extend(__lang, '.$unsafe_lang.');')}
ThemeSwitcher.init();
</script>
HTML;
}
function meta($ctx, $meta) {
if (empty($meta))
return '';
return implode('', array_map(function(array $item): string {
$s = '<meta';
foreach ($item as $k => $v)
$s .= ' '.htmlescape($k).'="'.htmlescape($v).'"';
$s .= '>';
$s .= "\n";
return $s;
}, $meta));
}
function renderStatic($ctx, $static, $theme) {
global $config;
$html = [];
$dark = $theme == 'dark';
$ctx->styleNames = [];
foreach ($static as $name) {
// javascript
if (str_starts_with($name, 'js/'))
$html[] = jsLink($name);
// css
else if (str_starts_with($name, 'css/')) {
$html[] = cssLink($name, 'light', $style_name);
$ctx->styleNames[] = $style_name;
if ($dark)
$html[] = cssLink($name, 'dark', $style_name);
else if (!$config['is_dev'])
$html[] = cssPrefetchLink($style_name.'_dark');
}
else
logError(__FUNCTION__.': unexpected static entry: '.$name);
}
return implode("\n", $html);
}
function jsLink(string $name): string {
global $config;
list (, $bname) = getStaticNameParts($name);
if ($config['is_dev']) {
$href = '/js.php?name='.urlencode($bname).'&amp;v='.time();
} else {
$href = '/dist-js/'.$bname.'.js?'.getStaticVersion($name);
}
return '<script src="'.$href.'" type="text/javascript"'.getStaticIntegrityAttribute($name).'></script>';
}
function cssLink(string $name, string $theme, &$bname = null): string {
list(, $bname) = getStaticNameParts($name);
$config_name = 'css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css';
if (is_dev()) {
$href = '/sass.php?name='.urlencode($bname).'&amp;theme='.$theme.'&amp;v='.time();
} else {
$version = getStaticVersion($config_name);
$href = '/dist-css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css?'.$version;
}
$id = 'style_'.$bname;
if ($theme == 'dark')
$id .= '_dark';
return '<link rel="stylesheet" id="'.$id.'" type="text/css" href="'.$href.'"'.getStaticIntegrityAttribute($config_name).'>';
}
function cssPrefetchLink(string $name): string {
$url = '/dist-css/'.$name.'.css?'.getStaticVersion('css/'.$name.'.css');
$integrity = getStaticIntegrityAttribute('css/'.$name.'.css');
return <<<HTML
<link rel="prefetch" href="{$url}"{$integrity} />
HTML;
}
function getStaticNameParts(string $name): array {
$dname = dirname($name);
$bname = basename($name);
if (($pos = strrpos($bname, '.'))) {
$ext = substr($bname, $pos+1);
$bname = substr($bname, 0, $pos);
} else {
$ext = '';
}
return [$dname, $bname, $ext];
}
function getStaticVersion(string $name): string {
global $config;
if ($config['is_dev'])
return time();
if (str_starts_with($name, '/')) {
logWarning(__FUNCTION__.': '.$name.' starts with /');
$name = substr($name, 1);
}
return $config['static'][$name]['version'] ?? 'notfound';
}
function getStaticIntegrityAttribute(string $name): string {
if (is_dev())
return '';
global $config;
return ' integrity="'.implode(' ', array_map(fn($hash_type) => $hash_type.'-'.$config['static'][$name]['integrity'][$hash_type], RESOURCE_INTEGRITY_HASHES)).'"';
}
function renderHeader(SkinContext $ctx,
string $theme,
?string $section,
?string $articles_lang,
bool $show_subtitle): string {
$icons = svg();
$items = [];
if (is_admin()) {
$items[] = ['url' => '/articles/'.($articles_lang ? '?lang='.$articles_lang : ''), 'label' => 'articles', 'selected' => $section === 'articles'];
}
$items[] = ['url' => '/files/', 'label' => 'files', 'selected' => $section === 'files'];
$items[] = ['url' => '/info/', 'label' => 'about', 'selected' => $section === 'about'];
if (is_admin())
$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
$class = 'head';
if (!$show_subtitle)
$class .= ' no-subtitle';
return <<<HTML
<div class="{$class}">
<div class="head-inner">
<div class="head-logo-wrap">
<div class="head-logo">
<a href="/">
<div class="head-logo-title">4in1 <span class="head-logo-title-author">by idb</span></div>
{$ctx->if_true($show_subtitle, '<div class="head-logo-subtitle">Mask of Shakespeare, Mysteries of Bacon, <br>Book by Cartier, Secrets of the NSA</div>')}
</a>
</div>
</div>
<div class="head-items">
{$ctx->for_each($items, fn($item) => $ctx->renderHeaderItem(
$item['url'],
$item['label'],
$item['type'] ?? false,
$item['type_opts'] ?? null,
$item['selected'] ?? false
))}
</div>
</div>
</div>
HTML;
}
function renderHeaderItem(SkinContext $ctx,
string $url,
?Stringable $unsafe_label,
?string $type,
?string $type_opts,
bool $selected): string {
$args = '';
$class = '';
switch ($type) {
case 'theme-switcher':
$args = ' onclick="return ThemeSwitcher.next(event)"';
$class = ' is-theme-switcher '.$type_opts;
break;
case 'settings':
$class = ' is-settings';
break;
}
if ($selected)
$class .= ' is-selected';
return <<<HTML
<a class="head-item{$class}" href="{$url}"{$args}>{$unsafe_label}</a>
HTML;
}
function renderFooter($ctx, $admin_email): string {
return <<<HTML
<div class="footer">
Email: <a href="mailto:{$admin_email}">{$admin_email}</a>
</div>
HTML;
}