initial commit
This commit is contained in:
commit
1424ec981c
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
logs/
|
||||
pics/*
|
||||
config.php
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
vendor
|
6
.nova/Configuration.json
Normal file
6
.nova/Configuration.json
Normal 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
37
auth_callback.php
Normal 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
34
composer.json
Normal 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
215
composer.lock
generated
Normal 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
24
fetch_mondays.php
Normal 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
58
get_posttext.php
Normal 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
43
get_userdetails.php
Normal 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
1
icons/close.svg
Normal 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
71
index.php
Normal 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
213
pictures.php
Normal 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
8
reset_avatars.php
Normal 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
739
scripts/pictureViewer.js
Normal 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;
|
||||
}
|
||||
}
|
222
scripts/roastmonday_common.js
Normal file
222
scripts/roastmonday_common.js
Normal 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
157
scripts/toast.js
Normal 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
36
set_temp_avatar.php
Normal 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
74
src/DB/BaseDB.php
Normal 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;
|
||||
}
|
||||
}
|
7
src/DB/ConnectionLimitReached.php
Normal file
7
src/DB/ConnectionLimitReached.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Phlaym\Roastmonday\DB;
|
||||
|
||||
class ConnectionLimitReached extends DBException
|
||||
{
|
||||
}
|
284
src/DB/DB.php
Normal file
284
src/DB/DB.php
Normal 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
7
src/DB/DBException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Phlaym\Roastmonday\DB;
|
||||
|
||||
class DBException extends \Exception
|
||||
{
|
||||
}
|
7
src/DB/DuplicateEntryException.php
Normal file
7
src/DB/DuplicateEntryException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Phlaym\Roastmonday\DB;
|
||||
|
||||
class DuplicateEntryException extends DBException
|
||||
{
|
||||
}
|
7
src/DB/OperationTimedOutException.php
Normal file
7
src/DB/OperationTimedOutException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Phlaym\Roastmonday\DB;
|
||||
|
||||
class OperationTimedOutException extends DBException
|
||||
{
|
||||
}
|
726
src/Roastmonday.php
Normal file
726
src/Roastmonday.php
Normal 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!');
|
||||
}
|
||||
}
|
||||
}
|
BIN
style/fonts/Inter-Black.woff
Normal file
BIN
style/fonts/Inter-Black.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Black.woff2
Normal file
BIN
style/fonts/Inter-Black.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-BlackItalic.woff
Normal file
BIN
style/fonts/Inter-BlackItalic.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-BlackItalic.woff2
Normal file
BIN
style/fonts/Inter-BlackItalic.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Bold.woff
Normal file
BIN
style/fonts/Inter-Bold.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Bold.woff2
Normal file
BIN
style/fonts/Inter-Bold.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-BoldItalic.woff
Normal file
BIN
style/fonts/Inter-BoldItalic.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-BoldItalic.woff2
Normal file
BIN
style/fonts/Inter-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ExtraBold.woff
Normal file
BIN
style/fonts/Inter-ExtraBold.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ExtraBold.woff2
Normal file
BIN
style/fonts/Inter-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ExtraBoldItalic.woff
Normal file
BIN
style/fonts/Inter-ExtraBoldItalic.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ExtraBoldItalic.woff2
Normal file
BIN
style/fonts/Inter-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ExtraLight-BETA.woff
Normal file
BIN
style/fonts/Inter-ExtraLight-BETA.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ExtraLight-BETA.woff2
Normal file
BIN
style/fonts/Inter-ExtraLight-BETA.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ExtraLightItalic-BETA.woff
Normal file
BIN
style/fonts/Inter-ExtraLightItalic-BETA.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ExtraLightItalic-BETA.woff2
Normal file
BIN
style/fonts/Inter-ExtraLightItalic-BETA.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Italic.woff
Normal file
BIN
style/fonts/Inter-Italic.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Italic.woff2
Normal file
BIN
style/fonts/Inter-Italic.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Light-BETA.woff
Normal file
BIN
style/fonts/Inter-Light-BETA.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Light-BETA.woff2
Normal file
BIN
style/fonts/Inter-Light-BETA.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-LightItalic-BETA.woff
Normal file
BIN
style/fonts/Inter-LightItalic-BETA.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-LightItalic-BETA.woff2
Normal file
BIN
style/fonts/Inter-LightItalic-BETA.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Medium.woff
Normal file
BIN
style/fonts/Inter-Medium.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Medium.woff2
Normal file
BIN
style/fonts/Inter-Medium.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-MediumItalic.woff
Normal file
BIN
style/fonts/Inter-MediumItalic.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-MediumItalic.woff2
Normal file
BIN
style/fonts/Inter-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Regular.woff
Normal file
BIN
style/fonts/Inter-Regular.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Regular.woff2
Normal file
BIN
style/fonts/Inter-Regular.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-SemiBold.woff
Normal file
BIN
style/fonts/Inter-SemiBold.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-SemiBold.woff2
Normal file
BIN
style/fonts/Inter-SemiBold.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-SemiBoldItalic.woff
Normal file
BIN
style/fonts/Inter-SemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-SemiBoldItalic.woff2
Normal file
BIN
style/fonts/Inter-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Thin-BETA.woff
Normal file
BIN
style/fonts/Inter-Thin-BETA.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-Thin-BETA.woff2
Normal file
BIN
style/fonts/Inter-Thin-BETA.woff2
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ThinItalic-BETA.woff
Normal file
BIN
style/fonts/Inter-ThinItalic-BETA.woff
Normal file
Binary file not shown.
BIN
style/fonts/Inter-ThinItalic-BETA.woff2
Normal file
BIN
style/fonts/Inter-ThinItalic-BETA.woff2
Normal file
Binary file not shown.
BIN
style/fonts/SourceCodePro-Black.ttf
Normal file
BIN
style/fonts/SourceCodePro-Black.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceCodePro-Bold.ttf
Normal file
BIN
style/fonts/SourceCodePro-Bold.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceCodePro-ExtraLight.ttf
Normal file
BIN
style/fonts/SourceCodePro-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceCodePro-Light.ttf
Normal file
BIN
style/fonts/SourceCodePro-Light.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceCodePro-Medium.ttf
Normal file
BIN
style/fonts/SourceCodePro-Medium.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceCodePro-Regular.ttf
Normal file
BIN
style/fonts/SourceCodePro-Regular.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceCodePro-Semibold.ttf
Normal file
BIN
style/fonts/SourceCodePro-Semibold.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-Black.ttf
Normal file
BIN
style/fonts/SourceSansPro-Black.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-BlackItalic.ttf
Normal file
BIN
style/fonts/SourceSansPro-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-Bold.ttf
Normal file
BIN
style/fonts/SourceSansPro-Bold.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-BoldItalic.ttf
Normal file
BIN
style/fonts/SourceSansPro-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-ExtraLight.ttf
Normal file
BIN
style/fonts/SourceSansPro-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-ExtraLightItalic.ttf
Normal file
BIN
style/fonts/SourceSansPro-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-Italic.ttf
Normal file
BIN
style/fonts/SourceSansPro-Italic.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-Light.ttf
Normal file
BIN
style/fonts/SourceSansPro-Light.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-LightItalic.ttf
Normal file
BIN
style/fonts/SourceSansPro-LightItalic.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-Regular.ttf
Normal file
BIN
style/fonts/SourceSansPro-Regular.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-SemiBold.ttf
Normal file
BIN
style/fonts/SourceSansPro-SemiBold.ttf
Normal file
Binary file not shown.
BIN
style/fonts/SourceSansPro-SemiBoldItalic.ttf
Normal file
BIN
style/fonts/SourceSansPro-SemiBoldItalic.ttf
Normal file
Binary file not shown.
184
style/fonts/inter.css
Normal file
184
style/fonts/inter.css
Normal 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");
|
||||
}
|
16
style/fonts/source_code_pro.css
Normal file
16
style/fonts/source_code_pro.css
Normal 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;
|
||||
}
|
280
style/fonts/source_sans_pro.css
Normal file
280
style/fonts/source_sans_pro.css
Normal 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
549
style/style.css
Normal 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
707
style/style_new.css
Normal 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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user