294 lines
11 KiB
PHP
294 lines
11 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) {
|
|
global $config;
|
|
$app_config = json_encode([
|
|
'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->renderMeta($meta)}
|
|
{$ctx->renderStatic($static, $theme)}
|
|
</head>
|
|
<body{$ctx->if_true($body_class, ' class="'.implode(' ', $body_class).'"')}>
|
|
<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>
|
|
<!-- {$exec_time} s -->
|
|
HTML;
|
|
}
|
|
|
|
function renderScript($ctx, $unsafe_js, $unsafe_lang) {
|
|
global $config;
|
|
|
|
$styles = json_encode($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 = json_encode($versions);
|
|
}
|
|
|
|
return <<<HTML
|
|
<script type="text/javascript">
|
|
StaticManager.init({$styles}, {$versions});
|
|
{$ctx->if_true($unsafe_js, '(function(){'.$unsafe_js.'})();')}
|
|
{$ctx->if_true($unsafe_lang, 'extend(__lang, '.$unsafe_lang.');')}
|
|
ThemeSwitcher.init();
|
|
</script>
|
|
HTML;
|
|
}
|
|
|
|
function renderMeta($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 .= '>';
|
|
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).'&v='.time();
|
|
} else {
|
|
$href = '/dist-js/'.$bname.'.js?'.getStaticVersion($name);
|
|
}
|
|
return '<script src="'.$href.'" type="text/javascript"></script>';
|
|
}
|
|
|
|
function cssLink(string $name, string $theme, &$bname = null): string {
|
|
global $config;
|
|
|
|
list(, $bname) = getStaticNameParts($name);
|
|
|
|
if ($config['is_dev']) {
|
|
$href = '/sass.php?name='.urlencode($bname).'&theme='.$theme.'&v='.time();
|
|
} else {
|
|
$version = getStaticVersion('css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css');
|
|
$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.'">';
|
|
}
|
|
|
|
function cssPrefetchLink(string $name): string {
|
|
$url = '/dist-css/'.$name.'.css?'.getStaticVersion('css/'.$name.'.css');
|
|
return <<<HTML
|
|
<link rel="prefetch" href="{$url}" />
|
|
HTML;
|
|
}
|
|
|
|
function getStaticNameParts(string $name): array {
|
|
$dname = dirname($name);
|
|
$bname = basename($name);
|
|
if (($pos = strrpos($bname, '.'))) {
|
|
$ext = substr($bname, $pos+1);
|
|
$bname = substr($bname, 0, $pos);
|
|
} else {
|
|
$ext = '';
|
|
}
|
|
return [$dname, $bname, $ext];
|
|
}
|
|
|
|
function getStaticVersion(string $name): string {
|
|
global $config;
|
|
if ($config['is_dev'])
|
|
return time();
|
|
if (str_starts_with($name, '/')) {
|
|
logWarning(__FUNCTION__.': '.$name.' starts with /');
|
|
$name = substr($name, 1);
|
|
}
|
|
return $config['static'][$name] ?? 'notfound';
|
|
}
|
|
|
|
|
|
function renderHeader(SkinContext $ctx,
|
|
string $theme,
|
|
?string $section,
|
|
?string $articles_lang,
|
|
bool $show_subtitle): string {
|
|
$items = [];
|
|
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']
|
|
);
|
|
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];
|
|
|
|
// 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 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">
|
|
Email: <a href="mailto:{$admin_email}">{$admin_email}</a>
|
|
</div>
|
|
HTML;
|
|
|
|
}
|