initial commit

This commit is contained in:
Max Nuding 2025-05-24 13:23:02 +02:00
commit 1424ec981c
Signed by: phlaym
SSH Key Fingerprint: SHA256:mionmF+5trOUI1AxqzAU1ZK3tv6IiDcdKGXcMWwa1nQ
83 changed files with 4718 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
logs/
pics/*
config.php
.DS_Store
**/.DS_Store
vendor

6
.nova/Configuration.json Normal file
View File

@ -0,0 +1,6 @@
{
"com.thorlaksson.phpcs.formatOnSave" : true,
"com.thorlaksson.phpcs.runOnChange" : "onSave",
"com.thorlaksson.phpcs.standard" : "PSR12",
"prettier.format-on-save" : "Disable"
}

37
auth_callback.php Normal file
View File

@ -0,0 +1,37 @@
<?php
session_start();
require __DIR__ . '/vendor/autoload.php';
if (empty($_GET['code'])) { //Auth token received by pnut
// TODO: Handle auth errors
die("Error authenticating, did not receive a code");
}
if (!empty($_GET['type'])) {
} else {
$config = include __DIR__ . '/config.php';
$internal_pics_dir = __DIR__ . $config['pics_dir'];
$app = new Phlaym\Roastmonday\Roastmonday($config, $internal_pics_dir);
}
$success = $app->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('<script>console.debug("Authentication successful!");</script>');
echo '<meta http-equiv="refresh" content="0;url=' . $url . '">';
echo '<script>window.location.replace("'
. $url
. '");</script>Redirecting to <a href="'
. $url
. '">'
. $url
. '</a>';
} else {
die('Succesfully authorized!');
}
} else {
die("Error authenticating");
}

34
composer.json Normal file
View File

@ -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"
}
]
}

215
composer.lock generated Normal file
View File

@ -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"
}

24
fetch_mondays.php Normal file
View File

@ -0,0 +1,24 @@
<?php
require __DIR__ . '/vendor/autoload.php';
echo 'Scanning for new themes' . PHP_EOL;
$config = include __DIR__ . '/config.php';
$internal_pics_dir = __DIR__ . $config['pics_dir'];
echo 'Pics dir: ' . realpath($internal_pics_dir) . '<br>' . 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<br>\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'] . '<br>\n';
try {
$app->savePicturesForTheme($monday);
} catch (\Exception $e) {
echo 'Error saving pictures for ' . $monday['tag'] . ':' . $e->getMessage();
}
}

58
get_posttext.php Normal file
View File

@ -0,0 +1,58 @@
<?php
require __DIR__ . '/vendor/autoload.php';
if (empty($_GET['id'])) {
echo(json_encode([]));
die();
}
if (empty($_GET['id'])) {
$resp = ['success' => 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);

43
get_userdetails.php Normal file
View File

@ -0,0 +1,43 @@
<?php
require __DIR__ . '/vendor/autoload.php';
if (empty($_GET['id'])) {
echo(json_encode([]));
die();
}
$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_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);

1
icons/close.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="0 0 8.4666665 10.583333625" version="1.1" x="0px" y="0px"><g transform="translate(0,-288.53333)"><path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="M 2.9960938 2 A 1.0001001 1.0001001 0 0 0 2.3027344 3.7207031 L 14.582031 16 L 2.3027344 28.279297 A 1.0021986 1.0021986 0 0 0 3.7207031 29.697266 L 16 17.417969 L 28.279297 29.697266 A 1.0021986 1.0021986 0 1 0 29.697266 28.279297 L 17.417969 16 L 29.697266 3.7207031 A 1.0001001 1.0001001 0 0 0 28.974609 2 A 1.0001001 1.0001001 0 0 0 28.279297 2.3027344 L 16 14.582031 L 3.7207031 2.3027344 A 1.0001001 1.0001001 0 0 0 2.9960938 2 z " transform="matrix(0.26458333,0,0,0.26458333,0,288.53333)"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

71
index.php Normal file
View File

@ -0,0 +1,71 @@
<?php
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<title>Roastmonday</title>
<link rel="stylesheet" href="style/style_new.css">
<link rel="prefetch" href="scripts/pictureViewer.js" as="script">
<link rel="prefetch" href="scripts/toast.js" as="script">
<link rel="reload" href="scripts/roastmonday_common.js" as="script">
<script src="scripts/roastmonday_common.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1 class="centertitle">Roastmonday</h1>
<div class="themelist">
<ul>
<?php
// Load composer
require __DIR__ . '/vendor/autoload.php';
$config = include __DIR__ . '/config.php';
$internal_pics_dir = realpath(__DIR__ . $config['pics_dir']);
$app = new Phlaym\Roastmonday\Roastmonday($config, $internal_pics_dir);
$tm = [];
try {
$tm = $app->getThemeMondays();
} catch (Phlaym\Roastmonday\DB\OperationTimedOutException $e) {
echo 'Error connecting to the database, the connection has timed out. Please try again later';
}
foreach ($tm as $monday) {
echo '<a href="pictures.php?id='
. $monday['idx']
. '" class="link"><li><span>#'
. $monday['tag']
. ' on '
. $monday['date']->format('F jS, Y')
. '</span></li></a>';
}
?>
</ul>
</div>
<div class="horizontal-list">
<div id="themeChooser" class="theme-chooser-wrapper ui-element">
<div>Choose a theme</div>
</div>
</div>
<div class="theme-preview">
<div>
<div class="hl-color">Highlight</div>
<div class="link-color">Links</div>
</div>
<div>
<div class="red">Red</div>
<div class="green">Green</div>
</div>
<div>
<div class="light-gray">Light Gray</div>
<div class="medium-gray">Medium Gray</div>
<div class="dark-gray">Dark Gray</div>
</div>
<div>
<div class="shadow">Shadow</div>
</div>
</div>
</body>
</html>

213
pictures.php Normal file
View File

@ -0,0 +1,213 @@
<?php
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<title>Roastmonday</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style/style_new.css">
<script src="scripts/roastmonday_common.js"></script>
<script src="scripts/pictureViewer.js"></script>
<script src="scripts/toast.js"></script>
</head>
<body>
<?php
require __DIR__ . '/vendor/autoload.php';
if (empty($_GET['id'])) {
die('No theme selected. <a href="index.php">Overview</a>');
}
$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 '<h1>' . $theme['tag'] . '</h1>';
?>
<a href="index.php" class="link linkbutton ui-element"><span>Overview</span></a>
<a href="#" class="link linkbutton ui-element" id="showUploadForm"><span>Manually add an avatar</span></a>
<a href="#" class="link linkbutton ui-element" id="addTempAvatar">
<span>Change your avatar</span>
<div id="loadingLoginStatus" class="hidden">
Checking login status...
<div id="spinnerLoadingLoginStatus"></div>
</div>
</a>
<div>
<form id="manuallyAddAvatarForm"
class="upload-form"
onsubmit="handleManualAvatarUpload(this); return false;"
method="POST"
action="upload_avatar.php">
<table>
<tr>
<td>Post ID:</td>
<td><input name="postID" type="text" min="0" required/></td>
</tr>
<tr>
<td>Avatar:*</td>
<td>
<div class="file-upload">
<input type="file"
name="avatar"
accept="image/*"
id="avatarupload"
class="file-upload-input"/>
<label for="avatarupload">
<strong>Choose a file</strong>
<span class="upload-box"> or drag it here</span>.
</label>
<div id="avatarPreviewContainer" class="avatarPreviewContainer">
<img id="avatarPreview" class="avatarPreview"/>
<div id="removeImage" class="removeImage">
<!-- <img src="icons/close.svg" title="Close by bayu wibowo from the Noun Project"> !-->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 8.4666665 10.583333625"
version="1.1"
x="0px"
y="0px">
<g transform="translate(0,-288.53333)">
<path d="M 2.9960938 2 A 1.0001001 1.0001001 0 0 0 2.3027344 3.7207031 L 14.582031 16 L 2.3027344 28.279297 A 1.0021986 1.0021986 0 0 0 3.7207031 29.697266 L 16 17.417969 L 28.279297 29.697266 A 1.0021986 1.0021986 0 1 0 29.697266 28.279297 L 17.417969 16 L 29.697266 3.7207031 A 1.0001001 1.0001001 0 0 0 28.974609 2 A 1.0001001 1.0001001 0 0 0 28.279297 2.3027344 L 16 14.582031 L 3.7207031 2.3027344 A 1.0001001 1.0001001 0 0 0 2.9960938 2 z " transform="matrix(0.26458333,0,0,0.26458333,0,288.53333)"/>
</g>
</svg>
</div>
</div>
<div class="outline-radius"></div>
</div>
<progress id="uploadProgress" class="uploadProgress"></progress>
</td>
</tr>
<tr>
<td colspan="2">
* Optional. If left empty, the current avatar of the post creator will be used
</td>
</tr>
<tr>
<td colspan="2"></td>
</tr>
</table>
<button type="submit" name="submit" class="link linkbutton ui-element">Submit</button>
</form>
<form id="uploadTempAvatar"
class="upload-form"
onsubmit="handleTempAvatarUpload(this); return false;"
method="POST"
action="set_temp_avatar.php">
<table>
<tr>
<td>Avatar:</td>
<td>
<div class="file-upload">
<input type="hidden" name="MAX_FILE_SIZE" value="2097152" />
<input type="file"
name="avatar"
accept="image/*"
id="avatarupload2"
class="file-upload-input"/>
<label for="avatarupload2">
<strong>Choose a file</strong>
<span class="upload-box"> or drag it here</span>.
</label>
<div id="avatarPreviewContainer2" class="avatarPreviewContainer">
<img id="avatarPreview2" class="avatarPreview"/>
<div id="removeImage2" class="removeImage">
<!-- <img src="icons/close.svg" title="Close by bayu wibowo from the Noun Project"> !-->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="0 0 8.4666665 10.583333625" version="1.1" x="0px" y="0px"><g transform="translate(0,-288.53333)"><path d="M 2.9960938 2 A 1.0001001 1.0001001 0 0 0 2.3027344 3.7207031 L 14.582031 16 L 2.3027344 28.279297 A 1.0021986 1.0021986 0 0 0 3.7207031 29.697266 L 16 17.417969 L 28.279297 29.697266 A 1.0021986 1.0021986 0 1 0 29.697266 28.279297 L 17.417969 16 L 29.697266 3.7207031 A 1.0001001 1.0001001 0 0 0 28.974609 2 A 1.0001001 1.0001001 0 0 0 28.279297 2.3027344 L 16 14.582031 L 3.7207031 2.3027344 A 1.0001001 1.0001001 0 0 0 2.9960938 2 z " transform="matrix(0.26458333,0,0,0.26458333,0,288.53333)"/></g></svg>
</div>
</div>
<div class="outline-radius"></div>
</div>
<progress id="uploadProgress" class="uploadProgress"></progress>
</td>
</tr>
<tr>
<td>Post</td>
<td>
<label class="checkbox-container">Post about your new avatar
<input id="cbShouldPostAvatar"
type="checkbox"
checked="checked"
name="shouldPostAvatar"
value="true" />
<span class="checkmark"></span>
</label>
<textarea class="posttext"
name="posttext">
<?= '#' . $theme['tag'] . ' #thememonday';?>
</textarea>
<br />
If you post about it, include #thememonday and <?= '#' . $theme['tag']; ?>,
and either keyword "avatar" or "picture" your new avatar will show up in this gallery!
</td>
</tr>
<tr class="duration-row">
<td>Duration</td>
<td>
<input type="number" name="avatarDuration" value="3" /> days.
Your avatar will reset to the current one on
<span><?= (new \DateTime("+3 days"))->format('Y-m-d'); ?></span>
</td>
</tr>
<tr>
<td colspan="2">
<input type="hidden" name="theme" value="<?=$id?>" />
<button type="submit"
name="submit"
class="link linkbutton ui-element"
value="submit">
Submit
</button>
</td>
</tr>
</table>
</form>
<div id="authorizeDiv" class="hidden">
This feature requires you to log in with pnut.
<a href="">Authorized with pnut</a>
</div>
</div>
<?php
$pics = $app->getPicturesForTheme($id);
if (empty($pics)) {
die('No pictures for theme selected. <a href="index.php">Overview</a>');
}
?><div class="avatar-grid">
<?php
foreach ($pics as $pic) : ?>
<div class="avatar-box">
<div class="avatar-wrapper"><img class="avatar" src="<?= $pic['file'] ?>" /></div>
<div class="post-box">
<div class="user-box">
<?php
if (empty($pic['user_realname'])) {
echo '<span>@' . $pic['username'] . '<div class="loader-small"></div></span>';
} else {
echo '<span>'
. $pic['user_realname']
. '</span><span class="username">@'
. $pic['username']
. '</span>';
}
?>
</div>
<div class="post-text-box"><div class="loader"></div></div>
<div class="post-meta-box">
<span><a href="https://posts.pnut.io/<?=$pic['post_id']?>">#<?=$pic['post_id']?></a></span>
<div class="separator"></div>
<span class="postdate" ts="<?=$pic['postdate']->getTimestamp()?>">
<?=$pic['postdate']->format('F jS, H:i:s e')?>
</span>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</body>
</html>

8
reset_avatars.php Normal file
View File

@ -0,0 +1,8 @@
<?php
require __DIR__ . '/vendor/autoload.php';
$config = include __DIR__ . '/config.php';
$internal_pics_dir = realpath(__DIR__ . $config['pics_dir']);
$app = new Phlaym\Roastmonday\Roastmonday($config, $internal_pics_dir);
$app->resetOriginalAvatars();

739
scripts/pictureViewer.js Normal file
View File

@ -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 = '<span>' + info.name + '</span><span class="username">' + id + '</span>';
}
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 = '<span>' + info.realname + '</span><span class="username">' + info.user + '</span>';
} else {
userDetails.innerHTML = '<span>' + info.user + '</span>';
}
/*
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 = '<span>' + info.realname + '</span><span class="username">' + info.user + '</span>';
}*/
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) {
//<span style="display: block;">Image too large</span>
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;
}
}

View File

@ -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();
});
}

157
scripts/toast.js Normal file
View File

@ -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;

36
set_temp_avatar.php Normal file
View File

@ -0,0 +1,36 @@
<?php
session_start();
require __DIR__ . '/vendor/autoload.php';
$config = include __DIR__ . '/config.php';
$internal_pics_dir = realpath(__DIR__ . $config['pics_dir']);
$app = new Phlaym\Roastmonday\Roastmonday($config, $internal_pics_dir);
if (!$app->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));

74
src/DB/BaseDB.php Normal file
View File

@ -0,0 +1,74 @@
<?php
namespace Phlaym\Roastmonday\DB;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
class BaseDB
{
protected LoggerInterface $logger;
protected static int $ERR_DUPLICATE_ENTRY = 1062;
protected static int $ERR_CONNECTION_LIMIT = 1062;
protected static int $ERR_TIMEOUT = 2002;
//TODO: Implement a better system
public static int $SUCCESS = 0;
public static int $SKIPPED_DUPLICATE = 1;
public static int $TRIED_INSERT_DUPLICATE = 2;
protected $conn = null;
public function __construct(
string $servername,
string $db,
string $username,
string $password,
?LoggerInterface $logger
) {
if ($logger === null) {
$this->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;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Phlaym\Roastmonday\DB;
class ConnectionLimitReached extends DBException
{
}

284
src/DB/DB.php Normal file
View File

@ -0,0 +1,284 @@
<?php
namespace Phlaym\Roastmonday\DB;
use Psr\Log\LoggerInterface;
class DB extends BaseDB
{
protected static $db_themes = 'thememonday';
protected static $db_avatars = 'themeavatar';
protected static $db_temp_avatars = 'tempavatar';
protected $except_on_duplicate_entries = true;
public function __construct(
string $servername,
string $db,
string $username,
string $password,
$except_on_duplicate_entries = true,
?LoggerInterface $logger = null
) {
parent::__construct($servername, $db, $username, $password, $logger);
$this->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();
}
}

7
src/DB/DBException.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace Phlaym\Roastmonday\DB;
class DBException extends \Exception
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Phlaym\Roastmonday\DB;
class DuplicateEntryException extends DBException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Phlaym\Roastmonday\DB;
class OperationTimedOutException extends DBException
{
}

726
src/Roastmonday.php Normal file
View File

@ -0,0 +1,726 @@
<?php
namespace Phlaym\Roastmonday;
use APnutI\APnutI;
use Phlaym\Roastmonday\DB\DB;
use APnutI\Exceptions\NotFoundException;
class Roastmonday extends APnutI
{
protected static $new_avatar_keywords = ['avatar', 'image', 'picture'];
protected DB $db;
protected string $pics_root;
protected string $temp_pics_root;
public function __construct($config, $pics_root = "", $app_name = 'Roastmonday')
{
parent::__construct(
$config['client_secret'],
$config['client_id'],
$config['scope'],
$app_name,
$config['redirect_url'],
__DIR__ . '/../logs/roastmonday.log',
'DEBUG'
);
$this->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,'
. '<br />but your posttext is empty.<br />'
. '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:<br />'
. $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,'
. '<br />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!');
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
style/fonts/Inter-Bold.woff Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

184
style/fonts/inter.css Normal file
View File

@ -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");
}

View File

@ -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;
}

View File

@ -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;
}

549
style/style.css Normal file
View File

@ -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;
}
}

707
style/style_new.css Normal file
View File

@ -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);
}