This commit is contained in:
E. S. 2024-01-01 00:57:22 +00:00
parent 7aa78dd7e4
commit fba84cd77a
19 changed files with 149 additions and 106 deletions

View File

@ -0,0 +1,19 @@
<?php
namespace handler;
use admin;
use posts;
class ArticlesHandler extends \RequestHandler
{
public function get(): \Response {
$posts = posts::getPosts(include_hidden: admin::isAdmin());
//$tags = posts::getAllTags(include_hidden: admin::isAdmin());
$this->skin->title = \LangData::getInstance()['articles'];
return $this->skin->renderPage('main/articles',
posts: $posts);
}
}

View File

@ -8,11 +8,16 @@ use posts;
class IndexHandler extends \RequestHandler {
public function get(): \Response {
$page = \pages::getPageByName('index-wgm9Fkl');
global $config;
$page = \pages::getPageByName($config['index_page_id']);
$this->skin->title = $page->title;
$this->skin->fixedTitle = true;
return $this->skin->renderPage('main/index',
unsafe_content: $page->getHtml($this->isRetina(), \themes::getUserTheme()));
return $this->skin->renderPage('main/page',
unsafe_html: $page->getHtml($this->isRetina(), \themes::getUserTheme()),
page_url: $page->getUrl(),
short_name: $page->shortName);
}
}

View File

@ -7,6 +7,7 @@ use Response;
class IndexHandler extends AdminRequestHandler {
public function get(): Response {
$this->skin->title = 'Admin';
return $this->skin->renderPage('admin/index');
}

View File

@ -13,13 +13,13 @@ class UploadDeleteHandler extends AdminRequestHandler {
$upload = \uploads::get($id);
if (!$upload)
return new RedirectResponse('/uploads/?error='.urlencode('upload not found'));
return new RedirectResponse('/admin/uploads/?error='.urlencode('upload not found'));
csrf::check('delupl'.$id);
\uploads::delete($id);
return new RedirectResponse('/uploads/');
return new RedirectResponse('/admin/uploads/');
}
}

View File

@ -12,14 +12,14 @@ class UploadEditNoteHandler extends AdminRequestHandler {
$upload = \uploads::get($id);
if (!$upload)
return new \RedirectResponse('/uploads/?error='.urlencode('upload not found'));
return new \RedirectResponse('/admin/uploads/?error='.urlencode('upload not found'));
csrf::check('editupl'.$id);
$note = $_POST['note'] ?? '';
$upload->setNote($note);
return new \RedirectResponse('/uploads/');
return new \RedirectResponse('/admin/uploads/');
}
}

View File

@ -29,7 +29,7 @@ class UploadsHandler extends AdminRequestHandler {
list($custom_name, $note) = $this->input('name, note');
if (!isset($_FILES['files']))
return new RedirectResponse('/uploads/?error='.urlencode('no file'));
return new RedirectResponse('/admin/uploads/?error='.urlencode('no file'));
$files = [];
for ($i = 0; $i < count($_FILES['files']['name']); $i++) {
@ -49,14 +49,14 @@ class UploadsHandler extends AdminRequestHandler {
foreach ($files as $f) {
if ($f['error'])
return new RedirectResponse('/uploads/?error='.urlencode('error code '.$f['error']));
return new RedirectResponse('/admin/uploads/?error='.urlencode('error code '.$f['error']));
if (!$f['size'])
return new RedirectResponse('/uploads/?error='.urlencode('received empty file'));
return new RedirectResponse('/admin/uploads/?error='.urlencode('received empty file'));
$ext = extension($f['name']);
if (!\uploads::isExtensionAllowed($ext))
return new RedirectResponse('/uploads/?error='.urlencode('extension not allowed'));
return new RedirectResponse('/admin/uploads/?error='.urlencode('extension not allowed'));
$upload_id = \uploads::add(
$f['tmp_name'],
@ -64,10 +64,10 @@ class UploadsHandler extends AdminRequestHandler {
$note);
if (!$upload_id)
return new RedirectResponse('/uploads/?error='.urlencode('failed to create upload'));
return new RedirectResponse('/admin/uploads/?error='.urlencode('failed to create upload'));
}
return new RedirectResponse('/uploads/');
return new RedirectResponse('/admin/uploads/');
}
}

View File

@ -33,7 +33,7 @@ class markup {
global $config;
$is_dark_theme = $user_theme === 'dark';
return preg_replace_callback(
'/('.preg_quote($config['uploads_host'], '/').'\/\w{8}\/)([ap])(\d+)x(\d+)(\.jpg)/',
'/(uploads\/\w{8}\/)([ap])(\d+)x(\d+)(\.jpg)/',
function($match) use ($is_retina, $is_dark_theme) {
$mult = $is_retina ? 2 : 1;
$is_alpha = $match[2] == 'a';

View File

@ -1,7 +1,6 @@
<?php
namespace model;
use Model;
use themes;
class Upload extends Model
@ -30,7 +29,7 @@ class Upload extends Model
public function getDirectUrl(): string {
global $config;
return 'https://'.$config['uploads_host'].'/'.$this->randomId.'/'.$this->name;
return $config['uploads_path'].'/'.$this->randomId.'/'.$this->name;
}
public function getDirectPreviewUrl(int $w, int $h, bool $retina = false): string {
@ -44,11 +43,11 @@ class Upload extends Model
}
$prefix = $this->imageMayHaveAlphaChannel() ? 'a' : 'p';
return 'https://'.$config['uploads_host'].'/'.$this->randomId.'/'.$prefix.$w.'x'.$h.'.jpg';
return $config['uploads_path'].'/'.$this->randomId.'/'.$prefix.$w.'x'.$h.'.jpg';
}
// TODO remove?
public function incrementDownloads() {
public function incrementDownloads(): void {
$db = getDb();
$db->query("UPDATE uploads SET downloads=downloads+1 WHERE id=?", $this->id);
$this->downloads++;
@ -70,7 +69,7 @@ class Upload extends Model
return $md;
}
public function setNote(string $note) {
public function setNote(string $note): void {
$db = getDb();
$db->query("UPDATE uploads SET note=? WHERE id=?", $note, $this->id);
}

View File

@ -82,7 +82,7 @@ class uploads {
public static function getAll(): array {
$db = getDb();
$q = $db->query("SELECT * FROM uploads ORDER BY id DESC");
return array_map('\Upload', $db->fetchAll($q));
return array_map('model\Upload::create_instance', $db->fetchAll($q));
}
public static function get(int $id): ?Upload {

View File

@ -18,10 +18,10 @@ return [
'password_salt' => '12345',
'csrf_token' => '12345',
'uploads_dir' => '/home/user/files.example.com',
'uploads_host' => 'files.example.com',
'dirs_mode' => 0775,
'files_mode' => 0664,
'group' => 33, // id -g www-data
'is_dev' => false,
'index_page_id' => 'index-wgm9Fkl'
];

View File

@ -6,20 +6,20 @@ $r = (new Router())
// route handler input
// ----- ------- -----
->add('/', 'index')
->add('([a-zA-Z0-9\-]+)/', 'auto name=$(1)')
->add('([a-zA-Z0-9\-]+)/', 'auto name=$(1)')
->add('feed.rss', 'RSS')
//->add('articles/', 'articles')
->add('articles/write/', 'admin/post_add')
// admin
->add('admin/', 'admin/index')
->add('admin/{login,logout,log}/', 'admin/${1}')
->add('([a-zA-Z0-9\-]+)/{delete,edit}/', 'admin/auto_${1} short_name=$(1)')
->add('([a-zA-Z0-9\-]+)/create/', 'admin/page_add short_name=$(1)')
//->add('write/', 'admin/post_add')
->add('([a-zA-Z0-9\-]+)/{delete,edit}/', 'admin/auto_${1} short_name=$(1)')
->add('([a-zA-Z0-9\-]+)/create/', 'admin/page_add short_name=$(1)')
->add('admin/markdown-preview.ajax', 'admin/markdown_preview')
->add('uploads/', 'admin/uploads')
->add('uploads/{edit_note,delete}/(\d+)/','admin/upload_${1} id=$(1)')
->add('admin/uploads/', 'admin/uploads')
->add('admin/uploads/{edit_note,delete}/(\d+)/', 'admin/upload_${1} id=$(1)')
;
(new RequestDispatcher($r))->dispatch();

View File

@ -85,7 +85,6 @@ function cancelEvent(evt) {
return false;
}
//
// Cookies
//

View File

@ -129,11 +129,14 @@ var ThemeSwitcher = (function() {
}
/**
* @param {string} mode
* @param {string} selectedMode
*/
function setLabel(mode) {
var labelEl = ge('theme-switcher-label');
labelEl.innerHTML = escape(lang('theme_'+mode));
function setIcon(selectedMode) {
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';
}
}
return {
@ -171,7 +174,7 @@ var ThemeSwitcher = (function() {
onSystemChange(window.matchMedia('(prefers-color-scheme: dark)').matches === true);
}
setLabel(modes[currentModeIndex]);
setIcon(modes[currentModeIndex]);
},
next: function(e) {
@ -198,7 +201,7 @@ var ThemeSwitcher = (function() {
break;
}
setLabel(modes[currentModeIndex]);
setIcon(modes[currentModeIndex]);
setCookie('theme', modes[currentModeIndex]);
return cancelEvent(e);

View File

@ -60,7 +60,19 @@ a.head-item {
font-size: $fs - 1px;
display: inline-block;
padding: 8px 12px;
//margin-right: -16px;
vertical-align: middle;
&.is-theme-switcher,
&.is-settings {
padding: 8px;
}
&.is-settings {
font-size: 0;
> svg {
width: 18px;
height: 18px;
}
}
&:hover {
border-radius: 4px;
@ -68,31 +80,13 @@ a.head-item {
text-decoration: none;
}
> span {
position: relative;
> span {
padding: 2px 0;
&.moon-icon {
padding: 0;
position: absolute;
top: 0;
left: 0;
> svg path {
fill: $fg;
}
}
}
}
&.is-theme-switcher > span {
padding-left: 20px;
}
//&:last-child > span {
// border-right: 0;
// padding-right: 1px;
//}
}
body a.head-item.is-theme-switcher svg path,
body a.head-item.is-settings svg path {
fill: $fg;
}

View File

@ -8,10 +8,9 @@ return [
'posts' => 'posts',
'all_posts' => 'all posts',
'blog' => 'blog',
'articles' => 'Articles',
'contacts' => 'contacts',
'email' => 'email',
'projects' => 'projects',
'unknown_error' => 'Unknown error',
'error' => 'Error',
'write' => 'Write',
@ -22,9 +21,9 @@ return [
'toc' => 'Table of Contents',
// theme switcher
'theme_auto' => 'auto',
'theme_dark' => 'dark',
'theme_light' => 'light',
//'theme_auto' => 'auto',
//'theme_dark' => 'dark',
//'theme_light' => 'light',
// contacts
'contacts_email' => 'email',
@ -40,7 +39,6 @@ return [
'blog_post_hidden' => 'Hidden',
'blog_tag_title' => 'Posts tagged with "%s"',
'blog_tag_not_found' => 'No posts found.',
'blog_comments_text' => 'If you have any comments, <a href="mailto:%s?subject=%s">contact me by email</a>.',
'blog_write_form_preview_btn' => 'Preview',
'blog_write_form_submit_btn' => 'Submit',

View File

@ -24,8 +24,7 @@ class Upload extends Model {
}
public function getDirectUrl(): string {
global $config;
return 'https://'.$config['uploads_host'].'/'.$this->randomId.'/'.$this->name;
return '/upload/'.$this->randomId.'/'.$this->name;
}
public function getDirectPreviewUrl(int $w, int $h, bool $retina = false): string {
@ -39,7 +38,7 @@ class Upload extends Model {
}
$prefix = $this->imageMayHaveAlphaChannel() ? 'a' : 'p';
return 'https://'.$config['uploads_host'].'/'.$this->randomId.'/'.$prefix.$w.'x'.$h.'.jpg';
return $config['uploads_dir'].'/'.$this->randomId.'/'.$prefix.$w.'x'.$h.'.jpg';
}
// TODO remove?

View File

@ -40,7 +40,8 @@ return [$html, $js];
function index($ctx) {
return <<<HTML
<div class="admin-page">
<!-- <a href="/admin/log/">Log</a><br/>-->
<!--<a href="/admin/log/">Log</a><br/>-->
<a href="/admin/uploads/">Uploads</a><br>
<a href="/admin/logout/?token={$ctx->csrf('logout')}">Sign out</a>
</div>
HTML;
@ -55,7 +56,7 @@ return <<<HTML
{$ctx->if_true($error, $ctx->formError, $error)}
<div class="blog-upload-form">
<form action="/uploads/" method="post" enctype="multipart/form-data" class="form-layout-h">
<form action="/admin/uploads/" method="post" enctype="multipart/form-data" class="form-layout-h">
<input type="hidden" name="token" value="{$ctx->csrf('addupl')}" />
<div class="form-field-wrap clearfix">
@ -107,8 +108,8 @@ return <<<HTML
<div class="blog-upload-item">
<div class="blog-upload-item-actions">
<a href="javascript:void(0)" onclick="var mdel = ge('upload{$id}_md'); mdel.style.display = (mdel.style.display === 'none' ? 'block' : 'none')">{$ctx->lang('blog_upload_show_md')}</a>
| <a href="javascript:void(0)" onclick="BlogUploadList.submitNoteEdit('/uploads/edit_note/{$id}/?token={$ctx->csrf('editupl'.$id)}', prompt('Note:', '{$addslashes_note}'))">Edit note</a>
| <a href="/uploads/delete/{$id}/?token={$ctx->csrf('delupl'.$id)}" onclick="return confirm('{$ctx->lang('blog_upload_delete_confirmation')}')">{$ctx->lang('blog_upload_delete')}</a>
| <a href="javascript:void(0)" onclick="BlogUploadList.submitNoteEdit('/admin/uploads/edit_note/{$id}/?token={$ctx->csrf('editupl'.$id)}', prompt('Note:', '{$addslashes_note}'))">Edit note</a>
| <a href="/admin/uploads/delete/{$id}/?token={$ctx->csrf('delupl'.$id)}" onclick="return confirm('{$ctx->lang('blog_upload_delete_confirmation')}')">{$ctx->lang('blog_upload_delete')}</a>
</div>
<div class="blog-upload-item-name"><a href="{$direct_url}">{$name}</a></div>
{$ctx->if_true($note, '<div class="blog-upload-item-note">'.$note.'</div>')}

View File

@ -192,8 +192,8 @@ $items = [
['url' => '/about/', 'label' => 'about']
];
if (\admin::isAdmin())
$items[] = ['url' => '/admin/', 'label' => 'admin'];
$items[] = ['url' => 'javascript:void(0)', 'label' => $theme, 'label_id' => 'theme-switcher-label', 'theme_switcher' => true];
$items[] = ['url' => '/admin/', 'label' => $ctx->renderSettingsIcon(), 'type' => 'settings'];
$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
@ -209,7 +209,12 @@ return <<<HTML
</div>
</div>
<div class="head-items">
{$ctx->for_each($items, fn($item) => $ctx->renderHeaderItem($item['url'], $item['label'], $item['label_id'] ?? null, $item['theme_switcher'] ?? false))}
{$ctx->for_each($items, fn($item) => $ctx->renderHeaderItem(
$item['url'],
$item['label'],
$item['type'] ?? false,
$item['type_opts'] ?? null,
))}
</div>
</div>
</div>
@ -219,22 +224,47 @@ HTML;
function renderHeaderItem(SkinContext $ctx,
string $url,
string $label,
?Stringable $label_id,
bool $is_theme_switcher): string {
?Stringable $unsafe_label,
?string $type,
?string $type_opts): 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;
}
return <<<HTML
<a class="head-item{$ctx->if_true($is_theme_switcher, ' is-theme-switcher')}" href="{$url}"{$ctx->if_true($is_theme_switcher, ' onclick="return ThemeSwitcher.next(event)"')}>
<span>
{$ctx->if_true($is_theme_switcher, '<span class="moon-icon">'.$ctx->renderMoonIcon().'</span>')}
<span{$ctx->if_true($label_id, ' id="'.$label_id.'"')}>{$label}</span>
</span>
</a>
<a class="head-item{$class}" href="{$url}"{$args}>{$unsafe_label}</a>
HTML;
}
function renderMoonIcon(SkinContext $ctx): string {
function renderMoonIcons(SkinContext $ctx): string {
return <<<SVG
<svg width="18" height="18" xmlns="http://www.w3.org/2000/svg"><path d="M14.54 10.37a5.4 5.4 0 01-6.91-6.91.59.59 0 00-.74-.75 6.66 6.66 0 00-2.47 1.54 6.6 6.6 0 1010.87 6.86.59.59 0 00-.75-.74zm-1.61 2.39a5.44 5.44 0 01-7.69-7.69 5.58 5.58 0 011-.76 6.55 6.55 0 007.47 7.47 5.15 5.15 0 01-.78.98z" fill-rule="evenodd" /></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;
}

View File

@ -13,25 +13,24 @@ function index($ctx, $unsafe_content) {
HTML;
}
//function articles($ctx): string {
//return <<<HTML
//<div class="empty">
// {$ctx->lang('blog_no')}
// {$ctx->if_admin('<a href="/write/">'.$ctx->lang('write').'</a>')}
//</div>
//HTML;
//}
function indexEmtpy($ctx): string {
return <<<HTML
<div class="empty">
{$ctx->lang('blog_no')}
{$ctx->if_admin('<a href="/write/">'.$ctx->lang('write').'</a>')}
</div>
HTML;
}
function indexBlog($ctx, array $posts): string {
function articles($ctx, array $posts): string {
return <<<HTML
<div class="blog-list">
<div class="blog-list-title">
all posts
<!--all posts-->
{$ctx->if_admin(
'<span>
<a href="/write/">new</a>
<a href="/uploads/">uploads</a>
<a href="/articles/write/">new</a>
<a href="/admin/uploads/">uploads</a>
</span>'
)}
</div>
@ -132,10 +131,6 @@ $html = <<<HTML
{$ctx->if_true($unsafe_toc_html, $ctx->postToc, $unsafe_toc_html)}
</div>
</div>
<div class="blog-post-comments">
{$ctx->langRaw('blog_comments_text', $email, $urlencoded_reply_subject)}
</div>
HTML;
return [$html, markdownThemeChangeListener()];