commit 1424ec981c19956a6967d74575d0e789369c7268 Author: Max Nuding Date: Sat May 24 13:23:02 2025 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9782919 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +logs/ +pics/* +config.php +.DS_Store +**/.DS_Store +vendor \ No newline at end of file diff --git a/.nova/Configuration.json b/.nova/Configuration.json new file mode 100644 index 0000000..7fdc1fd --- /dev/null +++ b/.nova/Configuration.json @@ -0,0 +1,6 @@ +{ + "com.thorlaksson.phpcs.formatOnSave" : true, + "com.thorlaksson.phpcs.runOnChange" : "onSave", + "com.thorlaksson.phpcs.standard" : "PSR12", + "prettier.format-on-save" : "Disable" +} diff --git a/auth_callback.php b/auth_callback.php new file mode 100644 index 0000000..b12948e --- /dev/null +++ b/auth_callback.php @@ -0,0 +1,37 @@ +authenticate($_GET['code']); +if ($success) { + if (!empty($_SESSION[$app->app_name . 'redirect_after_auth'])) { + $url = $_SESSION[$app->app_name . 'redirect_after_auth']; + header("Location: " . $url); + echo(''); + echo ''; + echo 'Redirecting to ' + . $url + . ''; + } else { + die('Succesfully authorized!'); + } +} else { + die("Error authenticating"); +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..736f47d --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "phlaym/roastmonday", + "version": "0.1.0", + "keywords": [ + "pnut" + ], + "license": "MIT", + "autoload": { + "psr-4": { + "Phlaym\\Roastmonday\\": "src/" + } + }, + "authors": [ + { + "name": "Max Nuding", + "email": "code@max.nuding.me" + } + ], + "require": { + "php": "^8.4|^8.3", + "hutattedonmyarm/apnuti": "@dev", + "ext-curl": "*", + "ext-date": "*", + "ext-pdo": "*", + "ext-pdo_mysql": "*", + "ext-mysqli": "*" + }, + "repositories": [ + { + "type": "vcs", + "url": "git@phlaym.net:phlaym/APnutI.git" + } + ] +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..20c05b6 --- /dev/null +++ b/composer.lock @@ -0,0 +1,215 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "01e67dd40fbc8781be2836a18e32d472", + "packages": [ + { + "name": "hutattedonmyarm/apnuti", + "version": "dev-main", + "source": { + "type": "git", + "url": "git@phlaym.net:phlaym/APnutI.git", + "reference": "7f0f8ac95b3cfe7c5de922079ac2f60d4c49bddb" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pdo": "*", + "monolog/monolog": "^3.0", + "php": "^8.3", + "psr/log": "^2.0 || ^3.0" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "APnutI\\": "src" + } + }, + "license": [ + "MIT" + ], + "description": "PHP Pnut library", + "keywords": [ + "api", + "pnut" + ], + "time": "2025-05-20T15:51:25+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "hutattedonmyarm/apnuti": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.4|^8.3", + "ext-curl": "*", + "ext-date": "*", + "ext-pdo": "*", + "ext-pdo_mysql": "*", + "ext-mysqli": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/fetch_mondays.php b/fetch_mondays.php new file mode 100644 index 0000000..888c95b --- /dev/null +++ b/fetch_mondays.php @@ -0,0 +1,24 @@ +' . PHP_EOL; +$app = new Phlaym\Roastmonday\Roastmonday($config, realpath($internal_pics_dir)); +$app->authenticateServerToken(); +$tm = $app->findThemeMondays(); +echo 'Only checking the latest four mondays for new pictures
\n'; +$tm = array_slice($tm, 0, 4); +foreach ($tm as $monday) { + $monday['id'] = $app->addTheme($monday['tag'], $monday['date']); + echo $monday['tag'] . ' with id ' . $monday['id'] . '
\n'; + try { + $app->savePicturesForTheme($monday); + } catch (\Exception $e) { + echo 'Error saving pictures for ' . $monday['tag'] . ':' . $e->getMessage(); + } +} diff --git a/get_posttext.php b/get_posttext.php new file mode 100644 index 0000000..2e6e046 --- /dev/null +++ b/get_posttext.php @@ -0,0 +1,58 @@ + false, 'error' => 400, 'text' => 'No IDs provided']; + header('Content-Type: application/json'); + die(json_encode($resp)); +} +$config = include __DIR__ . '/config.php'; +$internal_pics_dir = realpath(__DIR__ . $config['pics_dir']); +$app = new Phlaym\Roastmonday\Roastmonday($config, $internal_pics_dir); +$app->authenticateServerToken(); +$parameters = [ + 'include_deleted' => false, + 'include_client' => false, + 'include_counts' => false, + 'include_copy_mentions' => false, + 'include_raw' => false, + 'include_post_raw' => false, + 'include_presence' => true +]; +if (!empty($_GET['avatarWidth'])) { + $parameters['avatarWidth'] = $_GET['avatarWidth']; +} +$resp = ['success' => false, 'error' => 0, 'text' => '']; +try { + $post = $app->getPost($_GET['id'], $parameters); +} catch (APnutI\Exceptions\NotFoundException $e) { + $resp['error'] = 404; +} catch (Exception $e) { + $resp['error'] = 500; + $resp['text'] = 'Something went wrong'; +} +if (!empty($post)) { + $resp['success'] = true; + $resp['error'] = 0; + $resp['text'] = $post->getText(); // DEPRECATED! + if (!empty($post->user)) { + $resp['user'] = '@' . $post->user->username; + if (isset($post->user->name)) { + $resp['realname'] = $post->user->name; + } + } + if (!empty($post->user) && !empty($post->user->avatar_image) && !empty($post->user->avatar_image->link)) { + $resp['img'] = $post->user->avatar_image->link; + } + $resp['presence'] = $post->user->getPresenceInt(); + $resp['postid'] = $post->id; + $resp['posttext'] = $post->getText(); + $resp['timestamp'] = $post->created_at->getTimestamp(); +} +echo json_encode($resp); diff --git a/get_userdetails.php b/get_userdetails.php new file mode 100644 index 0000000..144bae8 --- /dev/null +++ b/get_userdetails.php @@ -0,0 +1,43 @@ +authenticateServerToken(); +$parameters = [ + 'include_html' => false, + 'include_counts' => false, + 'include_presence' => true, + 'include_raw' => false, + 'include_user_raw' => false +]; +$resp = ['success' => false, 'error' => 0, 'text' => '']; +$ids = explode(",", $_GET['id']); +if (count($ids) == 1) { + // TODO: Split up by multiple ids +} else { + +} +try { + $user = $app->getUser($ids[0], $parameters); +} catch (APnutI\Exceptions\NotFoundException $e) { + $resp['error'] = 404; +} catch (Exception $e) { + $resp['error'] = 500; + $resp['text'] = 'Something went wrong'; +} +if (!empty($user)) { + $resp['success'] = true; + $resp['error'] = 0; + $resp['name'] = $user->name; + $resp['presence'] = $user->getPresenceInt(); + $resp['presenceString'] = $user->getPresenceString(); +} +echo json_encode($resp); diff --git a/icons/close.svg b/icons/close.svg new file mode 100644 index 0000000..b548b70 --- /dev/null +++ b/icons/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..e6367a4 --- /dev/null +++ b/index.php @@ -0,0 +1,71 @@ + + + + + + Roastmonday + + + + + + + + + +

Roastmonday

+
+ +
+
+
+
Choose a theme
+
+
+
+
+
Highlight
+ +
+
+
Red
+
Green
+
+
+
Light Gray
+
Medium Gray
+
Dark Gray
+
+
+
Shadow
+
+
+ + \ No newline at end of file diff --git a/pictures.php b/pictures.php new file mode 100644 index 0000000..75e2db6 --- /dev/null +++ b/pictures.php @@ -0,0 +1,213 @@ + + + + + Roastmonday + + + + + + + + +Overview'); +} +$config = include __DIR__ . '/config.php'; +$id = $_GET['id']; +$internal_pics_dir = realpath(__DIR__ . $config['pics_dir']); +$app = new Phlaym\Roastmonday\Roastmonday($config, $internal_pics_dir); +$theme = $app->getThemeMonday($id); +echo '

' . $theme['tag'] . '

'; +?> +Overview + Manually add an avatar + + Change your avatar + + +
+
+ + + + + + + + + + + + + + + +
Post ID:
Avatar:* +
+ + +
+ +
+ + + + + + +
+
+
+
+ +
+ * Optional. If left empty, the current avatar of the post creator will be used +
+ +
+
+ + + + + + + + + + + + + + + + +
Avatar: +
+ + + +
+ +
+ + +
+
+
+
+ +
Post + + +
+ If you post about it, include #thememonday and , + and either keyword "avatar" or "picture" your new avatar will show up in this gallery! +
Duration + days. + Your avatar will reset to the current one on + format('Y-m-d'); ?> +
+ + +
+
+ +
+ getPicturesForTheme($id); + if (empty($pics)) { + die('No pictures for theme selected. Overview'); + } + ?>
+ +
+
+
+
+ @' . $pic['username'] . '
'; + } else { + echo '' + . $pic['user_realname'] + . '@' + . $pic['username'] + . ''; + } + ?> +
+
+ +
+
+ +
+ + \ No newline at end of file diff --git a/reset_avatars.php b/reset_avatars.php new file mode 100644 index 0000000..425ab65 --- /dev/null +++ b/reset_avatars.php @@ -0,0 +1,8 @@ +resetOriginalAvatars(); \ No newline at end of file diff --git a/scripts/pictureViewer.js b/scripts/pictureViewer.js new file mode 100644 index 0000000..759b942 --- /dev/null +++ b/scripts/pictureViewer.js @@ -0,0 +1,739 @@ +document.addEventListener("DOMContentLoaded", function(event) { + formatDates(); + getPostTexts(); + getUserDetails(); + handleAvatarHover(); + initFileUpload(); + setupUploadPostPreview(); +}); + +function formatDates() { + let timestampElements = document.querySelectorAll('.postdate[ts]'); + timestampElements.forEach(function(el) { + let d = new Date(el.attributes['ts'].value * 1000); + el.innerText = d.toLocaleString(); + }); +} + +function getPostTexts() { + let boxes = document.querySelectorAll('.post-box'); + // TODO: Implement multiple fetches with one call + boxes.forEach(function(el) { + let id = el.querySelector('.post-meta-box').querySelector('span').innerText.replace('#',''); + get('get_posttext.php?id='+id).then(function(response) { + let info; + try { + info = JSON.parse(response); + } catch (error) { + showError(`Unknown response from server: ${response}`); + return; + } + let postTextBox = el.querySelector('.post-text-box'); + let loader = postTextBox.querySelector('.loader'); + if (loader !== null) { + postTextBox.removeChild(loader); + } + if (info.success) { + postTextBox.innerHTML = info.text; + } else { + if (info.error == 404) { + console.log('Post ', id, ' not found'); + } else { + console.log('Error fetching post ', id, 'Error code: ', info.error); + } + } + }, function(error) { + console.log('Error fetching post ', id, error); + }); + }); +} + +function getUserDetails() { + let boxes = document.querySelectorAll('.user-box'); + // TODO: Implement multiple fetches with one call + boxes.forEach(function(el) { + let userBox = el.querySelector('.username'); + if (userBox == null) { + userBox = el.querySelector('span'); + } + let id = userBox.innerText; + get('get_userdetails.php?id='+id).then(function(response) { + let info; + try { + info = JSON.parse(response); + } catch (error) { + showError(`Unknown response from server: ${response}`); + return; + } + let loader = userBox.querySelector('.loader-small'); + if (loader !== null) { + userBox.removeChild(loader); + } + if (info.success) { + if (info.name != null) { + el.innerHTML = '' + info.name + '' + id + ''; + } + let presenceIndicator = document.createElement('div'); + presenceIndicator.classList.add('presence-indicator'); + switch (info.presence) { + case 0: + presenceIndicator.style.borderColor = "red"; + presenceIndicator.style.setProperty('border-color', 'var(--red)'); + break; + case 1: + presenceIndicator.style.borderColor = "green"; + presenceIndicator.style.setProperty('border-color', 'var(--green)'); + break; + } + el.appendChild(presenceIndicator); + } else { + if (info.error == 404) { + console.log('User ', id, ' not found'); + } else { + console.log('Error fetching user ', id, 'Error code: ', info.error); + } + } + }, function(error) { + console.log('Error fetching user ', id, error); + }); + + }); +} + +function handleAvatarHover() { + let avatars = document.querySelectorAll('img.avatar'); + avatars.forEach(function(el) { + el.addEventListener('mouseenter', function() { + // Blur non hovered + let nonHovered = document.querySelectorAll('img.avatar:not(:hover)'); + nonHovered.forEach(function(img) { + img.classList.add('blur'); + //img.classList.remove('unblur'); + //img.style.filter = "blur(5px)"; + }); + }); + el.addEventListener('mouseleave', function() { + // Unblur + let avtrs = document.querySelectorAll('img.avatar'); + avtrs.forEach(function(img) { + img.classList.remove('blur'); + //img.classList.add('unblur'); + //img.style.filter = "blur(0px)"; + }); + }); + }); +} + +function showLoginForm(authUrl) { + const authDiv = document.getElementById("authorizeDiv"); + authDiv.classList.remove("hidden"); + authDiv.querySelector("a").href = authUrl; +} + +function initUploadForm(formId) { + document.querySelectorAll('.upload-form').forEach(function(el) { + el.style.display = "none"; + }); + const form = document.getElementById(formId); + form.style.display = form.offsetHeight == 0 ? "inherit" : "none"; + form.reset(); + removeImage(); + document.getElementById('uploadProgress').style.display = "none"; +} + +function showAvatarResetDate(e) { + const val = e.valueAsNumber || parseInt(e.value) || 3; + let d = new Date(); + d.setDate(d.getDate() + val); + e.parentElement.querySelector('span').innerText = d.toLocaleDateString(); +} + +function checkLoginStatus() { + return new Promise(function(resolve, reject) { + let req = new XMLHttpRequest(); + req.open('GET', 'set_temp_avatar.php?status'); + + req.onload = function() { + // This is called even on 404 etc + // so check the status + console.log("onload", req); + if (req.status == 200) { + // Resolve the promise with the response text + //resolve(req.response); + let result = req.response; + if (result.length < 1) { + var error = new Error("No response"); + error.name = "NoResponse"; + reject(error); + } + try { + result = JSON.parse(result); + } catch (err) { + reject(err); + return; + } + resolve(result); + } else { + // Otherwise reject with the status text + // which will hopefully be a meaningful error + reject(Error(req.statusText)); + } + }; + + // Handle network errors + req.onerror = function() { + console.error("Network error"); + reject(Error('Network Error')); + }; + // Make the request + req.send(); + }); +} + +function transitionEndEventName () { + var i, + undefined, + el = document.createElement('div'), + transitions = { + 'transition':'transitionend', + 'OTransition':'otransitionend', // oTransitionEnd in very old Opera + 'MozTransition':'transitionend', + 'WebkitTransition':'webkitTransitionEnd' + }; + + for (i in transitions) { + if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) { + return transitions[i]; + } + } + + //TODO: throw 'TransitionEnd event is not supported in this browser'; +} + +function initFileUpload() { + document.getElementById('showUploadForm').addEventListener('click', e => { + initUploadForm('manuallyAddAvatarForm'); + }); + document.getElementById('addTempAvatar').addEventListener('click', e => { + document.getElementById('loadingLoginStatus').classList.remove('hidden'); + checkLoginStatus().then(result => { + document.getElementById('loadingLoginStatus').classList.add('hidden'); + console.log("Authentication status: ", result, !!result.authenticated); + if(result.error) { + console.error(`Error checking authentication status: ${result.error.message}`); + return; + } + if (!!result.authenticated) { + initUploadForm('uploadTempAvatar'); + document.getElementById('uploadTempAvatar').querySelector('textarea').maxlength = result.maxPostLength; + } else { + showLoginForm(result.authUrl); + } + }, error => { + console.error("Error checking authentication status", error); + }); + }); + document.getElementById('cbShouldPostAvatar').addEventListener('click', e => { + document.getElementById('uploadTempAvatar').querySelector('.posttext').style.display = e.target.checked ? 'inherit' : 'none'; + }); + document.querySelectorAll('.posttext').forEach(el => { + el.addEventListener('focus', e => { + if (e.target.selectionStart == e.target.selectionEnd && !document.firstTextboxFocus) { + document.firstTextboxFocus = true; + setTimeout(function() { + console.log("Selecting start", e, e.target); + e.target.selectionStart = 0; + e.target.selectionEnd = 0; + }); + } + }); + }); + const durationInput = document.querySelector('#uploadTempAvatar input[name=avatarDuration]'); + showAvatarResetDate(durationInput); + durationInput.addEventListener('change', e => { + showAvatarResetDate(e.target); + }); + var isAdvancedUpload = function() { + var div = document.createElement('div'); + return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window; + }(); + if (isAdvancedUpload) { + window.droppedFile = false; + const dragOverEvents = ['dragover', 'dragenter']; + const dragEndEvents = ['dragleave', 'dragend', 'drop']; + const events = dragOverEvents.concat(dragEndEvents).concat(['drag', 'dragstart']); + + const zone = document.querySelectorAll('.file-upload').forEach(function(form) { + form.classList.add('advanced-upload'); + const fileSelect = form.querySelector('input[type=file]'); + events.forEach(event => { + form.addEventListener(event, e => { + // preventing the unwanted behaviours + e.preventDefault(); + e.stopPropagation(); + }); + }); + dragOverEvents.forEach(event => { + form.addEventListener(event, e => { + form.classList.add('is-dragover'); + }); + }); + dragEndEvents.forEach(event => { + form.addEventListener(event, e => { + form.classList.remove('is-dragover'); + }); + }); + form.addEventListener('drop', e => { + imageAdded(e.dataTransfer.files, e); + }); + fileSelect.addEventListener('change', e => { + imageAdded(e.target.files, e); + }); + }); + } + console.log(document.querySelectorAll('.removeImage')); + document.querySelectorAll('.removeImage').forEach(function(el) { + el.style.zIndex = 1000; + el.addEventListener('click', e => { + removeImage(); + }); + }); +} + +function removeImage(e) { + console.log(e); + window.droppedFile = null; + //document.getElementById('removeImage').style.display = "none"; + document.querySelectorAll('.removeImage').forEach(function(el) { + el.style.display = "none"; + }); + document.querySelectorAll('.avatarPreview').forEach(function(el) { + window.URL.revokeObjectURL(el.src); + el.style.display = "none"; + el.src = ""; + }); +} + +function handleManualAvatarUpload(e) { + startUpload(e).then(result => { + notify("Avatar added successfully", {type: "success"}); + showNewlyAddedPicture(result); + e.style.display = "none"; + }, error => { + notify(`Error saving avatar: ${error.message}`, {type: "error"}); + e.style.display = "none"; + }); + return false; +} + +function handleTempAvatarUpload(e) { + startUpload(e).then(result => { + if (result.warn) { + notify(result.warn, {type: "warn"}); + } else { + notify("Your temporary theme avatar has been saved succesfully", {type: "success"}); + } + e.style.display = "none"; + }, error => { + console.error(error); + notify(`Error saving avatar: ${error.message}`, {type: "error"}); + }); + return false; +} + +function startUpload(e) { + console.log(window.droppedFile, e); + const progress = e.querySelector('.uploadProgress'); + + return new Promise(function(resolve, reject) { + postForm(e, (position, total) => { + progress.max = total; + progress.value = position; + progress.style.display = "inherit"; + }).then(result => { + if (result.length < 1) { + reject({'message': 'Received no response from server :( '}); + return; + } + try { + result = JSON.parse(result); + } catch (err) { + reject({'message': `Unknown response from server: ${result}`}); + return; + } + console.log("Done", result); + if (result.error) { + reject(result.error); + } else { + resolve(result); + } + }, error => { + console.error(error); + let serverError; + if (typeof(error) == "object") { + serverError = error; + } else { + try { + serverError = JSON.parse(error); + } catch (err) { + serverError = {message: "Unknown error"}; + } + } + reject(serverError); + }); + }); +} + +function showNewlyAddedPicture(postDetails) { + // TODO: Consolidate together with getUserDetails() + // TODO: Check if user already has a picture for this theme + document.querySelector('.avatar-grid').appendChild(createPostBox(info)); +} + +function createPostBox(info) { + /* + * info.succes: true (hopefully) + * info.error: Error object, containing "code" and "message" + * info.src: Avatar image + * info.presence: -1: gray, 0: red, 1: green + * info.user: @username. Might be null or empty string + * info.realname: Realname. Might be null or empty string + * info.posttext: HTML or pure text content of the post + * info.postid: ID of the post + * info.timestamp: timestamp of the post + */ + console.log(info); + const grid = document.querySelector('.avatar-grid'); + let postBox = null; + // If there is already oen in the grid, clone that + if (grid.querySelectorAll(".avatar-box").length) { + postBox = grid.children[grid.children.length - 1].cloneNode(true); + } else { + console.error("No images in grid yet. Not fully implemented. TODO"); + } + + // New image + postBox.querySelector('.avatar').src = info.img; + + // User info + const userDetails = postBox.querySelector('.user-box'); + + if (info.realname != null) { + userDetails.innerHTML = '' + info.realname + '' + info.user + ''; + } else { + userDetails.innerHTML = '' + info.user + ''; + } +/* + console.log(userDetails.outerHTML); + let userBox = userDetails.querySelector('.username'); + if (userBox == null) { + userBox = userDetails.querySelector('span'); + } + console.log(userBox.outerHTML); + userBox.innerText = info.user; + console.log(userBox.outerHTML); + + if (info.realname != null) { + userBox.innerHTML = '' + info.realname + '' + info.user + ''; + }*/ + let presenceIndicator = userDetails.querySelector('.presence-indicator'); + if (presenceIndicator == null) { + presenceIndicator = document.createElement('div'); + presenceIndicator.classList.add('presence-indicator'); + userDetails.appendChild(presenceIndicator); + } + switch (info.presence) { + case -1: + presenceIndicator.style.borderColor = "gray"; + presenceIndicator.style.setProperty('border-color', 'var(--gray-dark)'); + break; + case 0: + presenceIndicator.style.borderColor = "red"; + presenceIndicator.style.setProperty('border-color', 'var(--red)'); + break; + case 1: + presenceIndicator.style.borderColor = "green"; + presenceIndicator.style.setProperty('border-color', 'var(--green)'); + break; + } + + // Post info + const postTextBox = postBox.querySelector('.post-text-box'); + postTextBox.innerHTML = info.posttext; + + const postMetaBox = postBox.querySelector('.post-meta-box'); + const postLink = postMetaBox.querySelector('a'); + postLink.href = "https://posts.pnut.io/" + info.postid; + postLink.innerText = '#' + info.postid; + let d = new Date(info.timestamp * 1000); + postMetaBox.querySelector('.postdate').innerText = d.toLocaleString(); + return postBox; +} + +function postForm(form, progressCallback) { + // Return a new promise. + console.log('Form: ', form); + return new Promise(function(resolve, reject) { + //const promise = new Promise(); + if (form.id == 'manuallyAddAvatarForm') { + const idInput = form.querySelector('[name=postID]'); + const pID = getPostID(idInput); + if (pID == -1) { + reject("Invalid post id"); + } else { + idInput.value = pID; + } + } + + var ajaxData = new FormData(form); + if (window.droppedFile) { + ajaxData.append(form.querySelector('input[type=file]').getAttribute('name'), window.droppedFile); + } + const id = getQueries().id; + if (!id) { + reject("Unknown theme id"); + } else { + ajaxData.append('theme', id); + ajaxData.append('submit', 'submit'); + console.log(ajaxData); + // Do the usual XHR stuff + let req = new XMLHttpRequest(); + req.open(form.method, form.action); + + req.onload = function() { + // This is called even on 404 etc + // so check the status + console.log("onload", req); + if (req.status == 200) { + // Resolve the promise with the response text + resolve(req.response); + } + else { + // Otherwise reject with the status text + // which will hopefully be a meaningful error + reject(Error(req.statusText)); + } + }; + + // Handle network errors + req.onerror = function() { + console.error("Network error"); + reject(Error('Network Error')); + }; + if (progressCallback) { + var eventSource = req.upload || req; + eventSource.addEventListener("progress", function(e) { + // normalize position attributes across XMLHttpRequest versions and browsers + var position = e.position || e.loaded; + var total = e.totalSize || e.total; + progressCallback(position, total); + }); + } + + // Make the request + req.send(ajaxData); + } + }); +} + +function imageAdded(arr, evt) { + if (!arr.length) { + return; + } + console.log("Dropped image", arr, evt); + window.droppedFile = arr[0]; + const imgSize = window.droppedFile.size; + console.log("Size in Bytes", imgSize); + const maxSize = parseInt(document.querySelector('input[name=MAX_FILE_SIZE]').value); + let errorSpan = document.getElementById('imgTooLarge'); + if (imgSize > maxSize) { + //Image too large + if (errorSpan == null) { + errorSpan = document.createElement('span'); + errorSpan.classList.add('imgTooLarge'); + errorSpan.innerText = `Image is too large. It is ${(imgSize/1024/1024).toFixed(2)}MiB, but should be <= ${(maxSize/1024/1024).toFixed(2)}MiB`; + errorSpan.style.display = 'inherit'; + errorSpan.id = 'imgTooLarge'; + document.querySelector('#uploadTempAvatar table tr td:nth-child(2)').appendChild(errorSpan); + } else { + errorSpan.style.display = 'inherit'; + } + } else if (errorSpan != null) { + errorSpan.style.display = 'none'; + } + getOrientation(function(e){ + console.log("Orientation", e); + let prv = evt.target.parentElement.querySelector('.avatarPreview'); + switch (e) { + case 1: + prv.style.transform = "rotate(0)"; + break; + case 3: + prv.style.transform = "rotate(180deg)"; + break; + case 6: + prv.style.transform = "rotate(90deg)"; + break; + case 8: + prv.style.transform = "rotate(270deg)"; + break; + default: + prv.style.transform = "rotate(0)"; + break; + } + if (prv.src) { + window.URL.revokeObjectURL(prv.src); + } + prv.style.display = "inherit"; + prv.src = window.URL.createObjectURL(droppedFile); + evt.target.parentElement.querySelector('.removeImage').style.display = "inherit"; + evt.target.parentElement.querySelector('.removeImage').style.zIndex = 1000;zIndex = 1000; + }); +} + +function getOrientation(callback) { + var reader = new FileReader(); + reader.onload = function(e) { + var view = new DataView(e.target.result); + const magic = view.getUint16(0, false); + if (magic != 0xFFD8) { + console.warn("Unknown magic bytes: ", magic.toString(16)) + return callback(-2); + } + var length = view.byteLength, offset = 2; + console.debug("Length: ", length); + while (offset < length) { + const firstBytesTest = view.getUint16(offset+2, false); + if (firstBytesTest <= 8) { + console.warn("firstBytesTest <=8", firstBytesTest); + return callback(-1); + } + var marker = view.getUint16(offset, false); + offset += 2; + if (marker == 0xFFE1) { + console.debug("Found marker"); + if (view.getUint32(offset += 2, false) != 0x45786966) { + console.debug("Invalid sequence following marker: ", view.getUint32(offset += 2, false).toString(16)); + return callback(-1); + } + + var little = view.getUint16(offset += 6, false) == 0x4949; + offset += view.getUint32(offset + 4, little); + var tags = view.getUint16(offset, little); + offset += 2; + for (var i = 0; i < tags; i++) { + if (view.getUint16(offset + (i * 12), little) == 0x0112) { + console.debug("Found rotation tag"); + return callback(view.getUint16(offset + (i * 12) + 8, little)); + } + } + } else if ((marker & 0xFF00) != 0xFF00) { + break; + } else { + offset += view.getUint16(offset, false); + } + } + return callback(-1); + }; + reader.readAsArrayBuffer(window.droppedFile); +} + +function setupUploadPostPreview() { + const postIdInput = document.querySelector('input[name=postID]'); + window.lastPostIDKey = 0; + postIdInput.onkeyup = function() { + window.lastPostIDKey = Date.now(); + setTimeout(function() { + const now = Date.now(); + const diff = now - window.lastPostIDKey; + console.log(diff); + if (diff < 500) { + return; + } + const pID = getPostID(postIdInput); + get(`get_posttext.php?id=${pID}&avatarWidth=40`).then(function(response) { + try { + response = JSON.parse(response); + } catch (error) { + showError(`Unknown response from server: ${response}`); + return; + } + if (response.error) { + showError(`Error loading post #${pID}: ${response.error}`); + return; + } + const tbl = document.querySelector('.upload-form').querySelector('table'); + let i = tbl.rows[1].cells.length - 1; + // Remove existing post boxes + while (tbl.rows[1].cells[i].querySelector('.post-box')) { + tbl.rows[1].removeChild(tbl.rows[1].cells[i]); + i = tbl.rows[1].cells.length - 1; + } + const cell = tbl.rows[1].insertCell(); + const postBox = createPostBox(response); + const ab = postBox.querySelector('.avatar'); + ab.style.width = '20px'; + postBox.removeChild(ab.parentElement); + const ub = postBox.querySelector('.post-box').querySelector('.user-box'); + ub.insertBefore(ab, ub.firstChild); + ab.className = ""; + ab.style.borderRadius = "3px"; + cell.style.verticalAlign = 'top'; + cell.appendChild(postBox); + // Post box overflows horizontically (long links, etc). Remove set width and let it goooo + if (postBox.scrollWidth > postBox.clientWidth) { + postBox.style.width = "initial"; + } + }); + }, 500); + }; +} + +function getPostID(input) { + const parsed = parseInt(input.value); + if (isNaN(parsed)) { + matches = input.value.match(/(?:https?:\/\/)?((posts.*pnut.*)|.*pnut.*posts)\/(\d+)/); + if (!matches || Number.isInteger(matches[matches.length-1])) { + return -1; + } + return parseInt(matches[matches.length-1]); + } + return parsed; +} + +function showError(message, params = {}) { + params.type = 'error'; + notify(message, params); +} + +function notify(message, params) { + defaultOptions = { + class: 'notif', + position: 'top|right', + duration: 5, + type: 'info' + }; + const mergedOptions = { + ...defaultOptions, + ...params, + }; + mergedOptions.duration *= 1000; + switch (mergedOptions.type) { + case 'success': + console.log(message); + toast.success(message, mergedOptions); + break; + case 'warn': + console.warn(message); + toast.warn(message, mergedOptions); + break; + case 'error': + console.error(message); + toast.alert(message, mergedOptions); + break; + default: + console.log(message); + toast.message(message, mergedOptions); + break; + } +} diff --git a/scripts/roastmonday_common.js b/scripts/roastmonday_common.js new file mode 100644 index 0000000..076837e --- /dev/null +++ b/scripts/roastmonday_common.js @@ -0,0 +1,222 @@ +/* + * JS doesn't support directly declaring objects + * with '--' and '-' in their properties. + * + * Well technically it deos using bracket-notation like this: + * foo['--bar'] =42; + * But that's not that much better. + * + * So instead this JSON.parse is used. + */ + +window.themes = JSON.parse('{\ + "white": {\ + "--main-highlight-color": "#EF940B",\ + "--main-bg-color": "white", \ + "--secondary-bg-color": "white", \ + "--main-text-color": "black",\ + "--main-link-color": "#106BF4",\ + "--shadow-color": "#888",\ + "--gray-light": "#cacaca",\ + "--gray-medium": "#e0e0e0",\ + "--gray-dark": "#4c4c4c",\ + "--red": "red",\ + "--green": "green"\ + },\ + "black": {\ + "--main-highlight-color": "#EE6E1F",\ + "--main-link-color": "#1191e0",\ + "--main-bg-color": "black",\ + "--secondary-bg-color": "black", \ + "--main-text-color": "white",\ + "--shadow-color": "#777",\ + "--gray-light": "#4c4c4c",\ + "--gray-medium": "#333",\ + "--gray-dark": "#cacaca",\ + "--red": "#ef0000",\ + "--green": "#00ab00"\ + },\ + "ash": {\ + "--main-highlight-color": "#EE6E1F",\ + "--main-link-color": "#1191e0",\ + "--main-bg-color": "#333",\ + "--secondary-bg-color": "#333", \ + "--main-text-color": "#d0d0d0",\ + "--shadow-color": "#656565",\ + "--gray-light": "#4c4c4c",\ + "--gray-medium": "#3b3b3b",\ + "--gray-dark": "#cacaca",\ + "--red": "#e4053e",\ + "--green": "#04c446"\ + },\ + "midnight": {\ + "--main-highlight-color": "#1caff6",\ + "--main-link-color": "#1caff6",\ + "--main-bg-color": "#111",\ + "--secondary-bg-color": "#045d89", \ + "--main-text-color": "#d0d0d0",\ + "--shadow-color": "#656565",\ + "--gray-light": "#3f3f3f",\ + "--gray-medium": "#3b3b3b",\ + "--gray-dark": "#cacaca",\ + "--red": "#f72900",\ + "--green": "#82dd00"\ + }\ +}'); +window.IS_TOUCH_DEVICE = false; +window.addEventListener('touchstart', function() { + window.IS_TOUCH_DEVICE = true; +}); + +window.queries = getQueries(); +setTheme(queries.theme); +if (queries.debugTouch) { + IS_TOUCH_DEVICE = true; + console.log("Touch device mode set"); +} + +document.addEventListener("DOMContentLoaded", function() { + displayThemeChooser(); +}); + +function setTheme(t, save = true, element = null) { + let themeName = 'white'; + if (!!t && (t in themes)) { + themeName = t; + } else { + const ls = localStorage.getItem('theme'); + if (!!ls && (ls in themes)) { + themeName = ls; + } else { + save = false; + // Use default. Switch to ash if dark mode and don't save + if (matchMedia("(prefers-color-scheme: dark)")) { + themeName = 'ash'; + } + } + } + if (save) { + localStorage.setItem('theme', themeName); + } + const theme = themes[themeName]; + const doc = element || document.querySelector(':root'); + Object.keys(theme).forEach(varName => { + doc.style.setProperty(varName, theme[varName]); + }); +} + +function displayThemeChooser() { + const wrapper = document.getElementById('themeChooser'); + if (!wrapper) { + return; + } + const themeNames = Object.keys(themes); + if (themeNames.length < 2) { + return; + } + const list = document.createElement('ul'); + list.classList.add('theme-chooser-list'); + themeNames.forEach(t => { + const link = document.createElement('a'); + link.style.position = 'relative'; + link.href = `?theme=${t}`; + link.onclick = function (e) { + const t = this.href.split('theme=')[1]; + e.preventDefault(); + if (!IS_TOUCH_DEVICE) { + setTheme(t); + return; + } + const callout = document.querySelector('.theme-preview'); + if (callout && callout.getAttribute('currentTheme') == t) { + // Clicked on already seelcted theme - apply! + callout.style.display = "none"; + document.querySelector('.themelist').style.filter = ''; + try { + let d = document.querySelector('.dimmer').remove(); + } catch (err) { + // Do nothing + } + setTheme(t); + return; + } else { + const rect = list.getBoundingClientRect(); + setTheme(t, false, callout); + callout.setAttribute('currentTheme', t); + callout.style.display = "inherit"; + callout.style.bottom = document.querySelector('.theme-chooser-list').getBoundingClientRect().top + "px"; + console.log(callout, callout.style); + document.querySelector('.themelist').style.filter = 'blur(5px)'; + if (!document.querySelector('.dimmer')) { + let dimmer = document.createElement('div'); + dimmer.classList.add('dimmer'); + document.body.appendChild(dimmer); + } + } + }; + link.onmouseenter = function (e) { + if (!IS_TOUCH_DEVICE) { + const t = this.href.split('theme=')[1]; + setTheme(t, false); + } + }; + link.onmouseleave = function (e) { + if (!IS_TOUCH_DEVICE) { + setTheme(null, false); + } + }; + const container = document.createElement('li'); + container.classList.add('theme-chooser-list-item'); + const textContainer = document.createElement('span'); + link.appendChild(container); + textContainer.innerText = t; + container.appendChild(textContainer); + list.appendChild(link); + }); + wrapper.appendChild(list); +} + +function getQueries() { + const query = window.location.search.substring(1); + const vars = query.split('&'); + const queries = {}; + vars.forEach(v => { + const pair = v.split('='); + if (pair.length > 1) { + queries[pair[0]] = decodeURIComponent(pair[1]); + } else { + queries[pair[0]] = null; + } + }); + return queries; +} +function get(url) { + // Return a new promise. + return new Promise(function(resolve, reject) { + // Do the usual XHR stuff + let req = new XMLHttpRequest(); + req.open('GET', url); + + req.onload = function() { + // This is called even on 404 etc + // so check the status + if (req.status == 200) { + // Resolve the promise with the response text + resolve(req.response); + } + else { + // Otherwise reject with the status text + // which will hopefully be a meaningful error + reject(Error(req.statusText)); + } + }; + + // Handle network errors + req.onerror = function() { + reject(Error('Network Error')); + }; + + // Make the request + req.send(); + }); +} \ No newline at end of file diff --git a/scripts/toast.js b/scripts/toast.js new file mode 100644 index 0000000..95d71ec --- /dev/null +++ b/scripts/toast.js @@ -0,0 +1,157 @@ +const setStyles = (el, styles) => { + Object.keys(styles).forEach((key) => { + el.style[key] = styles[key]; + }); +}; + +const setAttrs = (el, attrs) => { + Object.keys(attrs).forEach((key) => { + el.setAttribute(key, attrs[key]); + }); +}; + +const getAttr = (el, attr) => el.getAttribute(attr); + +const privateKeys = { + defaultOptions: Symbol('defaultOptions'), + render: Symbol('render'), + show: Symbol('show'), + hide: Symbol('hide'), + removeDOM: Symbol('removeDOM'), +}; + +const siiimpleToast = { + [privateKeys.defaultOptions]: { + container: 'body', + class: 'siiimpleToast', + position: 'top|center', + margin: 15, + delay: 0, + duration: 3000, + style: {}, + }, + + setOptions(options = {}) { + return { + ...siiimpleToast, + [privateKeys.defaultOptions]: { + ...this[privateKeys.defaultOptions], + ...options, + }, + }; + }, + + [privateKeys.render](state, message, options = {}) { + const mergedOptions = { + ...this[privateKeys.defaultOptions], + ...options, + }; + + const { + class: className, + position, + delay, + duration, + style, + } = mergedOptions; + + const newToast = document.createElement('div'); + + // logging via attrs + newToast.className = className; + newToast.innerHTML = message; + + setAttrs(newToast, { + 'data-position': position, + 'data-state': state, + }); + + setStyles(newToast, style); + + // use .setTimeout() instead of $.queue() + let time = 0; + setTimeout(() => { + this[privateKeys.show](newToast, mergedOptions); + }, time += delay); + setTimeout(() => { + this[privateKeys.hide](newToast, mergedOptions); + }, time += duration); + + // support method chaining + return this; + }, + + [privateKeys.show](el, { container, class: className, margin }) { + const hasPos = (v, pos) => getAttr(v, 'data-position').indexOf(pos) > -1; + + const root = document.querySelector(container); + root.insertBefore(el, root.firstChild); + + // set initial position + setStyles(el, { + position: container === 'body' ? 'fixed' : 'absolute', + [hasPos(el, 'top') ? 'top' : 'bottom']: '-100px', + [hasPos(el, 'left') && 'left']: '15px', + [hasPos(el, 'center') && 'left']: `${(root.clientWidth / 2) - (el.clientWidth / 2)}px`, + [hasPos(el, 'right') && 'right']: '15px', + }); + + setStyles(el, { + transform: 'scale(1)', + opacity: 1, + }); + + // push effect + let pushStack = margin; + + Array + .from(document.querySelectorAll(`.${className}[data-position="${getAttr(el, 'data-position')}"]`)) + .filter(toast => toast.parentElement === el.parentElement)// matching container + .forEach((toast) => { + setStyles(toast, { + [hasPos(toast, 'top') ? 'top' : 'bottom']: `${pushStack}px`, + }); + + pushStack += toast.offsetHeight + margin; + }); + }, + + [privateKeys.hide](el) { + const hasPos = (v, pos) => getAttr(v, 'data-position').indexOf(pos) > -1; + const { left, width } = el.getBoundingClientRect(); + + setStyles(el, { + [hasPos(el, 'left') && 'left']: `${width}px`, + [hasPos(el, 'center') && 'left']: `${left + width}px`, + [hasPos(el, 'right') && 'right']: `-${width}px`, + opacity: 0, + }); + + const whenTransitionEnd = () => { + this[privateKeys.removeDOM](el); + el.removeEventListener('transitionend', whenTransitionEnd); + }; + + el.addEventListener('transitionend', whenTransitionEnd); + }, + + [privateKeys.removeDOM](el) {// eslint-disable-line + const parent = el.parentElement; + parent.removeChild(el); + }, + + message(message, options) { + return this[privateKeys.render]('default', message, options); + }, + success(message, options) { + return this[privateKeys.render]('success', message, options); + }, + warn(message, options) { + return this[privateKeys.render]('warn', message, options); + }, + alert(message, options) { + return this[privateKeys.render]('alert', message, options); + } +}; + +window.toast = siiimpleToast; \ No newline at end of file diff --git a/set_temp_avatar.php b/set_temp_avatar.php new file mode 100644 index 0000000..84c4abe --- /dev/null +++ b/set_temp_avatar.php @@ -0,0 +1,36 @@ +isAuthenticated()) { + $url = $app->getAuthURL(); + die('{"authenticated":false, "authUrl": "' . $url . '"}'); +} +if (isset($_GET['status'])) { + $resp = [ + 'authenticated' => true, + 'maxPostLength' => $app->getMaxPostLength() + ]; + die(json_encode($resp)); +} +if (empty($_POST['submit'])) { + $e = ['error' => [ + 'message' => 'Unknown action', + ]]; + die(json_encode($e)); +} +$resp = $app->addAvatar( + $_POST['theme'], + $_POST['shouldPostAvatar'] ?? false, + $_FILES['avatar'], + $_POST['avatarDuration'], + $_POST['posttext'], + overwrite_if_exist: true +); +die(json_encode($resp)); diff --git a/src/DB/BaseDB.php b/src/DB/BaseDB.php new file mode 100644 index 0000000..a4307de --- /dev/null +++ b/src/DB/BaseDB.php @@ -0,0 +1,74 @@ +logger = new NullLogger(); + } else { + $this->logger = $logger; + } + try { + $this->conn = new \PDO( + 'mysql:host=' + . $servername + . ';dbname=' + . $db, + $username, + $password + ); + } catch (\PDOException $Exception) { + if (count($Exception->errorInfo) >= 2 && $Exception->errorInfo[1] === DB::$ERR_CONNECTION_LIMIT) { + throw new ConnectionLimitReached(); + } elseif (count($Exception->errorInfo) >= 2 && $Exception->errorInfo[1] === DB::$ERR_TIMEOUT) { + throw new OperationTimedOutException($Exception->getMessage()); + } else { + #echo json_encode($Exception->errorInfo); + throw new DBException($Exception->getMessage()); + } + } + $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + } + + /** + * Get name of the constant for the result value + * @param integer $value + * + * @return string + */ + public static function getResultName(int $value) + { + $constantNames = array_flip( + array_filter( + (new \ReflectionClass(static::class))->getStaticProperties(), + static fn($v) => is_scalar($v), // only int/string + ), + ); + + return $constantNames[$value] ?? null; + } +} diff --git a/src/DB/ConnectionLimitReached.php b/src/DB/ConnectionLimitReached.php new file mode 100644 index 0000000..48f8190 --- /dev/null +++ b/src/DB/ConnectionLimitReached.php @@ -0,0 +1,7 @@ +except_on_duplicate_entries = $except_on_duplicate_entries; + $this->logger->info("DB test"); + } + + public function listThemes() + { + $stmt = $this->conn->prepare( + 'SELECT * FROM `' + . static::$db_themes + . '` ORDER BY themedate DESC' + ); + $stmt->execute(); + + // set the resulting array to associative + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + $themes = $stmt->fetchAll(); + $t = []; + foreach ($themes as $theme) { + $t[] = [ + 'idx' => $theme['id'], + 'date' => new \DateTime($theme['themedate']), + 'tag' => $theme['themetag'] + ]; + } + + return $t; + } + + public function getTheme($id) + { + $stmt = $this->conn->prepare( + 'SELECT * FROM `' + . static::$db_themes + . '` WHERE id = :id' + ); + $stmt->bindParam(':id', $id); + $stmt->execute(); + // set the resulting array to associative + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + $theme = $stmt->fetch(); + return [ + 'idx' => $theme['id'], + 'date' => new \DateTime($theme['themedate']), + 'tag' => $theme['themetag'] + ]; + } + + public function saveAvatar( + $theme_id, + $username, + $user_realname, + $post_id, + $posttext, + $postdate, + $avatar, + $overwrite_if_exist = false + ) { + $search = $this->conn->prepare( + 'SELECT COUNT(id) as pics FROM `' + . static::$db_avatars + . '` WHERE user_name LIKE :username AND thememonday = :theme' + ); + $search->bindParam(':theme', $theme_id); + $search->bindParam(':username', $username); + + $should_update = false; + try { + $search->execute(); + $search->setFetchMode(\PDO::FETCH_ASSOC); + $p = $search->fetch(); + if ($p['pics'] != 0) { + if ($overwrite_if_exist) { + $should_update = true; + } else { + # Avatar already in DB + return static::$SKIPPED_DUPLICATE; + } + } + } catch (\PDOException $Exception) { + throw new DBException($Exception->getMessage()); + } + + if ($should_update) { + $this->logger->info("Found existing avatar. Will overwrite", ['user' => $username, 'theme' => $theme_id]); + $sql = $this->conn->prepare( + 'UPDATE `' + . static::$db_avatars + . '` SET postid = :postid, posttext = :posttext, postdate = :postdate, ' + . ' avatar_filename = :avatar' + . ' WHERE user_name LIKE :username AND thememonday = :theme' + ); + } else { + $sql = $this->conn->prepare( + 'INSERT INTO `' + . static::$db_avatars + . '` (thememonday, user_name, postid, avatar_filename, posttext, user_realname, postdate)' + . 'VALUES (:theme, :username, :postid, :avatar, :posttext, :user_realname, :postdate)' + ); + $sql->bindParam(':user_realname', $user_realname); + } + + + $sql->bindParam(':theme', $theme_id); + $sql->bindParam(':username', $username); + $sql->bindParam(':postid', $post_id); + $sql->bindParam(':posttext', $posttext); + $sql->bindParam(':postdate', $postdate); + $sql->bindParam(':avatar', $avatar); + try { + $sql->execute(); + } catch (\PDOException $Exception) { + if (count($Exception->errorInfo) >= 2 && $Exception->errorInfo[1] === static::$ERR_DUPLICATE_ENTRY) { + if ($this->except_on_duplicate_entries) { + throw new DuplicateEntryException(); + } else { + return static::$TRIED_INSERT_DUPLICATE; + } + } else { + throw new DBException($Exception->getMessage()); + } + } + return static::$SUCCESS; + } + + public function getAvatarsForTheme($id) + { + $stmt = $this->conn->prepare( + 'SELECT * FROM `' + . static::$db_avatars + . '` WHERE thememonday = :id' + ); + $stmt->bindParam(':id', $id); + $stmt->execute(); + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + $avatars = $stmt->fetchAll(); + $a = []; + + foreach ($avatars as $avatar) { + $d = new \DateTime(); + $d->setTimestamp($avatar['postdate']); + $a[] = [ + 'file' => $avatar['avatar_filename'], + 'post_id' => $avatar['postid'], + 'posttext' => $avatar['posttext'], + 'postdate' => $d, + 'username' => $avatar['user_name'], + 'user_realname' => $avatar['user_realname'], + ]; + } + return $a; + } + + public function addTheme($tag, $date) + { + $search = $this->conn->prepare( + 'SELECT id FROM`' + . static::$db_themes + . '` WHERE themedate LIKE :date AND themetag = :tag' + ); + $search->bindParam(':date', $date); + $search->bindParam(':tag', $tag); + + try { + $search->execute(); + $search->setFetchMode(\PDO::FETCH_ASSOC); + $p = $search->fetch(); + if ($p !== false) { + return $p['id']; + } + } catch (\PDOException $Exception) { + throw new DBException($Exception->getMessage()); + } + + $sql = $this->conn->prepare( + 'INSERT INTO `' + . static::$db_themes + . '` (themedate, themetag) VALUES (:date, :tag)' + ); + $sql->bindParam(':date', $date); + $sql->bindParam(':tag', $tag); + try { + $sql->execute(); + } catch (\PDOException $Exception) { + if (count($Exception->errorInfo) >= 2 && $Exception->errorInfo[1] === DB::$ERR_DUPLICATE_ENTRY) { + if ($this->except_on_duplicate_entries) { + throw new DuplicateEntryException(); + } else { + return $this->getIdForTag($tag); + } + } else { + throw new DBException($Exception->getMessage()); + } + } + return $this->conn->lastInsertId(); + } + + public function getIdForTag($tag) + { + $stmt = $this->conn->prepare( + 'SELECT id FROM `' + . static::$db_themes + . '` WHERE themetag LIKE :tag' + ); + $stmt->bindParam(':tag', $tag); + $stmt->execute(); + // set the resulting array to associative + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + $theme = $stmt->fetch(); + return $theme['id']; + } + + public function addTempAvatar($user_id, $remove_at, $original_avatar, $auth_token) + { + # Delete old entries + $sql = $this->conn->prepare( + 'DELETE FROM `' + . static::$db_temp_avatars + . '` WHERE user_id = :user_id' + ); + $sql->bindParam(':user_id', $user_id); + try { + $sql->execute(); + } catch (\PDOException $Exception) { + throw new DBException($Exception->getMessage()); + } + # Add new entry + $sql = $this->conn->prepare( + 'INSERT INTO `' + . static::$db_temp_avatars + . '` (user_id, remove_at, original_avatar, auth_token)' + . 'VALUES (:user_id, FROM_UNIXTIME(:remove_at), :original_avatar, :auth_token)' + ); + $sql->bindParam(':user_id', $user_id); + $sql->bindParam(':remove_at', $remove_at); + $sql->bindParam(':original_avatar', $original_avatar); + $sql->bindParam(':auth_token', $auth_token); + try { + $sql->execute(); + } catch (\PDOException $Exception) { + throw new DBException($Exception->getMessage()); + } + return $this->conn->lastInsertId(); + } + + public function getOutdatedThemeAvatars() + { + $sql = 'SELECT * FROM `' . static::$db_temp_avatars . '` WHERE remove_at <= CURRENT_TIMESTAMP()'; + $stmt = $this->conn->prepare($sql); + $stmt->execute(); + $res = $stmt->fetchAll(\PDO::FETCH_ASSOC); + return $res; + } + + public function removeOutdatedThemeAvatars($user_id) + { + $sql = 'DELETE FROM `' . static::$db_temp_avatars . '` WHERE user_id = :user_id'; + $stmt = $this->conn->prepare($sql); + $stmt->bindParam(':user_id', $user_id); + $stmt->execute(); + } +} diff --git a/src/DB/DBException.php b/src/DB/DBException.php new file mode 100644 index 0000000..c4f1e1b --- /dev/null +++ b/src/DB/DBException.php @@ -0,0 +1,7 @@ +logger->info("App: {$this->app_name}"); + $this->pics_root = $pics_root . 'pics/'; + $this->temp_pics_root = '../temp_avatars/'; + if (!is_dir($this->pics_root . $this->temp_pics_root)) { + mkdir($this->pics_root . $this->temp_pics_root); + } + // TODO: Clean up logger everywhere to use context + // TODO: move to logrotate + // TODO: Convert TODOs to issues + $this->logger->info('Pics root ', ['path' => $this->pics_root]); + $this->logger->info('Pics root realpath', ['path' => realpath($this->pics_root)]); + $this->logger->info('Temp pics root', ['path' => $this->pics_root . $this->temp_pics_root]); + $this->logger->info('Temp pics root realpath', ['path' => realpath($this->pics_root . $this->temp_pics_root)]); + + $db_logger = null; + if ($this->logger instanceof \Monolog\Logger) { + $db_logger = ($this->logger)->withName('database'); + $this->logger->info("Setting up DB Logger"); + } else { + $this->logger->info("Logger is not a Monolog logger, can not clone for DB Logger"); + } + $this->db = new DB( + $config['db_servername'], + $config['db_database'], + $config['db_username'], + $config['db_password'], + logger: $db_logger + ); + } + + // TODO: Support more extensions + protected function getExtension($ct) + { + $ext = 'jpg'; + switch ($ct) { + case 'image/png': + $ext = 'png'; + break; + case 'image/bmp': + $ext = 'bmp'; + break; + case 'image/gif': + $ext = 'gif'; + break; + case 'image/jpeg': + default: + $ext = 'jpg'; + break; + } + return $ext; + } + + protected function downloadPicture($link, $target_folder = '') + { + $this->logger->info("Downloading picture", ['url' => $link]); + $img_header = get_headers($link, 1); + if (strpos($img_header[0], "200") === false && strpos($img_header[0], "OK")) { + $this->logger->error("Error fetching avatar header: " . json_encode($img_header)); + return null; + } + $ext = $this->getExtension($img_header['Content-Type']); + $exp = explode('/', explode('?', $link)[0]); + $img_name = array_pop($exp) . '.' . $ext; + + $this->savePicture($link, $target_folder, $img_name); + return $img_name; + } + + protected function savePicture($url, $target_folder, $filename) + { + $pics_folder = $this->pics_root . $target_folder; + $this->logger->debug("Checking pics directory", ['path' => $pics_folder]); + if (!is_dir($pics_folder)) { + mkdir($pics_folder); + } + $d = $pics_folder; + + if (!(substr($d, -1) === '/')) { + $d .= '/'; + } + $d .= $filename; + if (!file_exists($d)) { + $this->logger->info('Saving avatar to ', ['path' => $d]); + $av = file_get_contents($url); + $fp = fopen($d, "w"); + fwrite($fp, $av); + fclose($fp); + } else { + $this->logger->info('already exists. Skipping.', ['path' => $d]); + } + } + + protected function getThemeMondaysFromWiki() + { + $m = []; + $html = file_get_contents('https://wiki.pnut.io/ThemeMonday'); + if ($html === false) { + $this->logger->error('Error connecting to pnut wiki'); + return []; + } + // TODO: Use \Dom\HTMLDocument + $doc = new \DOMDocument(); + $doc->loadHTML($html); + if ($doc === false) { + $this->logger->error('Error loading HTML from pnut wiki'); + return []; + } + $past_themes_element = $doc->getElementById('Past_Themes_on_Pnut'); + $past_themes_list = []; + try { + $past_themes_list = $past_themes_element->parentNode->nextSibling->nextSibling; + } catch (\Exception $e) { + $this->logger->error('Error parsing wiki: ' . $e->getMessage()); + return []; + } + foreach ($past_themes_list->childNodes as $child) { + if ($child->nodeName === 'li') { + $tag = null; + $date = null; + foreach ($child->childNodes as $list_entry) { + if ($list_entry->nodeName === 'a' && $list_entry->nodeValue !== null) { + $date = \DateTime::createFromFormat('Y F d', $list_entry->nodeValue); + if ($date === false) { + $date = null; + } + } elseif ($list_entry->nodeName === '#text' && $list_entry->nodeValue !== null) { + $arr = explode('#', str_replace(': ', '', $list_entry->nodeValue)); + if (count($arr) > 1) { + $tag = '#' . $arr[1]; + } + } + if ($tag !== null && $date !== null) { + $this->logger->info( + 'Found ThemeMonday in wiki: ' + . trim($tag) + . ' on ' + . $date->format('Y-m-d') + ); + $m[] = [ + 'date' => new \DateTime($date->format('Y-m-d')), + 'tag' => trim($tag) + ]; + } + } + } + } + return $m; + } + + public function getThemeMonday($id) + { + $t = $this->db->getTheme($id); + $this->logger->debug('Theme ' . $id . ': ' . json_encode($t)); + return $t; + } + + protected function getThemeMondaysFromAccountPost() + { + $this->logger->info('Parsing @ThemeMonday polls'); + $m = []; + $p = []; + try { + $p = $this->getPollsFromUser(616); + } catch (\Exception $e) { + $this->logger->error('Error reading @ThemeMonday polls: ' . $e->getMessage()); + return []; + } + + $p = array_filter($p, function ($e) { + return stripos($e->prompt, '#thememonday') !== false; + }); + $this->logger->info('Found ' . count($p) . ' polls'); + if (count($p) === 0) { + return []; + } + foreach ($p as $poll) { + $tag = $poll->getMostVotedOption(); + if (count($tag) !== 1) { + $this->logger->info('Skipping ' . implode(', ', $tag) . ', because it was a tie'); + continue; + } + $date = Roastmonday::getMondayAfterPoll($poll); + $this->logger->info( + 'Found ThemeMonday: ' + . $tag[0]->text + . ' on ' + . $date->format('Y-m-d') + ); + $m[] = [ + 'date' => new \DateTime($date->format('Y-m-d')), + 'tag' => $tag[0]->text + ]; + } + return $m; + } + + protected static function getMondayAfterPoll($poll) + { + $d = $poll->closed_at; + $days_to_add = (7 - ($d->format('w') - 1)) % 7; + return $d->modify('+' . $days_to_add . ' days'); + } + + protected function savePictureForPost($post, $theme_id) + { + $this->logger->info('Checking post ' . $post->id . ' for theme ' . $theme_id); + if (!empty($post->user) && !empty($post->user->avatar_image)) { + $this->logger->info( + 'Found new avatar on post ' + . $post->id + . ' by @' + . $post->user->username + ); + $filename = $this->downloadPicture($post->user->avatar_image->link, $theme_id); + if ($filename !== null) { + $realname = null; + if (isset($post->user, $post->user->name)) { + $realname = $post->user->name; + } + $text = ""; + if (isset($post->content)) { + if (isset($post->content->html)) { + $text = $post->content->html; + } elseif (isset($post->content->text)) { + $text = $post->content->text; + } + } + $this->logger->info( + 'Saving avatar by ' + . $post->user->username + . ' for ' + . $theme_id + . ' to database' + ); + try { + $status = $this->db->saveAvatar( + $theme_id, + $post->user->username, + $realname, + $post->id, + $text, + $post->created_at->getTimestamp(), + $filename + ); + switch ($status) { + case DB::$TRIED_INSERT_DUPLICATE: + $this->logger->info('Error, tried to insert a duplicate avatar'); + break; + case DB::$SKIPPED_DUPLICATE: + $this->logger->info('Skipped, duplicate avatar'); + break; + case DB::$SUCCESS: + $this->logger->info('Successfully inserted avatar'); + break; + default: + $this->logger->info('Unknown return code ' . $status); + break; + } + } catch (\Exception $e) { + $this->logger->error('Error inserting avatar into database: ' . $e->getMessage()); + } + return $filename; + } + $this->logger->warning( + "Cannot save picture for post {$post->id}: Doesn't have a user or user doesn't have an avatar" + ); + } + } + + public function getPost($post_id, $args = []) + { + $post = parent::getPost($post_id, $args); + if (array_key_exists('avatarWidth', $args)) { + $a = parent::getAvatar($post->user->id, ['w' => $args['avatarWidth']]); + $this->logger->debug("Resized avatar: {$a}"); + $post->user->avatar_image->link = $a; + } + return $post; + } + + protected function removeDuplicateThemes($arr) + { + $tmp = []; + foreach ($arr as $e) { + if (!in_array($e, $tmp)) { + $f = false; + foreach ($tmp as $t) { + if (strtolower($t['tag']) === strtolower($e['tag'])) { + $f = true; + break; + } + } + if (!$f) { + $tmp[] = $e; + } + } + } + return $tmp; + } + + public function getThemeMondays() + { + $themes = $this->db->listThemes(); + $this->logger->debug('Themes: ' . json_encode($themes)); + return $themes; + } + + public function getPicturesForTheme($id) + { + $this->logger->debug("Avatars for theme {$id}: "); + $pics_folder = $this->pics_root . $id . '/'; + $pics = []; + if (!is_dir($pics_folder)) { + $this->logger->warning($pics_folder . ' ist not a directory'); + return []; + } + $this->logger->debug("DB: " . json_encode($this->db)); + $avatars = $this->db->getAvatarsForTheme($id); + foreach ($avatars as $avatar) { + $avatar['file'] = $pics_folder . $avatar['file']; + $pics[] = $avatar; + } + return $pics; + } + + public function findThemeMondays() + { + $this->logger->info('Searching for theme mondays'); + $m = $this->getThemeMondaysFromAccountPost(); + $m = array_filter($m, function ($e) { + return !empty($e); + }); + $m = $this->removeDuplicateThemes( + array_merge($m, $this->getThemeMondaysFromWiki()) + ); + usort($m, function ($a, $b) { + if ($a['date'] == $b['date']) { + return 0; + } + return $a['date'] > $b['date'] ? -1 : 1; + }); + $this->logger->info('Found theme mondays', $m); + return $m; + } + + public function savePicturesForTheme($theme) + { + $tag = preg_replace('/^#/', '', $theme['tag']); + $tag = preg_replace('/ .*$/', '', $tag); + $posts = []; + $this->logger->info('Searching pictures for: ' . $tag); + foreach (Roastmonday::$new_avatar_keywords as $keyword) { + $query = [ + 'tags' => $tag, + 'q' => $keyword, + 'include_deleted' => false, + 'include_client' => false, + 'include_counts' => false, + 'include_html' => false, + ]; + $p = $this->searchPosts($query); + $this->logger->info('Found ' . count($p) . ' posts'); + foreach ($p as $post) { + $this->savePictureForPost($post, $theme['id']); + if (!in_array($post, $posts)) { + $posts[] = $post; + } + } + } + } + + public function addTheme($tag, $date) + { + $tag = preg_replace('/^#/', '', $tag); + $tag = preg_replace('/ .*$/', '', $tag); + $this->logger->info('Adding: ' . $tag . ' to database'); + $id = $this->db->addTheme($tag, $date->format('Y-m-d')); + if ($id !== -1) { + $pics_folder = $this->pics_root . $id . '/'; + if (!is_dir($pics_folder)) { + mkdir($pics_folder); + } + } + return $id; + } + + public function addAvatar( + $theme, + $should_post, + $avatar, + $avatar_duration, + $posttext = null, + $overwrite_if_exist = false + ) { + $this->logger->info("Adding temporary avatar to theme {$theme} for {$avatar_duration} days"); + + switch ($avatar['error']) { + case UPLOAD_ERR_OK: + # ok + break; + case UPLOAD_ERR_NO_FILE: + return ['error' => ['message' => 'No avatar has been uploaded']]; + case UPLOAD_ERR_FORM_SIZE: + return ['error' => ['message' => 'The uploaded avatar\'s file size is too big']]; + default: + return ['error' => ['message' => 'An unknown error has occured while uploading the avatar']]; + } + + #1. Save current user avatar + $a = self::getAvatar('me'); + $this->logger->info("Current avatar: {$a}"); + #$a = explode('/', $a); + #$filename = explode('?', end($a))[0]; + $original_avatar = $this->downloadPicture($a, $this->temp_pics_root); + if (empty($original_avatar)) { + return ['error' => ['message' => 'Could not download original avatar']]; + } + + #2. Save current avatar filename + enddate to DB + $current_user = null; + try { + $current_user = $this->getAuthorizedUser(); + if (empty($current_user)) { + return ['error' => ['message' => 'Could not fetch the authorized user']]; + } + $this->logger->info("Current user: @{$current_user->username} ({$current_user->id})"); + $avatar_duration = min($avatar_duration, 1); + $d = new \DateTime(); + $d->add(new \DateInterval("P{$avatar_duration}D")); + $this->db->addTempAvatar($current_user->id, $d->getTimestamp(), $original_avatar, $this->access_token); + } catch (\Exception $e) { + return ['error' => ['message' => 'Could not save original avatar: ' . $e->getMessage()]]; + } + + #3. Upload new avatar + $astr = print_r($avatar, true); + $this->logger->info("Avatar: {$astr}"); + try { + $current_user = $this->updateAvatarFromUploaded($avatar); + } catch (\Exception $e) { + return ['error' => ['message' => 'Could not update to the theme avatar: ' . $e->getMessage()]]; + } + #4. Write post? + $this->logger->info('Post about theme avatar? ' . ($should_post ? 'Yes' : 'No') . ': ' . $should_post); + if (!$should_post) { + try { + $this->logger->info('Adding post-less avatar to gallery'); + $this->authenticateServerToken(); + $response = $this->manuallyAddAvatar( + $theme, + 0, + $avatar, + $current_user, + overwrite_if_exist: $overwrite_if_exist + ); + if (!empty($response['error'])) { + $this->logger->warning('Error adding to gallery'); + $rs = print_r($response, true); + $this->logger->warning($rs); + return [ + 'success' => true, + 'warn' => 'Your avatar has been updated, but an error occured adding it to the gallery: ' + . $response['error']['message'] + ]; + } else { + $this->logger->info('Successfully added to gallery'); + } + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + $this->logger->error($e->getTraceAsString()); + return [ + 'success' => true, + 'warn' => 'Your avatar has been updated, but an error occured adding it to the gallery: ' + . $e->getMessage() + ]; + } + return ['success' => true]; + } + if (empty($posttext)) { + return [ + 'success' => true, + 'warn' => 'You selected to post about new avatar,' + . '
but your posttext is empty.
' + . 'Your avatar has been updated, but no post has been created' + ]; + } + $this->logger->info('Posting about theme avatar'); + $post = null; + try { + // TODO: Add photo as attachment to post as well + $post = $this->createPost($posttext, is_nsfw: false, auto_crop: true); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + $this->logger->error($e->getTraceAsString()); + return [ + 'success' => true, + 'warn' => 'Your avatar has been updated, but an error occured creating the post:
' + . $e->getMessage() + ]; + } + $this->logger->info('Post created successfully'); + + #5. Add to theme DB + try { + $this->authenticateServerToken(); + $response = $this->manuallyAddAvatar($theme, $post->id, null, overwrite_if_exist: $overwrite_if_exist); + $this->logger->info('Successfully added to gallery'); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + $this->logger->error($e->getTraceAsString()); + return [ + 'success' => true, + 'postID' => $post->id, + 'warn' => 'Your avatar has been updated, and your post created,' + . '
but an error occured adding it to the gallery: ' + . $e->getMessage() + ]; + } + return ['success' => true, 'postID' => $post->id, 'newGalleryAvatar' => $response]; + } + + public function manuallyAddAvatar($theme, $post_id, $avatar, $user = null, $overwrite_if_exist = false) + { + $this->logger->info( + "Manually adding avatar to theme from post", + ['theme' => $theme, 'post_id' => $post_id] + ); + $resp = []; + $date = new \DateTime(); + $post = null; + try { + if (empty($user)) { + $post = $this->getPost($post_id); + $user = $post->user; + $date = $post->created_at; + if (empty($user)) { + $this->logger->error("Error fetching avatar from post #{$post_id}. Post has no creator."); + $resp['error'] = [ + 'code' => static::$ERROR_FETCHING_CREATOR_FIELD, + 'message' => 'Error fetching avatar. Post has no creator.' + ]; + return $resp; + } + } + if (empty($user->avatar_image) || empty($user->avatar_image->link)) { + $this->logger->error("Error fetching avatar from post #{$post_id}. Post creator has no avatar."); + $resp['error'] = [ + 'code' => static::$ERROR_FETCHING_CREATOR_FIELD, + 'message' => 'Error fetching avatar. Post creator has no avatar.' + ]; + return $resp; + } + $astr = print_r($avatar, true); + $this->logger->debug("Manually adding avatar: {$astr}"); + if (!empty($avatar) && (empty($avatar['error']) || $avatar['error'] === 0)) { + $this->logger->info("Manually added post #{$post_id} has an avatar attached."); + $ext = $this->getExtension($avatar['type']); + $target_file_name_without_theme = time() . '_' . $post_id . '.' . $ext; + $target_file_name = $theme . '/' . $target_file_name_without_theme; + $target_file = $this->pics_root . $target_file_name; + if (move_uploaded_file($avatar["tmp_name"], $target_file)) { + // TODO: Remove hardcoded path + $resp['img'] = 'pics/' . $target_file_name; + } else { + $this->logger->error("Error saving uploaded avatar from post #{$post_id}. Post has no creator."); + $resp['error'] = [ + 'code' => static::$ERROR_UNKNOWN_SERVER_ERROR, + 'message' => 'Uploaded file could not be moved' + ]; + return $resp; + } + $this->logger->info("Saved manually uploaded file to " . $target_file . ". Adding to database"); + $realname = null; + $username = ''; + if (isset($user)) { + if (isset($user->name)) { + $realname = $user->name; + } + $username = $user->username; + $resp['presence'] = $user->getPresenceInt(); + } + $posttext = empty($post) ? '' : $post->getText(); + $status = $this->db->saveAvatar( + $theme, + $username, + $realname, + $post_id, + $posttext, + $date->getTimestamp(), + $target_file_name_without_theme, + $overwrite_if_exist + ); + $this->logger->info( + "Saved manually uploaded file and added to database", + ['path' => $target_file, 'db_status_code' => $status, 'db_status' => DB::getResultName($status)] + ); + } else { + $this->logger->info( + "Manually added post #{$post_id} doesn't have an avatar attached. fetching from user object." + ); + $filename = $this->savePictureForPost($post, $theme); + if (empty($filename)) { + $this->logger->error("Error fetching avatar from post #{$post_id}."); + $resp['error'] = [ + 'code' => static::$ERROR_UNKNOWN_SERVER_ERROR, + 'message' => "Error fetching avatar from post #{$post_id}." + ]; + return $resp; + } + $resp['img'] = 'pics/' . $theme . '/' . $filename; + //protected function savePicture($avatar, $theme_id, $filename) { + //$avatar = file_get_contents($this->pics_root.$theme.'/'.$filename); + $this->logger->info("Saved downloaded avatar to {$resp['img']}"); + } + $resp['presence'] = -1; + $realname = null; + $username = ''; + if (isset($user)) { + if (isset($user->name)) { + $realname = $user->name; + } + $username = $user->username; + $resp['presence'] = $user->getPresenceInt(); + } + $resp['user'] = '@' . $username; + + $text = ""; + /* + if (isset($post->content)) { + if (isset($post->content->html)) { + $text = $post->content->html; + } elseif (isset($post->content->text)) { + $text = $post->content->text; + } + } + */ + $text = empty($post) ? '' : $post->getText(); + $resp['success'] = true; + $resp['realname'] = $realname; + $resp['posttext'] = $text; + $resp['postid'] = $post_id; + $resp['timestamp'] = $date->getTimestamp(); + $this->logger->info( + "Successfully added manually uploaded avatar for user to theme", + ['user' => $resp['user'], 'theme' => $theme] + ); + return $resp; + } catch (NotFoundException $nfe) { + $this->logger->error( + "Error adding manually uploaded avatar for theme. Post could not be found", + ['post_id' => $post_id, 'theme' => $theme, 'exception' => $nfe] + ); + $resp['error'] = ['code' => static::$ERROR_NOT_FOUND, 'message' => 'Post could not be found']; + return $resp; + } catch (\Exception $e) { + $this->logger->error("Error adding manually uploaded avatar for theme {$theme}: " . $e->getMessage()); + $resp['error'] = ['code' => static::$ERROR_UNKNOWN_SERVER_ERROR, 'message' => $e->getMessage()]; + return $resp; + } + } + + public function resetOriginalAvatars() + { + $this->logger->info('Get outdated avatars'); + $to_reset = $this->db->getOutdatedThemeAvatars(); + $this->logger->info('Found ' . count($to_reset) . ' outdated avatars'); + foreach ($to_reset as $value) { + $user_id = $value['user_id']; + $p = $this->pics_root . $this->temp_pics_root . $value['original_avatar']; + $avatar_path = realpath($p); + if ($avatar_path === false) { + $this->logger->error("Avatar path '{$p}' for user {$user_id} does not exist!"); + } + $this->logger->info('Resetting avatar for user ' . $user_id . ' to original at: ' . $avatar_path); + $this->access_token = $value['auth_token']; + try { + $this->updateAvatar($avatar_path); + } catch (\Exception $e) { + $this->logger->error('Error resetting user avatar: ' . $e->getMessage()); + $this->logger->error($e->getTraceAsString()); + return; + } + $this->logger->info('Resetted avatar for user ' . $user_id . ' to original at: ' . $avatar_path); + $this->logger->info('Deleting avatar for user ' . $user_id . ' at: ' . $avatar_path); + $res = unlink($avatar_path); + if (!$res) { + $this->logger->error('Failed to delete avatar at: ' . $avatar_path); + return; + } + $this->logger->info('Deleted avatar for user ' . $user_id . ' at: ' . $avatar_path); + try { + $this->db->removeOutdatedThemeAvatars($user_id); + } catch (\Exception $e) { + $this->logger->error('Error resetting user avatar', ['exception' => $e]); + return; + } + $this->logger->info('Avatar for user ' . $user_id . ' is back to its original value!'); + } + } +} diff --git a/style/fonts/Inter-Black.woff b/style/fonts/Inter-Black.woff new file mode 100644 index 0000000..4df244f Binary files /dev/null and b/style/fonts/Inter-Black.woff differ diff --git a/style/fonts/Inter-Black.woff2 b/style/fonts/Inter-Black.woff2 new file mode 100644 index 0000000..35526c5 Binary files /dev/null and b/style/fonts/Inter-Black.woff2 differ diff --git a/style/fonts/Inter-BlackItalic.woff b/style/fonts/Inter-BlackItalic.woff new file mode 100644 index 0000000..60fb72e Binary files /dev/null and b/style/fonts/Inter-BlackItalic.woff differ diff --git a/style/fonts/Inter-BlackItalic.woff2 b/style/fonts/Inter-BlackItalic.woff2 new file mode 100644 index 0000000..d3f36e4 Binary files /dev/null and b/style/fonts/Inter-BlackItalic.woff2 differ diff --git a/style/fonts/Inter-Bold.woff b/style/fonts/Inter-Bold.woff new file mode 100644 index 0000000..78fde16 Binary files /dev/null and b/style/fonts/Inter-Bold.woff differ diff --git a/style/fonts/Inter-Bold.woff2 b/style/fonts/Inter-Bold.woff2 new file mode 100644 index 0000000..d5f9c27 Binary files /dev/null and b/style/fonts/Inter-Bold.woff2 differ diff --git a/style/fonts/Inter-BoldItalic.woff b/style/fonts/Inter-BoldItalic.woff new file mode 100644 index 0000000..5bdace3 Binary files /dev/null and b/style/fonts/Inter-BoldItalic.woff differ diff --git a/style/fonts/Inter-BoldItalic.woff2 b/style/fonts/Inter-BoldItalic.woff2 new file mode 100644 index 0000000..973670e Binary files /dev/null and b/style/fonts/Inter-BoldItalic.woff2 differ diff --git a/style/fonts/Inter-ExtraBold.woff b/style/fonts/Inter-ExtraBold.woff new file mode 100644 index 0000000..4c65a62 Binary files /dev/null and b/style/fonts/Inter-ExtraBold.woff differ diff --git a/style/fonts/Inter-ExtraBold.woff2 b/style/fonts/Inter-ExtraBold.woff2 new file mode 100644 index 0000000..a98733c Binary files /dev/null and b/style/fonts/Inter-ExtraBold.woff2 differ diff --git a/style/fonts/Inter-ExtraBoldItalic.woff b/style/fonts/Inter-ExtraBoldItalic.woff new file mode 100644 index 0000000..1303316 Binary files /dev/null and b/style/fonts/Inter-ExtraBoldItalic.woff differ diff --git a/style/fonts/Inter-ExtraBoldItalic.woff2 b/style/fonts/Inter-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..9490e9e Binary files /dev/null and b/style/fonts/Inter-ExtraBoldItalic.woff2 differ diff --git a/style/fonts/Inter-ExtraLight-BETA.woff b/style/fonts/Inter-ExtraLight-BETA.woff new file mode 100644 index 0000000..d38ffdc Binary files /dev/null and b/style/fonts/Inter-ExtraLight-BETA.woff differ diff --git a/style/fonts/Inter-ExtraLight-BETA.woff2 b/style/fonts/Inter-ExtraLight-BETA.woff2 new file mode 100644 index 0000000..621c7be Binary files /dev/null and b/style/fonts/Inter-ExtraLight-BETA.woff2 differ diff --git a/style/fonts/Inter-ExtraLightItalic-BETA.woff b/style/fonts/Inter-ExtraLightItalic-BETA.woff new file mode 100644 index 0000000..62f87a9 Binary files /dev/null and b/style/fonts/Inter-ExtraLightItalic-BETA.woff differ diff --git a/style/fonts/Inter-ExtraLightItalic-BETA.woff2 b/style/fonts/Inter-ExtraLightItalic-BETA.woff2 new file mode 100644 index 0000000..94e2e46 Binary files /dev/null and b/style/fonts/Inter-ExtraLightItalic-BETA.woff2 differ diff --git a/style/fonts/Inter-Italic.woff b/style/fonts/Inter-Italic.woff new file mode 100644 index 0000000..40cdb0a Binary files /dev/null and b/style/fonts/Inter-Italic.woff differ diff --git a/style/fonts/Inter-Italic.woff2 b/style/fonts/Inter-Italic.woff2 new file mode 100644 index 0000000..08a8347 Binary files /dev/null and b/style/fonts/Inter-Italic.woff2 differ diff --git a/style/fonts/Inter-Light-BETA.woff b/style/fonts/Inter-Light-BETA.woff new file mode 100644 index 0000000..f8dddec Binary files /dev/null and b/style/fonts/Inter-Light-BETA.woff differ diff --git a/style/fonts/Inter-Light-BETA.woff2 b/style/fonts/Inter-Light-BETA.woff2 new file mode 100644 index 0000000..2d7c6b6 Binary files /dev/null and b/style/fonts/Inter-Light-BETA.woff2 differ diff --git a/style/fonts/Inter-LightItalic-BETA.woff b/style/fonts/Inter-LightItalic-BETA.woff new file mode 100644 index 0000000..59dab11 Binary files /dev/null and b/style/fonts/Inter-LightItalic-BETA.woff differ diff --git a/style/fonts/Inter-LightItalic-BETA.woff2 b/style/fonts/Inter-LightItalic-BETA.woff2 new file mode 100644 index 0000000..da1fdd5 Binary files /dev/null and b/style/fonts/Inter-LightItalic-BETA.woff2 differ diff --git a/style/fonts/Inter-Medium.woff b/style/fonts/Inter-Medium.woff new file mode 100644 index 0000000..ef80852 Binary files /dev/null and b/style/fonts/Inter-Medium.woff differ diff --git a/style/fonts/Inter-Medium.woff2 b/style/fonts/Inter-Medium.woff2 new file mode 100644 index 0000000..8a1ecc1 Binary files /dev/null and b/style/fonts/Inter-Medium.woff2 differ diff --git a/style/fonts/Inter-MediumItalic.woff b/style/fonts/Inter-MediumItalic.woff new file mode 100644 index 0000000..968406f Binary files /dev/null and b/style/fonts/Inter-MediumItalic.woff differ diff --git a/style/fonts/Inter-MediumItalic.woff2 b/style/fonts/Inter-MediumItalic.woff2 new file mode 100644 index 0000000..4747ee4 Binary files /dev/null and b/style/fonts/Inter-MediumItalic.woff2 differ diff --git a/style/fonts/Inter-Regular.woff b/style/fonts/Inter-Regular.woff new file mode 100644 index 0000000..9aa5423 Binary files /dev/null and b/style/fonts/Inter-Regular.woff differ diff --git a/style/fonts/Inter-Regular.woff2 b/style/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000..e42fbfb Binary files /dev/null and b/style/fonts/Inter-Regular.woff2 differ diff --git a/style/fonts/Inter-SemiBold.woff b/style/fonts/Inter-SemiBold.woff new file mode 100644 index 0000000..6e2c0aa Binary files /dev/null and b/style/fonts/Inter-SemiBold.woff differ diff --git a/style/fonts/Inter-SemiBold.woff2 b/style/fonts/Inter-SemiBold.woff2 new file mode 100644 index 0000000..b11bfdf Binary files /dev/null and b/style/fonts/Inter-SemiBold.woff2 differ diff --git a/style/fonts/Inter-SemiBoldItalic.woff b/style/fonts/Inter-SemiBoldItalic.woff new file mode 100644 index 0000000..72a1b6c Binary files /dev/null and b/style/fonts/Inter-SemiBoldItalic.woff differ diff --git a/style/fonts/Inter-SemiBoldItalic.woff2 b/style/fonts/Inter-SemiBoldItalic.woff2 new file mode 100644 index 0000000..a54f9a8 Binary files /dev/null and b/style/fonts/Inter-SemiBoldItalic.woff2 differ diff --git a/style/fonts/Inter-Thin-BETA.woff b/style/fonts/Inter-Thin-BETA.woff new file mode 100644 index 0000000..f32df54 Binary files /dev/null and b/style/fonts/Inter-Thin-BETA.woff differ diff --git a/style/fonts/Inter-Thin-BETA.woff2 b/style/fonts/Inter-Thin-BETA.woff2 new file mode 100644 index 0000000..8452c52 Binary files /dev/null and b/style/fonts/Inter-Thin-BETA.woff2 differ diff --git a/style/fonts/Inter-ThinItalic-BETA.woff b/style/fonts/Inter-ThinItalic-BETA.woff new file mode 100644 index 0000000..8fd7793 Binary files /dev/null and b/style/fonts/Inter-ThinItalic-BETA.woff differ diff --git a/style/fonts/Inter-ThinItalic-BETA.woff2 b/style/fonts/Inter-ThinItalic-BETA.woff2 new file mode 100644 index 0000000..893dfc4 Binary files /dev/null and b/style/fonts/Inter-ThinItalic-BETA.woff2 differ diff --git a/style/fonts/SourceCodePro-Black.ttf b/style/fonts/SourceCodePro-Black.ttf new file mode 100644 index 0000000..e2422b2 Binary files /dev/null and b/style/fonts/SourceCodePro-Black.ttf differ diff --git a/style/fonts/SourceCodePro-Bold.ttf b/style/fonts/SourceCodePro-Bold.ttf new file mode 100644 index 0000000..2e545fe Binary files /dev/null and b/style/fonts/SourceCodePro-Bold.ttf differ diff --git a/style/fonts/SourceCodePro-ExtraLight.ttf b/style/fonts/SourceCodePro-ExtraLight.ttf new file mode 100644 index 0000000..8a80a64 Binary files /dev/null and b/style/fonts/SourceCodePro-ExtraLight.ttf differ diff --git a/style/fonts/SourceCodePro-Light.ttf b/style/fonts/SourceCodePro-Light.ttf new file mode 100644 index 0000000..8f95a47 Binary files /dev/null and b/style/fonts/SourceCodePro-Light.ttf differ diff --git a/style/fonts/SourceCodePro-Medium.ttf b/style/fonts/SourceCodePro-Medium.ttf new file mode 100644 index 0000000..b7471ba Binary files /dev/null and b/style/fonts/SourceCodePro-Medium.ttf differ diff --git a/style/fonts/SourceCodePro-Regular.ttf b/style/fonts/SourceCodePro-Regular.ttf new file mode 100644 index 0000000..fa1f90b Binary files /dev/null and b/style/fonts/SourceCodePro-Regular.ttf differ diff --git a/style/fonts/SourceCodePro-Semibold.ttf b/style/fonts/SourceCodePro-Semibold.ttf new file mode 100644 index 0000000..77ebab2 Binary files /dev/null and b/style/fonts/SourceCodePro-Semibold.ttf differ diff --git a/style/fonts/SourceSansPro-Black.ttf b/style/fonts/SourceSansPro-Black.ttf new file mode 100644 index 0000000..7ea0260 Binary files /dev/null and b/style/fonts/SourceSansPro-Black.ttf differ diff --git a/style/fonts/SourceSansPro-BlackItalic.ttf b/style/fonts/SourceSansPro-BlackItalic.ttf new file mode 100644 index 0000000..e1a7482 Binary files /dev/null and b/style/fonts/SourceSansPro-BlackItalic.ttf differ diff --git a/style/fonts/SourceSansPro-Bold.ttf b/style/fonts/SourceSansPro-Bold.ttf new file mode 100644 index 0000000..f698646 Binary files /dev/null and b/style/fonts/SourceSansPro-Bold.ttf differ diff --git a/style/fonts/SourceSansPro-BoldItalic.ttf b/style/fonts/SourceSansPro-BoldItalic.ttf new file mode 100644 index 0000000..5c00b64 Binary files /dev/null and b/style/fonts/SourceSansPro-BoldItalic.ttf differ diff --git a/style/fonts/SourceSansPro-ExtraLight.ttf b/style/fonts/SourceSansPro-ExtraLight.ttf new file mode 100644 index 0000000..f1da6b2 Binary files /dev/null and b/style/fonts/SourceSansPro-ExtraLight.ttf differ diff --git a/style/fonts/SourceSansPro-ExtraLightItalic.ttf b/style/fonts/SourceSansPro-ExtraLightItalic.ttf new file mode 100644 index 0000000..15f7344 Binary files /dev/null and b/style/fonts/SourceSansPro-ExtraLightItalic.ttf differ diff --git a/style/fonts/SourceSansPro-Italic.ttf b/style/fonts/SourceSansPro-Italic.ttf new file mode 100644 index 0000000..82e8762 Binary files /dev/null and b/style/fonts/SourceSansPro-Italic.ttf differ diff --git a/style/fonts/SourceSansPro-Light.ttf b/style/fonts/SourceSansPro-Light.ttf new file mode 100644 index 0000000..ea1104b Binary files /dev/null and b/style/fonts/SourceSansPro-Light.ttf differ diff --git a/style/fonts/SourceSansPro-LightItalic.ttf b/style/fonts/SourceSansPro-LightItalic.ttf new file mode 100644 index 0000000..b78f1b0 Binary files /dev/null and b/style/fonts/SourceSansPro-LightItalic.ttf differ diff --git a/style/fonts/SourceSansPro-Regular.ttf b/style/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000..278ad8a Binary files /dev/null and b/style/fonts/SourceSansPro-Regular.ttf differ diff --git a/style/fonts/SourceSansPro-SemiBold.ttf b/style/fonts/SourceSansPro-SemiBold.ttf new file mode 100644 index 0000000..ac3e0d1 Binary files /dev/null and b/style/fonts/SourceSansPro-SemiBold.ttf differ diff --git a/style/fonts/SourceSansPro-SemiBoldItalic.ttf b/style/fonts/SourceSansPro-SemiBoldItalic.ttf new file mode 100644 index 0000000..b0737bb Binary files /dev/null and b/style/fonts/SourceSansPro-SemiBoldItalic.ttf differ diff --git a/style/fonts/inter.css b/style/fonts/inter.css new file mode 100644 index 0000000..235dfea --- /dev/null +++ b/style/fonts/inter.css @@ -0,0 +1,184 @@ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + src: url("Inter-Thin.woff2") format("woff2"), + url("Inter-Thin.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 100; + src: url("Inter-ThinItalic.woff2") format("woff2"), + url("Inter-ThinItalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + src: url("Inter-ExtraLight.woff2") format("woff2"), + url("Inter-ExtraLight.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 200; + src: url("Inter-ExtraLightItalic.woff2") format("woff2"), + url("Inter-ExtraLightItalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + src: url("Inter-Light.woff2") format("woff2"), + url("Inter-Light.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 300; + src: url("Inter-LightItalic.woff2") format("woff2"), + url("Inter-LightItalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + src: url("Inter-Regular.woff2") format("woff2"), + url("Inter-Regular.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + src: url("Inter-Italic.woff2") format("woff2"), + url("Inter-Italic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + src: url("Inter-Medium.woff2") format("woff2"), + url("Inter-Medium.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 500; + src: url("Inter-MediumItalic.woff2") format("woff2"), + url("Inter-MediumItalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + src: url("Inter-SemiBold.woff2") format("woff2"), + url("Inter-SemiBold.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 600; + src: url("Inter-SemiBoldItalic.woff2") format("woff2"), + url("Inter-SemiBoldItalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + src: url("Inter-Bold.woff2") format("woff2"), + url("Inter-Bold.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 700; + src: url("Inter-BoldItalic.woff2") format("woff2"), + url("Inter-BoldItalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 800; + src: url("Inter-ExtraBold.woff2") format("woff2"), + url("Inter-ExtraBold.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 800; + src: url("Inter-ExtraBoldItalic.woff2") format("woff2"), + url("Inter-ExtraBoldItalic.woff") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + src: url("Inter-Black.woff2") format("woff2"), + url("Inter-Black.woff") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 900; + src: url("Inter-BlackItalic.woff2") format("woff2"), + url("Inter-BlackItalic.woff") format("woff"); +} + +/* ------------------------------------------------------- +Variable font. +Usage: + + html { font-family: 'Inter', sans-serif; } + @supports (font-variation-settings: normal) { + html { font-family: 'Inter var', sans-serif; } + } +*/ +@font-face { + font-family: 'Inter var'; + font-weight: 100 900; + font-style: normal; + font-named-instance: 'Regular'; + src: url("Inter-upright.var.woff2") format("woff2 supports variations(gvar)"), + url("Inter-upright.var.woff2") format("woff2-variations"), + url("Inter-upright.var.woff2") format("woff2"); +} +@font-face { + font-family: 'Inter var'; + font-weight: 100 900; + font-style: italic; + font-named-instance: 'Italic'; + src: url("Inter-italic.var.woff2") format("woff2 supports variations(gvar)"), + url("Inter-italic.var.woff2") format("woff2-variations"), + url("Inter-italic.var.woff2") format("woff2"); +} + + +/* -------------------------------------------------------------------------- +[EXPERIMENTAL] Multi-axis, single variable font. + +Slant axis is not yet widely supported (as of February 2019) and thus this +multi-axis single variable font is opt-in rather than the default. + +When using this, you will probably need to set font-variation-settings +explicitly, e.g. + + * { font-variation-settings: "slnt" 0deg } + .italic { font-variation-settings: "slnt" 10deg } + +*/ +@font-face { + font-family: 'Inter var experimental'; + font-weight: 100 900; + font-style: oblique 0deg 10deg; + src: url("Inter.var.woff2") format("woff2-variations"), + url("Inter.var.woff2") format("woff2"); +} diff --git a/style/fonts/source_code_pro.css b/style/fonts/source_code_pro.css new file mode 100644 index 0000000..da58487 --- /dev/null +++ b/style/fonts/source_code_pro.css @@ -0,0 +1,16 @@ +/* latin-ext */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Code Pro'), local('SourceCodePro-Regular'), url(SourceCodePro-Regular.ttf); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Code Pro'), local('SourceCodePro-Regular'), url(SourceCodePro-Regular.ttf); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} \ No newline at end of file diff --git a/style/fonts/source_sans_pro.css b/style/fonts/source_sans_pro.css new file mode 100644 index 0000000..3257b26 --- /dev/null +++ b/style/fonts/source_sans_pro.css @@ -0,0 +1,280 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 300; + src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url('SourceSansPro-LightItalic.ttf'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 300; + src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url('SourceSansPro-LightItalic.ttf'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 300; + src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url('SourceSansPro-LightItalic.ttf'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 300; + src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url('SourceSansPro-LightItalic.ttf'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 300; + src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url('SourceSansPro-LightItalic.ttf'); + unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 300; + src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url('SourceSansPro-LightItalic.ttf'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 300; + src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url('SourceSansPro-LightItalic.ttf'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 400; + src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url('SourceSansPro-Italic.ttf'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 400; + src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url('SourceSansPro-Italic.ttf'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 400; + src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url('SourceSansPro-Italic.ttf'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 400; + src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url('SourceSansPro-Italic.ttf'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 400; + src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url('SourceSansPro-Italic.ttf'); + unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 400; + src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url('SourceSansPro-Italic.ttf'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 400; + src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url('SourceSansPro-Italic.ttf'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 300; + src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url('SourceSansPro-Light.ttf'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 300; + src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url('SourceSansPro-Light.ttf'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 300; + src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url('SourceSansPro-Light.ttf'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 300; + src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url('SourceSansPro-Light.ttf'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 300; + src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url('SourceSansPro-Light.ttf'); + unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 300; + src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url('SourceSansPro-Light.ttf'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 300; + src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url('SourceSansPro-Light.ttf'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url('SourceSansPro-Regular.ttf'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url('SourceSansPro-Regular.ttf'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url('SourceSansPro-Regular.ttf'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url('SourceSansPro-Regular.ttf'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url('SourceSansPro-Regular.ttf'); + unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url('SourceSansPro-Regular.ttf'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url('SourceSansPro-Regular.ttf'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url('SourceSansPro-Bold.ttf'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url('SourceSansPro-Bold.ttf'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url('SourceSansPro-Bold.ttf'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url('SourceSansPro-Bold.ttf'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url('SourceSansPro-Bold.ttf'); + unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url('SourceSansPro-Bold.ttf'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url('SourceSansPro-Bold.ttf'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/style/style.css b/style/style.css new file mode 100644 index 0000000..3ee26dd --- /dev/null +++ b/style/style.css @@ -0,0 +1,549 @@ +:root { + --main-highlight-color: #EF940B; + --main-bg-color: white; + --main-text-color: black; + --main-link-color: #106BF4; + --shadow-color: #888; + --gray-light: #cacaca; + --gray-medium: #e0e0e0; + --gray-dark: #4c4c4c; + --red: red; + --green: green; + + --default-margin-tiny: 0.5em; + --default-margin-small: 0.75em; + --default-margin: 1em; + --default-margin-tiny: 0.15em; + --border-radius-small: 0.35em; + --border-radius-medium: 0.5em; + + --thin-border-width: 1px; + --medium-border-width: 2px; +} + +body { + color: var(--main-text-color); + background-color: var(--main-bg-color); +} + +a { + color: var(--main-link-color); +} + +.themelist, +.centertitle, +.horizontal-list { + text-align: center; +} + +.themelist > ul { + background: var(--gray-light); + border-radius: var(--border-radius-medium); + padding: var(--default-margin) calc(var(--default-margin) * 1.5); + list-style: none; + display: inline-block; +} + +.themelist li { + background-color: var(--main-bg-color); + margin: calc(var(--default-margin) / 2) 0; + padding: calc(var(--default-margin) / 2); + border-radius: var(--border-radius-small); +} + +.link { + text-decoration: none; + color: inherit; +} + +.link span { + position: relative; +} + +.link span:before, +.link span:after { + content: ""; + position: absolute; + bottom: -0.25em; + width: 0; + height: var(--medium-border-width); + margin: 0 0 0; + background-color: var(--main-highlight-color); +} + +.link span:before { + left: 50%; +} + +.link span:after { + right: 50%; +} + +.link:hover span:before, +.link:hover span:after { + width: 50%; +} + +.avatar-grid { + display: flex; + flex-wrap: wrap; + align-items: flex-start; +} + +img.avatar { + width: 100%; + border-radius: var(--border-radius-medium); +} + +.avatar { + box-shadow: 5px 5px 8px var(--shadow-color); +} + +.avatar:hover, +.themelist li:hover, +.link:hover { + transform: scale(1.1); +} + +.themelist li:hover { + background-color: var(--secondary-bg-color); +} + +.blur { + transition: all .2s ease-in-out 2s; + filter: blur(5px) grayscale(80%); +} + +.separator { + display: inline-block; + border-left: var(--thin-border-width) solid black; + width: var(--thin-border-width); + height: var(--default-margin-small); + box-sizing: border-box; +} + +.avatar-box { + width: 300px; + margin: var(--default-margin); +} + +.username { + margin-left: 5px; + color: var(--gray-dark); +} + +.post-box { + background: var(--gray-medium); + border-radius: 5px; +} + +.post-box { + box-shadow: 5px 5px 10px var(--shadow-color); +} + +.post-text-box { + margin: 5px 0; + padding: 3px; + box-shadow: 0 0 11px 2px var(--shadow-color); + border-radius: 5px; +} + +.user-box, +.post-meta-box { + border-radius: 5px; + background: var(--gray-light); + padding: 3px; + box-sizing: border-box; + width: 100%; + display: inline-block; +} + +.user-box { + padding-bottom: 0; +} + +.post-meta-box { + padding-top: 0; +} + +.loader, +.loader-small { + border: var(--medium-border-width) solid var(--main-bg-color); /* Light grey */ + border-top: var(--medium-border-width) solid var(--main-highlight-color); /* Blue */ + border-radius: 50%; + width: 20px; + height: 20px; + animation: spin 1s linear infinite; + margin: auto; +} + +.loader-small { + width: 0.5em; + height: 0.5em; + margin-left: 0.5em; + display: inline-block; +} + +.presence-indicator { + border: 5px solid var(--gray-dark); + border-radius: 50%; + display: inline-block; + margin-left: 0.2em; +} + +.linkbutton { + border-radius: var(--border-radius-small); + padding: calc(var(--default-margin) / 2); + text-decoration: none; + background: var(--gray-light); + display: inline-block; + margin-left: var(--default-margin); + color: inherit; +} + +button { + border: none; +} + +.avatar, +.linkbutton, +.themelist li, +.link span:before, +.link span:after, +.theme-chooser-list-item, +.theme-chooser-list-item span:before, +.theme-chooser-list-item span:after { + transition: all .2s ease-in-out; +} + +.theme-chooser-wrapper { + background: var(--gray-light); + border-radius: var(--border-radius-medium); + padding: 0 var(--default-margin); + display: inline-block; +} + +.theme-chooser-wrapper > span, .theme-chooser-wrapper > div { + padding: calc(var(--default-margin) / 2); + border-radius: var(--border-radius-small); + margin: calc(var(--default-margin) / 4); + color: var(--main-bg-color); + background: var(--gray-dark); + display: inline-block; +} + +.theme-chooser-list { + display: inline-block; + text-align: left; + list-style: none; + margin: calc(var(--default-margin) / 2); + padding: 0; +} + +.theme-chooser-list-item { + display: inline-block; + padding: calc(var(--default-margin) / 2); + background: var(--main-bg-color); + border-radius: var(--border-radius-small); + margin: calc(var(--default-margin) / 4); +} + +.theme-chooser-list > a { + text-decoration: none; + color: inherit; +} + +.theme-chooser-list-item:hover { + transform: scale(1.1); +} + +.theme-chooser-list-item span { + position: relative; +} + +.theme-chooser-list-item span:before, +.theme-chooser-list-item span:after { + content: ""; + position: absolute; + bottom: -0.25em; + width: 0; + height: var(--medium-border-width); + margin: 0 0 0; + background-color: var(--main-highlight-color); +} + +.theme-chooser-list-item span:before { + left: 50%; +} + +.theme-chooser-list-item span:after { + right: 50%; +} + +.theme-chooser-list-item:hover span:before, +.theme-chooser-list-item:hover span:after { + width: 50%; +} + +.theme-preview { + background: var(--main-bg-color); + color: var(--main-text-color); + display: inline-block; + border-radius: var(--border-radius-small); + text-align: center; + position: absolute; + width: 80%; + max-width: 400px; + display: none; + z-index: 1000; + bottom: 10%; + left: 50%; + transform: translate(-50%, calc(100% - 2em)); +} + +.theme-preview div { + padding: 0.5em; + border-radius: inherit; + margin: 0.5em; +} + +.theme-preview > div { + background: var(--gray-light); +} + +.theme-preview > div > div { + background: var(--main-bg-color); +} + +.theme-preview .hl-color { + border: 2px solid var(--main-highlight-color); +} + +.theme-preview .link-color { + border: 2px solid var(--main-link-color); +} + +.theme-preview .red { + border: 2px solid var(--red); +} + +.theme-preview .green { + border: 2px solid var(--green); +} + +.theme-preview .light-gray { + border: 2px solid var(--gray-light); +} + +.theme-preview .medium-gray { + border: 2px solid var(--gray-medium); +} + +.theme-preview .dark-gray { + border: 2px solid var(--gray-dark); +} + +.theme-preview .shadow { + box-shadow: 0px 0px 5px 5px var(--shadow-color); +} + +.theme-preview::before { + content: ""; + width: 0px; + height: 0px; + border: 0.8em solid transparent; + position: absolute; + left: 49%; + bottom: -30px; + border-top: 20px solid var(--main-bg-color); +} + +.dimmer { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: black; + opacity: 0.5; + z-index: 999; + pointer-events: none; +} + +.advanced-upload { + background-color: var(--gray-light); + /* + outline: 2px dashed var(--main-highlight-color); + outline-offset: -10px; + */ + border-radius: var(--border-radius-medium); +} + +.advanced-upload .upload-box { + display: inline; +} + +input:not([type=submit]):not([type=file]) { + padding: var(--border-radius-small); + background: var(--gray-light); + border-radius: var(--border-radius-medium); + border: none; + -moz-appearance: textfield; + -webkit-appearance: textfield; + appearance: textfield; + margin: 0; + color: inherit; +} + +input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +input:focus { + outline: outline: 5px solid var(--main-highlight-color); +} + +#avatarupload { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; +} + +.file-upload label { + width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + display: inline-block; + overflow: hidden; + text-align: center; +} + +.upload-form { + display: none; +} + +.file-upload { + padding: 100px 20px; + position: relative; +} + +.advanced-upload .outline-radius { + width: calc(100% - 20px); + height: calc(100% - 20px); + border: 2px dashed var(--main-highlight-color); + position: absolute; + left: 9px; + top: 9px; + border-radius: var(--border-radius-medium); +} + + +button.linkbutton { + padding: var(--border-radius-small); +} + +#avatarPreviewContainer { + position: absolute; + left: 0; + bottom: 0; +} + +#avatarPreview { + max-width: 100px; + max-height: 100px; +} + +#removeImage { + width: 20px; + height: 20px; + position: absolute; + top: 0; + right: 0; + display: none; +} + +#removeImage svg { + fill: var(--red); +} + +#uploadProgress { + display: none; + width: 100%; + /* + color: red; + background: red; + border: 1px solid red; + */ +} + +.notif { + position: absolute; + padding: var(--default-margin-small); + min-width: 250px; + z-index: 999999; + border-radius: var(--border-radius-small); + color: var(--shadow-color); + font-weight: 300; + white-space: nowrap; + user-select: none; + opacity: 0; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); + box-sizing: border-box; + transform: scaleX(0.5); + transition: all 0.4s ease-out; + color: var(--gray-dark); +} + +.notif[data-state="success"] { + background-color: var(--green); + color: var(--gray-light); +} + +.notif[data-state="alert"] { + background-color: var(--red); +} + +.notif[data-state="default"] { + background: var(--main-link-color); + +} +/* +progress::-webkit-progress-bar { + background: var(--gray-light); +} + +progress::-webkit-progress-value { + background: red; +} + +progress::-moz-progress-bar { + background: red; +} + +progress { + color: red; +} +*/ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@media (prefers-color-scheme: dark) { + :root { + --main-highlight-color: #EE6E1F; + --main-bg-color: #333; + --main-text-color: #d0d0d0; + --main-link-color: #1191e0; + --gray-light: #4c4c4c; + --gray-dark: #cacaca; + --gray-medium: #3b3b3b; + --shadow-color: #656565; + --red: #e4053e; + --green: #04c446; + } +} \ No newline at end of file diff --git a/style/style_new.css b/style/style_new.css new file mode 100644 index 0000000..99880a6 --- /dev/null +++ b/style/style_new.css @@ -0,0 +1,707 @@ +@import "fonts/source_sans_pro.css"; +@import "fonts/inter.css"; +:root { + --main-highlight-color: #EF940B; + --main-bg-color: white; + --main-text-color: black; + --main-link-color: #106BF4; + --shadow-color: #3333334d; + --gray-light: #cacaca; + --gray-medium: #e0e0e0; + --gray-dark: #4c4c4c; + --red: red; + --green: green; + + --default-margin-tiny: 0.5em; + --default-margin-small: 0.75em; + --default-margin: 1em; + --default-margin-tiny: 2px; + --border-radius-small: 5px; + --border-radius-medium: 8px; + --border-radius-big: 10px; + + --thin-border-width: 1px; + --medium-border-width: 2px; +} + +body { + color: var(--main-text-color); + background-color: var(--main-bg-color); + -webkit-font-smoothing:subpixel-antialiased; + font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, Ubuntu, Cantarell, "Segoe UI", Roboto, Oxygen-Sans, "San Francisco", "Helvetica Neue", Helvetica, "Lucida Grande", Tahoma, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; + font-size: 15px; +} + +a { + color: var(--main-link-color); +} + +.themelist, +.centertitle, +.horizontal-list { + text-align: center; +} + +.themelist > ul { + background: var(--gray-light); + border-radius: var(--border-radius-medium); + padding: var(--default-margin) calc(var(--default-margin) * 1.5); + list-style: none; + display: inline-block; +} + +.themelist li { + background-color: var(--main-bg-color); + margin: calc(var(--default-margin) / 2) 0; + padding: calc(var(--default-margin) / 2); + border-radius: var(--border-radius-small); +} + +.link { + text-decoration: none; + color: inherit; +} + +.link span { + position: relative; +} + +.link span:before, +.link span:after { + content: ""; + position: absolute; + bottom: -0.25em; + width: 0; + height: var(--medium-border-width); + margin: 0 0 0; + background-color: var(--main-highlight-color); +} + +.link span:before { + left: 50%; +} + +.link span:after { + right: 50%; +} + +.link:hover span:before, +.link:hover span:after { + width: 50%; +} + +.avatar-grid { + display: flex; + flex-wrap: wrap; + align-items: flex-start; +} + +img.avatar { + width: 100%; +} + +.avatar { + border-radius: var(--border-radius-big) var(--border-radius-big) 0 0; +} + +.themelist li:hover, +.link:hover { + transform: perspective(1px) scale(1.1) translateZ(0); +} +/* +.avatar-box:hover { + transform: perspective(1px) scale(1.1) translateZ(0); +} +*/ + + +.avatar-wrapper:hover { + transform: perspective(1px) scale(1.1) translateZ(0); + z-index: 1000 +} + +.avatar-wrapper:hover .avatar { + border-radius: 10px; + z-index: -1000; +} + +/* +.avatar-wrapper:hover ~ * { + transform: translateY(-100%); +} +*/ + +.blur { + transition: all .2s ease-in-out 2s; + filter: blur(5px) grayscale(80%); +} + +.separator { + display: inline-block; + border-left: var(--thin-border-width) solid black; + width: var(--thin-border-width); + height: var(--default-margin-small); + box-sizing: border-box; +} + +.avatar-box { + width: 300px; + margin: var(--default-margin); + border-radius: var(--border-radius-big); + filter:drop-shadow(0 0 var(--border-radius-big) var(--shadow-color)); + backface-visibility:hidden; + border: 1px solid var(--gray-light); +} + +.avatar-wrapper { + height: 300px; +} + +.username { + margin-left: 5px; + color: var(--gray-dark); +} + +.post-box { + background: var(--gray-medium); + border-top:1px solid var(--gray-light); +} + +.post-box > div { + padding: 5px; +} + +.post-text-box { + margin: 5px 0; +} + +.user-box, +.post-meta-box { + background: var(--gray-light); + box-sizing: border-box; + width: 100%; + display: inline-block; +} + +.post-box, .post-meta-box:last-child{ + border-radius:0 0 var(--border-radius-big) var(--border-radius-big); +} + +.post-meta-box, .username, [itemprop="tag"], .themelist li { + font-family: "Source Code Pro", "SF Mono", "Segoe UI Mono", "Roboto Mono", Menlo, Courier, monospace; +} +.post-meta-box { + font-size: smaller; +} + +[itemprop="tag"] { + text-decoration: underline var(--main-highlight-color); +} + +.loader, +.loader-small { + border: var(--medium-border-width) solid var(--main-bg-color); /* Light grey */ + border-top: var(--medium-border-width) solid var(--main-highlight-color); /* Blue */ + border-radius: 50%; + width: 20px; + height: 20px; + animation: spin 1s linear infinite; + margin: auto; +} + +.loader-small { + width: 0.5em; + height: 0.5em; + margin-left: 0.5em; + display: inline-block; +} + +.presence-indicator { + border: 5px solid var(--gray-dark); + border-radius: 50%; + display: inline-block; + margin-left: 0.2em; +} + +.linkbutton { + border-radius: var(--border-radius-small); + padding: calc(var(--default-margin) / 2); + text-decoration: none; + background: var(--gray-light); + display: inline-block; + margin-left: var(--default-margin); + color: inherit; +} +.linkbutton:not(:last-child) { + margin-bottom: var(--default-margin); +} + +button { + border: none; +} + +.avatar-box, +.avatar, +.avatar-wrapper, +.linkbutton, +.themelist li, +.link span:before, +.link span:after, +.theme-chooser-list-item, +.theme-chooser-list-item span:before, +.theme-chooser-list-item span:after { + transition: all .2s ease-in-out; +} + +.theme-chooser-wrapper { + background: var(--gray-light); + border-radius: var(--border-radius-medium); + padding: 0 var(--default-margin); + display: inline-block; +} + +.theme-chooser-wrapper > span, .theme-chooser-wrapper > div { + padding: calc(var(--default-margin) / 2); + border-radius: var(--border-radius-small); + margin: calc(var(--default-margin) / 4); + color: var(--main-bg-color); + background: var(--gray-dark); + display: inline-block; +} + +.theme-chooser-list { + display: inline-block; + text-align: left; + list-style: none; + margin: calc(var(--default-margin) / 2); + padding: 0; +} + +.theme-chooser-list-item { + display: inline-block; + padding: calc(var(--default-margin) / 2); + background: var(--main-bg-color); + border-radius: var(--border-radius-small); + margin: calc(var(--default-margin) / 4); +} + +.theme-chooser-list > a { + text-decoration: none; + color: inherit; +} + +.theme-chooser-list-item:hover { + transform: scale(1.1); +} + +.theme-chooser-list-item span { + position: relative; +} + +.theme-chooser-list-item span:before, +.theme-chooser-list-item span:after { + content: ""; + position: absolute; + bottom: -0.25em; + width: 0; + height: var(--medium-border-width); + margin: 0 0 0; + background-color: var(--main-highlight-color); +} + +.theme-chooser-list-item span:before { + left: 50%; +} + +.theme-chooser-list-item span:after { + right: 50%; +} + +.theme-chooser-list-item:hover span:before, +.theme-chooser-list-item:hover span:after { + width: 50%; +} + +.theme-preview { + background: var(--main-bg-color); + color: var(--main-text-color); + display: inline-block; + border-radius: var(--border-radius-small); + text-align: center; + position: absolute; + width: 80%; + max-width: 400px; + display: none; + z-index: 1000; + bottom: 10%; + left: 50%; + transform: translate(-50%, calc(100% - 2em)); +} + +.theme-preview div { + padding: 0.5em; + border-radius: inherit; + margin: 0.5em; +} + +.theme-preview > div { + background: var(--gray-light); +} + +.theme-preview > div > div { + background: var(--main-bg-color); +} + +.theme-preview .hl-color { + border: 2px solid var(--main-highlight-color); +} + +.theme-preview .link-color { + border: 2px solid var(--main-link-color); +} + +.theme-preview .red { + border: 2px solid var(--red); +} + +.theme-preview .green { + border: 2px solid var(--green); +} + +.theme-preview .light-gray { + border: 2px solid var(--gray-light); +} + +.theme-preview .medium-gray { + border: 2px solid var(--gray-medium); +} + +.theme-preview .dark-gray { + border: 2px solid var(--gray-dark); +} + +.theme-preview .shadow { + box-shadow: 0px 0px 5px 5px var(--shadow-color); +} + +.theme-preview::before { + content: ""; + width: 0px; + height: 0px; + border: 0.8em solid transparent; + position: absolute; + left: 49%; + bottom: -30px; + border-top: 20px solid var(--main-bg-color); +} + +.dimmer { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: black; + opacity: 0.5; + z-index: 999; + pointer-events: none; +} + +.advanced-upload { + background-color: var(--gray-light); + width: 300px; + border-radius: var(--border-radius-medium); +} + +.advanced-upload .upload-box { + display: inline; +} + +input:not([type=submit]):not([type=file]):not([type=checkbox]) { + padding: var(--border-radius-small); + background: var(--gray-light); + /*border-radius: var(--border-radius-medium);*/ + border-radius: var(--border-radius-small); + border: none; + -moz-appearance: textfield; + -webkit-appearance: textfield; + appearance: textfield; + margin: 0; + color: inherit; +} + +.checkbox-container { + display: block; + position: relative; + padding-left: 1.5em; + /*margin-bottom: 12px;*/ + cursor: pointer; + /*font-size: 22px;*/ + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.checkbox-container input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +/* Create a custom checkbox */ +.checkmark { + position: absolute; + top: 0; + left: 0; + height: 1em; + width: 1em; + background-color: var(--gray-dark); +} + +/* On mouse-over, add a grey background color */ +.checkbox-container:hover input ~ .checkmark { + background-color: var(--gray-light); +} + +/* When the checkbox is checked, add a blue background */ +.checkbox-container input:checked ~ .checkmark { + background-color: var(--gray-light); +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.checkmark:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +.checkbox-container input:checked ~ .checkmark:after { + display: block; +} + +/* Style the checkmark/indicator */ +.checkbox-container .checkmark:after { + left: 0.25em; + top: 0; + width: 0.3em; + height: 0.6em; + border: solid var(--main-highlight-color); + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +input:focus { + outline: 5px solid var(--main-highlight-color); +} + +textarea { + background: var(--gray-light); + border: none; + border-radius: var(--border-radius-small); + color: inherit; + font: inherit; +} + +textarea.posttext { + width: 100%; + /*display: none;*/ +} + +.file-upload-input { + /*width: 0.1px; + height: 0.1px;*/ + opacity: 0; + overflow: hidden; + position: absolute; + /*z-index: -1;*/ + width: 100%; + height: 100%; + z-index: 999; + transform: translateX(-20px) translateY(-100px); +} + +.file-upload label { + width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + display: inline-block; + overflow: hidden; + text-align: center; +} + +.upload-form { + display: none; +} + +.file-upload { + padding: 100px 20px; + position: relative; +} + +.advanced-upload .outline-radius { + width: calc(100% - 20px); + height: calc(100% - 20px); + border: 2px dashed var(--main-highlight-color); + position: absolute; + left: 9px; + top: 9px; + border-radius: var(--border-radius-medium); +} + +#uploadTempAvatar input[type=number] { + width: 30px; +} + + +button.linkbutton { + padding: var(--border-radius-small); +} + +.avatarPreviewContainer { + position: absolute; + left: 0; + bottom: 0; +} + +.avatarPreview { + max-width: 100px; + max-height: 100px; +} + +.removeImage { + width: 20px; + height: 20px; + position: absolute; + top: 0; + right: 0; + display: none; +} + +.removeImage svg { + fill: var(--red); +} + +.uploadProgress { + display: none; + width: 100%; + /* + color: red; + background: red; + border: 1px solid red; + */ +} + +.duration-row td { + vertical-align: baseline; +} + +input:invalid { + /*border: 1px solid var(--red) !important;*/ + box-shadow: inset 0px 0px 1px 1px var(--red); +} +/* +input:not(invalid) { + border: 1px solid transparent !important; +} +*/ +input { + box-sizing: border-box; +} + +.upload-form table { + max-width: 400px; +} + +.ui-element { + font-family: "Inter"; +} + +.hidden { + display: none; +} + +.notif { + position: absolute; + padding: var(--default-margin-small); + min-width: 250px; + z-index: 999999; + border-radius: var(--border-radius-small); + color: var(--shadow-color); + font-weight: 300; + white-space: nowrap; + user-select: none; + opacity: 0; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); + box-sizing: border-box; + transform: scaleX(0.5); + transition: all 0.4s ease-out; + color: var(--gray-dark); +} + +.notif[data-state="success"] { + background-color: var(--green); + color: var(--gray-light); +} + +.notif[data-state="alert"] { + background-color: var(--red); +} + +.notif[data-state="default"] { + background: var(--main-link-color); + +} +.notif[data-state="warn"] { + background: rgb(255, 166, 32); + color: var(--main-text-color); +} +/* +progress::-webkit-progress-bar { + background: var(--gray-light); +} + +progress::-webkit-progress-value { + background: red; +} + +progress::-moz-progress-bar { + background: red; +} + +progress { + color: red; +} +*/ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@media (prefers-color-scheme: dark) { + :root { + --main-highlight-color: #EF940B; + --main-bg-color: white; + --main-text-color: black; + --main-link-color: #106BF4; + --gray-light: #cacaca; + --gray-dark: #4c4c4c; + --gray-medium: #e0e0e0; + --shadow-color: #888; + } +} + +#imgTooLarge { + display: none; + color: var(--red); +}