support dark mode for images with alpha channel
This commit is contained in:
parent
24982a48f5
commit
c2f382aba8
@ -57,6 +57,14 @@ function posts_html(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function posts_images(): void {
|
||||
$kw = ['include_hidden' => true];
|
||||
$posts = posts::getPosts(0, posts::getPostsCount(...$kw), ...$kw);
|
||||
foreach ($posts as $p) {
|
||||
$p->updateImagePreviews(true);
|
||||
}
|
||||
}
|
||||
|
||||
function pages_html(): void {
|
||||
$pages = pages::getAll();
|
||||
foreach ($pages as $p) {
|
||||
|
@ -23,8 +23,8 @@ class Skin {
|
||||
else
|
||||
$js = null;
|
||||
|
||||
$theme = ($_COOKIE['theme'] ?? 'auto');
|
||||
if (!in_array($theme, ['auto', 'dark', 'light']))
|
||||
$theme = themes::getUserTheme();
|
||||
if ($theme != 'auto' && !themes::themeExists($theme))
|
||||
$theme = 'auto';
|
||||
|
||||
$layout_ctx = new SkinContext('\\skin\\base');
|
||||
|
38
engine/themes.php
Normal file
38
engine/themes.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
class themes {
|
||||
|
||||
public static array $Themes = [
|
||||
'dark' => [
|
||||
'bg' => 0x222222,
|
||||
// 'alpha' => 0x303132,
|
||||
'alpha' => 0x222222,
|
||||
],
|
||||
'light' => [
|
||||
'bg' => 0xffffff,
|
||||
// 'alpha' => 0xf2f2f2,
|
||||
'alpha' => 0xffffff,
|
||||
]
|
||||
];
|
||||
|
||||
public static function getThemes(): array {
|
||||
return array_keys(self::$Themes);
|
||||
}
|
||||
|
||||
public static function themeExists(string $name): bool {
|
||||
return array_key_exists($name, self::$Themes);
|
||||
}
|
||||
|
||||
public static function getThemeAlphaColorAsRGB(string $name): array {
|
||||
$color = self::$Themes[$name]['alpha'];
|
||||
$r = ($color >> 16) & 0xff;
|
||||
$g = ($color >> 8) & 0xff;
|
||||
$b = $color & 0xff;
|
||||
return [$r, $g, $b];
|
||||
}
|
||||
|
||||
public static function getUserTheme(): string {
|
||||
return ($_COOKIE['theme'] ?? 'auto');
|
||||
}
|
||||
|
||||
}
|
@ -68,7 +68,7 @@ class Auto extends RequestHandler {
|
||||
return $s->renderPage('main/post',
|
||||
title: $post->title,
|
||||
id: $post->id,
|
||||
unsafe_html: $post->getHtml($this->isRetina()),
|
||||
unsafe_html: $post->getHtml($this->isRetina(), \themes::getUserTheme()),
|
||||
date: $post->getFullDate(),
|
||||
tags: $tags,
|
||||
visible: $post->visible,
|
||||
@ -98,7 +98,7 @@ class Auto extends RequestHandler {
|
||||
|
||||
$this->skin->title = $page ? $page->title : '???';
|
||||
return $this->skin->renderPage('main/page',
|
||||
unsafe_html: $page->getHtml($this->isRetina()),
|
||||
unsafe_html: $page->getHtml($this->isRetina(), \themes::getUserTheme()),
|
||||
page_url: $page->getUrl(),
|
||||
short_name: $page->shortName);
|
||||
}
|
||||
|
@ -14,6 +14,11 @@ var ThemeSwitcher = (function() {
|
||||
*/
|
||||
var systemState = null;
|
||||
|
||||
/**
|
||||
* @type {function[]}
|
||||
*/
|
||||
var changeListeners = [];
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
@ -71,6 +76,13 @@ var ThemeSwitcher = (function() {
|
||||
var onDone = function() {
|
||||
window.requestAnimationFrame(function() {
|
||||
removeClass(document.body, 'theme-changing');
|
||||
changeListeners.forEach(function(f) {
|
||||
try {
|
||||
f(dark)
|
||||
} catch (e) {
|
||||
console.error('[ThemeSwitcher->changeTheme->onDone] error while calling user callback:', e)
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
@ -171,7 +183,7 @@ var ThemeSwitcher = (function() {
|
||||
currentModeIndex = (currentModeIndex + 1) % modes.length;
|
||||
switch (modes[currentModeIndex]) {
|
||||
case 'auto':
|
||||
if (systemState !== null)
|
||||
if (systemState !== null && systemState !== isDarkModeApplied())
|
||||
changeTheme(systemState);
|
||||
break;
|
||||
|
||||
@ -190,6 +202,13 @@ var ThemeSwitcher = (function() {
|
||||
setCookie('theme', modes[currentModeIndex]);
|
||||
|
||||
return cancelEvent(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {function} f
|
||||
*/
|
||||
addOnChangeListener: function(f) {
|
||||
changeListeners.push(f);
|
||||
}
|
||||
};
|
||||
})();
|
@ -99,6 +99,7 @@ class MyParsedown extends ParsedownHighlight {
|
||||
nolabel: $opts['nolabel'],
|
||||
align: $opts['align'],
|
||||
padding_top: round($h / $w * 100, 4),
|
||||
may_have_alpha: $image->imageMayHaveAlphaChannel(),
|
||||
|
||||
url: $image_url,
|
||||
direct_url: $image->getDirectUrl(),
|
||||
|
@ -16,12 +16,15 @@ class markup {
|
||||
return $text;
|
||||
}
|
||||
|
||||
public static function htmlRetinaFix(string $html): string {
|
||||
public static function htmlImagesFix(string $html, bool $is_retina, string $user_theme): string {
|
||||
global $config;
|
||||
$is_dark_theme = $user_theme === 'dark';
|
||||
return preg_replace_callback(
|
||||
'/('.preg_quote($config['uploads_host'], '/').'\/\w{8}\/p)(\d+)x(\d+)(\.jpg)/',
|
||||
function($match) {
|
||||
return $match[1].(intval($match[2])*2).'x'.(intval($match[3])*2).$match[4];
|
||||
'/('.preg_quote($config['uploads_host'], '/').'\/\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';
|
||||
return $match[1].$match[2].(intval($match[3])*$mult).'x'.(intval($match[4])*$mult).($is_alpha && $is_dark_theme ? '_dark' : '').$match[5];
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
@ -23,6 +23,9 @@ class posts {
|
||||
return (int)$db->result($db->query($sql, $tag_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Post[]
|
||||
*/
|
||||
public static function getPosts(int $offset = 0, int $count = -1, bool $include_hidden = false): array {
|
||||
$db = getDb();
|
||||
$sql = "SELECT * FROM posts";
|
||||
|
@ -24,10 +24,9 @@ class Page extends Model {
|
||||
return $this->updateTs && $this->updateTs != $this->ts;
|
||||
}
|
||||
|
||||
public function getHtml(bool $retina): string {
|
||||
public function getHtml(bool $is_retina, string $user_theme): string {
|
||||
$html = $this->html;
|
||||
if ($retina)
|
||||
$html = markup::htmlRetinaFix($html);
|
||||
$html = markup::htmlImagesFix($html, $is_retina, $user_theme);
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
@ -22,8 +22,8 @@ class Post extends Model {
|
||||
$data['update_ts'] = $cur_ts;
|
||||
|
||||
if ($data['md'] != $this->md) {
|
||||
$data['html'] = \markup::markdownToHtml($data['md']);
|
||||
$data['text'] = \markup::htmlToText($data['html']);
|
||||
$data['html'] = markup::markdownToHtml($data['md']);
|
||||
$data['text'] = markup::htmlToText($data['html']);
|
||||
}
|
||||
|
||||
parent::edit($data);
|
||||
@ -31,15 +31,15 @@ class Post extends Model {
|
||||
}
|
||||
|
||||
public function updateHtml() {
|
||||
$html = \markup::markdownToHtml($this->md);
|
||||
$html = markup::markdownToHtml($this->md);
|
||||
$this->html = $html;
|
||||
|
||||
getDb()->query("UPDATE posts SET html=? WHERE id=?", $html, $this->id);
|
||||
}
|
||||
|
||||
public function updateText() {
|
||||
$html = \markup::markdownToHtml($this->md);
|
||||
$text = \markup::htmlToText($html);
|
||||
$html = markup::markdownToHtml($this->md);
|
||||
$text = markup::htmlToText($html);
|
||||
$this->text = $text;
|
||||
|
||||
getDb()->query("UPDATE posts SET text=? WHERE id=?", $text, $this->id);
|
||||
@ -81,10 +81,9 @@ class Post extends Model {
|
||||
return date('j F Y', $this->updateTs);
|
||||
}
|
||||
|
||||
public function getHtml(bool $retina): string {
|
||||
public function getHtml(bool $is_retina, string $theme): string {
|
||||
$html = $this->html;
|
||||
if ($retina)
|
||||
$html = markup::htmlRetinaFix($html);
|
||||
$html = markup::htmlImagesFix($html, $is_retina, $theme);
|
||||
return $html;
|
||||
}
|
||||
|
||||
@ -173,9 +172,8 @@ class Post extends Model {
|
||||
foreach ($images[$u->randomId] as $s) {
|
||||
list($w, $h) = $s;
|
||||
list($w, $h) = $u->getImagePreviewSize($w, $h);
|
||||
if ($u->createImagePreview($w, $h, $update)) {
|
||||
if ($u->createImagePreview($w, $h, $update, $u->imageMayHaveAlphaChannel()))
|
||||
$images_affected++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,8 @@ class Upload extends Model {
|
||||
$h *= 2;
|
||||
}
|
||||
|
||||
return 'https://'.$config['uploads_host'].'/'.$this->randomId.'/p'.$w.'x'.$h.'.jpg';
|
||||
$prefix = $this->imageMayHaveAlphaChannel() ? 'a' : 'p';
|
||||
return 'https://'.$config['uploads_host'].'/'.$this->randomId.'/'.$prefix.$w.'x'.$h.'.jpg';
|
||||
}
|
||||
|
||||
// TODO remove?
|
||||
@ -73,6 +74,12 @@ class Upload extends Model {
|
||||
return in_array(extension($this->name), self::$ImageExtensions);
|
||||
}
|
||||
|
||||
// assume all png images have alpha channel
|
||||
// i know this is wrong, but anyway
|
||||
public function imageMayHaveAlphaChannel(): bool {
|
||||
return strtolower(extension($this->name)) == 'png';
|
||||
}
|
||||
|
||||
public function isVideo(): bool {
|
||||
return in_array(extension($this->name), self::$VideoExtensions);
|
||||
}
|
||||
@ -94,36 +101,40 @@ class Upload extends Model {
|
||||
return [$w, $h];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?int $w
|
||||
* @param ?int $h
|
||||
* @param bool $update Whether to proceed if preview already exists
|
||||
* @return bool
|
||||
*/
|
||||
public function createImagePreview(?int $w = null, ?int $h = null, bool $update = false): bool {
|
||||
public function createImagePreview(?int $w = null,
|
||||
?int $h = null,
|
||||
bool $force_update = false,
|
||||
bool $may_have_alpha = false): bool {
|
||||
global $config;
|
||||
|
||||
$orig = $config['uploads_dir'].'/'.$this->randomId.'/'.$this->name;
|
||||
$updated = false;
|
||||
|
||||
for ($mult = 1; $mult <= 2; $mult++) {
|
||||
$dw = $w * $mult;
|
||||
$dh = $h * $mult;
|
||||
$dst = $config['uploads_dir'].'/'.$this->randomId.'/p'.$dw.'x'.$dh.'.jpg';
|
||||
foreach (themes::getThemes() as $theme) {
|
||||
if (!$may_have_alpha && $theme == 'dark')
|
||||
continue;
|
||||
|
||||
if (file_exists($dst)) {
|
||||
if (!$update)
|
||||
continue;
|
||||
unlink($dst);
|
||||
for ($mult = 1; $mult <= 2; $mult++) {
|
||||
$dw = $w * $mult;
|
||||
$dh = $h * $mult;
|
||||
|
||||
$prefix = $may_have_alpha ? 'a' : 'p';
|
||||
$dst = $config['uploads_dir'].'/'.$this->randomId.'/'.$prefix.$dw.'x'.$dh.($theme == 'dark' ? '_dark' : '').'.jpg';
|
||||
|
||||
if (file_exists($dst)) {
|
||||
if (!$force_update)
|
||||
continue;
|
||||
unlink($dst);
|
||||
}
|
||||
|
||||
$img = imageopen($orig);
|
||||
imageresize($img, $dw, $dh, themes::getThemeAlphaColorAsRGB($theme));
|
||||
imagejpeg($img, $dst, $mult == 1 ? 93 : 67);
|
||||
imagedestroy($img);
|
||||
|
||||
setperm($dst);
|
||||
$updated = true;
|
||||
}
|
||||
|
||||
$img = imageopen($orig);
|
||||
imageresize($img, $dw, $dh, [255, 255, 255]);
|
||||
imagejpeg($img, $dst, $mult == 1 ? 93 : 67);
|
||||
imagedestroy($img);
|
||||
|
||||
setperm($dst);
|
||||
$updated = true;
|
||||
}
|
||||
|
||||
return $updated;
|
||||
@ -138,7 +149,7 @@ class Upload extends Model {
|
||||
$files = scandir($dir);
|
||||
$deleted = 0;
|
||||
foreach ($files as $f) {
|
||||
if (preg_match('/^p(\d+)x(\d+)\.jpg$/', $f)) {
|
||||
if (preg_match('/^[ap](\d+)x(\d+)(?:_dark)?\.jpg$/', $f)) {
|
||||
if (is_file($dir.'/'.$f))
|
||||
unlink($dir.'/'.$f);
|
||||
else
|
||||
|
@ -26,9 +26,9 @@ $html = <<<HTML
|
||||
</form>
|
||||
HTML;
|
||||
|
||||
$js = <<<JAVASCRIPT
|
||||
$js = <<<JS
|
||||
ge('as_password').focus();
|
||||
JAVASCRIPT;
|
||||
JS;
|
||||
|
||||
return [$html, $js];
|
||||
}
|
||||
@ -264,9 +264,9 @@ $html = <<<HTML
|
||||
HTML;
|
||||
|
||||
$js_params = json_encode(['pages' => true, 'edit' => $is_edit]);
|
||||
$js = <<<JAVASCRIPT
|
||||
$js = <<<JS
|
||||
AdminWriteForm.init({$js_params});
|
||||
JAVASCRIPT;
|
||||
JS;
|
||||
|
||||
return [$html, $js];
|
||||
}
|
||||
|
@ -116,12 +116,14 @@ HTML;
|
||||
// --------
|
||||
|
||||
function page($ctx, $page_url, $short_name, $unsafe_html) {
|
||||
return <<<HTML
|
||||
$html = <<<HTML
|
||||
<div class="page">
|
||||
{$ctx->if_admin($ctx->pageAdminLinks, $page_url, $short_name)}
|
||||
<div class="blog-post-text">{$unsafe_html}</div>
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
return [$html, markdownThemeChangeListener()];
|
||||
}
|
||||
|
||||
function pageAdminLinks($ctx, $url, $short_name) {
|
||||
@ -139,7 +141,7 @@ HTML;
|
||||
// ---------
|
||||
|
||||
function post($ctx, $id, $title, $unsafe_html, $date, $visible, $url, $tags, $email, $urlencoded_reply_subject) {
|
||||
return <<<HTML
|
||||
$html = <<<HTML
|
||||
<div class="blog-post">
|
||||
<div class="blog-post-title">
|
||||
<h1>{$title}</h1>
|
||||
@ -158,6 +160,8 @@ return <<<HTML
|
||||
{$ctx->langRaw('blog_comments_text', $email, $urlencoded_reply_subject)}
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
return [$html, markdownThemeChangeListener()];
|
||||
}
|
||||
|
||||
function postAdminLinks($ctx, $url, $id) {
|
||||
@ -171,7 +175,32 @@ function postTag($ctx, $url, $name) {
|
||||
return <<<HTML
|
||||
<a href="{$url}"><span>#</span>{$name}</a>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function markdownThemeChangeListener() {
|
||||
return <<<JS
|
||||
ThemeSwitcher.addOnChangeListener(function(isDark) {
|
||||
var nodes = document.querySelectorAll('.md-image-wrap');
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i];
|
||||
var alpha = parseInt(node.getAttribute('data-alpha'), 10);
|
||||
if (!alpha)
|
||||
continue;
|
||||
var div = node.querySelector('a > div');
|
||||
if (!div) {
|
||||
console.warn('could not found a>div on this node:', node);
|
||||
continue;
|
||||
}
|
||||
var style = div.getAttribute('style');
|
||||
if (isDark) {
|
||||
style = style.replace(/(a[\d]+x[\d]+)\.jpg/, '$1_dark.jpg');
|
||||
} else {
|
||||
style = style.replace(/(a[\d]+x[\d]+)_dark\.jpg/, '$1.jpg');
|
||||
}
|
||||
div.setAttribute('style', style);
|
||||
}
|
||||
});
|
||||
JS;
|
||||
}
|
||||
|
||||
|
||||
|
@ -14,14 +14,14 @@ HTML;
|
||||
|
||||
function image($ctx,
|
||||
// options
|
||||
$align, $nolabel, $w, $padding_top,
|
||||
$align, $nolabel, $w, $padding_top, $may_have_alpha,
|
||||
// image data
|
||||
$direct_url, $url, $note) {
|
||||
return <<<HTML
|
||||
<div class="md-image align-{$align}">
|
||||
<div class="md-image-wrap">
|
||||
<div class="md-image-wrap" data-alpha="{$ctx->if_then_else($may_have_alpha, '1', '0')}">
|
||||
<a href="{$direct_url}">
|
||||
<div style="background: #f2f2f2 url('{$url}') no-repeat; background-size: contain; width: {$w}px; padding-top: {$padding_top}%;"></div>
|
||||
<div style="background: url('{$url}') no-repeat; background-size: contain; width: {$w}px; padding-top: {$padding_top}%;"></div>
|
||||
</a>
|
||||
{$ctx->if_true(
|
||||
$note != '' && !$nolabel,
|
||||
|
Loading…
x
Reference in New Issue
Block a user