Procházet zdrojové kódy

theme switcher
mysql support
used space indicator

Sergio Brighenti před 6 roky
rodič
revize
d6a9fcf600

+ 4 - 1
CHANGELOG.md

@@ -1,7 +1,10 @@
-## v2.0 [WIP]
+## v2.0
 + Migrated from Flight to Slim 3 framework.
 + Migrated from Flight to Slim 3 framework.
 + Added install wizard (using the CLI is no longer required).
 + Added install wizard (using the CLI is no longer required).
 + Allow discord bot to display the preview.
 + Allow discord bot to display the preview.
++ Theme switcher on the web UI.
++ Added used space indicator per user.
++ MySQL support.
 + Improvements under the hood.
 + Improvements under the hood.
 
 
 ## v1.3
 ## v1.3

+ 23 - 1
app/Controllers/Controller.php

@@ -4,6 +4,7 @@ namespace App\Controllers;
 
 
 
 
 use League\Flysystem\Adapter\Local;
 use League\Flysystem\Adapter\Local;
+use League\Flysystem\FileNotFoundException;
 use League\Flysystem\Filesystem;
 use League\Flysystem\Filesystem;
 use Slim\Container;
 use Slim\Container;
 use Slim\Http\Request;
 use Slim\Http\Request;
@@ -59,7 +60,7 @@ abstract class Controller
 	/**
 	/**
 	 * @param $path
 	 * @param $path
 	 */
 	 */
-	public function removeDirectory($path)
+	protected function removeDirectory($path)
 	{
 	{
 		$files = glob($path . '/*');
 		$files = glob($path . '/*');
 		foreach ($files as $file) {
 		foreach ($files as $file) {
@@ -68,4 +69,25 @@ abstract class Controller
 		rmdir($path);
 		rmdir($path);
 		return;
 		return;
 	}
 	}
+
+	/**
+	 * @param $id
+	 * @return int
+	 */
+	protected function getUsedSpaceByUser($id): int
+	{
+		$medias = $this->database->query('SELECT `uploads`.`storage_path` FROM `uploads` WHERE `user_id` = ?', $id)->fetchAll();
+
+		$totalSize = 0;
+
+		$filesystem = $this->getStorage();
+		foreach ($medias as $media) {
+			try {
+				$totalSize += $filesystem->getSize($media->storage_path);
+			} catch (FileNotFoundException $e) {
+			}
+		}
+
+		return $totalSize;
+	}
 }
 }

+ 28 - 1
app/Controllers/DashboardController.php

@@ -23,6 +23,7 @@ class DashboardController extends Controller
 	{
 	{
 
 
 		if ($request->getParam('afterInstall') !== null && is_dir('install')) {
 		if ($request->getParam('afterInstall') !== null && is_dir('install')) {
+			Session::alert('Installation completed successfully!', 'success');
 			$this->removeDirectory('install');
 			$this->removeDirectory('install');
 		}
 		}
 
 
@@ -101,7 +102,33 @@ class DashboardController extends Controller
 			'mediasCount' => $mediasCount,
 			'mediasCount' => $mediasCount,
 			'orphanFilesCount' => $orphanFilesCount,
 			'orphanFilesCount' => $orphanFilesCount,
 			'totalSize' => $this->humanFilesize($totalSize),
 			'totalSize' => $this->humanFilesize($totalSize),
-			'max_filesize' => ini_get('post_max_size') . '/' . ini_get('upload_max_filesize'),
+			'post_max_size' => ini_get('post_max_size'),
+			'upload_max_filesize' => ini_get('upload_max_filesize'),
 		]);
 		]);
 	}
 	}
+
+	/**
+	 * @param Request $request
+	 * @param Response $response
+	 * @return Response
+	 */
+	public function getThemes(Request $request, Response $response): Response
+	{
+		$apiJson = json_decode(file_get_contents('https://bootswatch.com/api/4.json'));
+
+		$out = [];
+
+		foreach ($apiJson->themes as $theme) {
+			$out["{$theme->name} - {$theme->description}"] = $theme->cssMin;
+		}
+
+		return $response->withJson($out);
+	}
+
+
+	public function applyTheme(Request $request, Response $response): Response
+	{
+		file_put_contents('static/bootstrap/css/bootstrap.min.css', file_get_contents($request->getParam('css')));
+		return $response->withRedirect('/system')->withAddedHeader('Cache-Control', 'no-cache, must-revalidate');
+	}
 }
 }

+ 1 - 0
app/Controllers/LoginController.php

@@ -48,6 +48,7 @@ class LoginController extends Controller
 		Session::set('user_id', $result->id);
 		Session::set('user_id', $result->id);
 		Session::set('username', $result->username);
 		Session::set('username', $result->username);
 		Session::set('admin', $result->is_admin);
 		Session::set('admin', $result->is_admin);
+		Session::set('used_space', $this->humanFilesize($this->getUsedSpaceByUser($result->id)));
 
 
 		Session::alert("Welcome, $result->username!", 'info');
 		Session::alert("Welcome, $result->username!", 'info');
 		$this->logger->info("User $result->username logged in.");
 		$this->logger->info("User $result->username logged in.");

+ 6 - 5
app/Controllers/UploadController.php

@@ -60,7 +60,7 @@ class UploadController extends Controller
 			$user->id,
 			$user->id,
 			$code,
 			$code,
 			$file->getClientFilename(),
 			$file->getClientFilename(),
-			$storagePath
+			$storagePath,
 		]);
 		]);
 
 
 		$base_url = $this->settings['base_url'];
 		$base_url = $this->settings['base_url'];
@@ -104,7 +104,7 @@ class UploadController extends Controller
 				$type = explode('/', $mime)[0];
 				$type = explode('/', $mime)[0];
 				if ($type === 'text') {
 				if ($type === 'text') {
 					$media->text = $filesystem->read($media->storage_path);
 					$media->text = $filesystem->read($media->storage_path);
-				} elseif (in_array($type, ['image', 'video']) && $request->getHeaderLine('Scheme') === 'HTTP/2.0') {
+				} else if (in_array($type, ['image', 'video']) && $request->getHeaderLine('Scheme') === 'HTTP/2.0') {
 					$response = $response->withHeader('Link', "<{$this->settings['base_url']}/$args[userCode]/$args[mediaCode]/raw>; rel=preload; as={$type}");
 					$response = $response->withHeader('Link', "<{$this->settings['base_url']}/$args[userCode]/$args[mediaCode]/raw>; rel=preload; as={$type}");
 				}
 				}
 
 
@@ -115,7 +115,7 @@ class UploadController extends Controller
 			return $this->view->render($response, 'upload/public.twig', [
 			return $this->view->render($response, 'upload/public.twig', [
 				'media' => $media,
 				'media' => $media,
 				'type' => $mime,
 				'type' => $mime,
-				'extension' => pathinfo($media->filename, PATHINFO_EXTENSION)
+				'extension' => pathinfo($media->filename, PATHINFO_EXTENSION),
 			]);
 			]);
 		}
 		}
 	}
 	}
@@ -196,7 +196,7 @@ class UploadController extends Controller
 			throw new NotFoundException($request, $response);
 			throw new NotFoundException($request, $response);
 		}
 		}
 
 
-		$this->database->query('UPDATE `uploads` SET `published`=? WHERE `id`=?', [!$media->published, $media->id]);
+		$this->database->query('UPDATE `uploads` SET `published`=? WHERE `id`=?', [$media->published ? 0 : 1, $media->id]);
 
 
 		return $response->withStatus(200);
 		return $response->withStatus(200);
 	}
 	}
@@ -223,6 +223,7 @@ class UploadController extends Controller
 			} finally {
 			} finally {
 				$this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $args['id']);
 				$this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $args['id']);
 				$this->logger->info('User ' . Session::get('username') . ' deleted a media.', [$args['id']]);
 				$this->logger->info('User ' . Session::get('username') . ' deleted a media.', [$args['id']]);
+				Session::set('used_space', $this->humanFilesize($this->getUsedSpaceByUser(Session::get('user_id'))));
 			}
 			}
 		} else {
 		} else {
 			throw new UnauthorizedException();
 			throw new UnauthorizedException();
@@ -242,7 +243,7 @@ class UploadController extends Controller
 
 
 		$media = $this->database->query('SELECT * FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1', [
 		$media = $this->database->query('SELECT * FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1', [
 			$userCode,
 			$userCode,
-			$mediaCode
+			$mediaCode,
 		])->fetch();
 		])->fetch();
 
 
 		return $media;
 		return $media;

+ 6 - 1
app/Database/DB.php

@@ -45,7 +45,12 @@ class DB
 			$parameters = [$parameters];
 			$parameters = [$parameters];
 		}
 		}
 		$query = $this->pdo->prepare($query);
 		$query = $this->pdo->prepare($query);
-		$query->execute($parameters);
+
+		foreach ($parameters as $index => $parameter) {
+			$query->bindValue($index + 1, $parameter, is_int($parameter) ? PDO::PARAM_INT : PDO::PARAM_STR);
+		}
+
+		$query->execute();
 		return $query;
 		return $query;
 	}
 	}
 
 

+ 2 - 0
app/routes.php

@@ -3,6 +3,8 @@
 $app->group('', function () {
 $app->group('', function () {
 	$this->get('/home[/page/{page}]', \App\Controllers\DashboardController::class . ':home');
 	$this->get('/home[/page/{page}]', \App\Controllers\DashboardController::class . ':home');
 	$this->get('/system', \App\Controllers\DashboardController::class . ':system')->add(\App\Middleware\AdminMiddleware::class);
 	$this->get('/system', \App\Controllers\DashboardController::class . ':system')->add(\App\Middleware\AdminMiddleware::class);
+	$this->get('/system/themes', \App\Controllers\DashboardController::class . ':getThemes')->add(\App\Middleware\AdminMiddleware::class);
+	$this->post('/system/theme/apply', \App\Controllers\DashboardController::class . ':applyTheme')->add(\App\Middleware\AdminMiddleware::class);
 
 
 	$this->group('', function () {
 	$this->group('', function () {
 		$this->get('/users[/page/{page}]', \App\Controllers\UserController::class . ':index');
 		$this->get('/users[/page/{page}]', \App\Controllers\UserController::class . ':index');

+ 5 - 5
bin/clean

@@ -1,7 +1,7 @@
 #!/usr/bin/env php
 #!/usr/bin/env php
 <?php
 <?php
 
 
-require 'vendor/autoload.php';
+require __DIR__ . '/../vendor/autoload.php';
 
 
 if (php_sapi_name() !== 'cli') {
 if (php_sapi_name() !== 'cli') {
 	die();
 	die();
@@ -22,14 +22,14 @@ $action = isset($argv[1]) ? $argv[1] : 'all';
 
 
 switch ($action) {
 switch ($action) {
 	case 'cache':
 	case 'cache':
-		cleanDir('resources/cache');
+		cleanDir(__DIR__ . '/../resources/cache');
 		break;
 		break;
 	case 'sessions':
 	case 'sessions':
-		cleanDir('resources/sessions');
+		cleanDir(__DIR__ . '/../resources/sessions');
 		break;
 		break;
 	case 'all':
 	case 'all':
-		cleanDir('resources/cache');
-		cleanDir('resources/sessions');
+		cleanDir(__DIR__ . '/../resources/cache');
+		cleanDir(__DIR__ . '/../resources/sessions');
 		break;
 		break;
 	case 'help':
 	case 'help':
 	default:
 	default:

+ 1 - 1
bin/migrate

@@ -3,7 +3,7 @@
 
 
 use App\Database\DB;
 use App\Database\DB;
 
 
-require 'vendor/autoload.php';
+require __DIR__ . '/../vendor/autoload.php';
 
 
 if (php_sapi_name() !== 'cli') {
 if (php_sapi_name() !== 'cli') {
 	die();
 	die();

+ 1 - 1
bin/theme

@@ -1,7 +1,7 @@
 #!/usr/bin/env php
 #!/usr/bin/env php
 <?php
 <?php
 
 
-require 'vendor/autoload.php';
+require __DIR__ . '/../vendor/autoload.php';
 
 
 if (php_sapi_name() !== 'cli') {
 if (php_sapi_name() !== 'cli') {
 	die();
 	die();

+ 7 - 0
bootstrap/app.php

@@ -8,6 +8,13 @@ use Monolog\Logger;
 use Slim\App;
 use Slim\App;
 use Slim\Container;
 use Slim\Container;
 
 
+if (!file_exists('config.php') && is_dir('install/')) {
+	header('Location: /install/');
+	exit();
+} else if (!file_exists('config.php') && !is_dir('install/')) {
+	die('Cannot find the config file.');
+}
+
 // Load the config
 // Load the config
 $config = array_replace_recursive([
 $config = array_replace_recursive([
 	'app_name' => 'XBackBone',
 	'app_name' => 'XBackBone',

+ 2 - 3
install/index.php

@@ -60,8 +60,8 @@ $app->post('/', function (Request $request, Response $response) use (&$config) {
 	$config['displayErrorDetails'] = false;
 	$config['displayErrorDetails'] = false;
 	$config['db']['connection'] = $request->getParam('connection');
 	$config['db']['connection'] = $request->getParam('connection');
 	$config['db']['dsn'] = $request->getParam('dsn');
 	$config['db']['dsn'] = $request->getParam('dsn');
-	$config['db']['username'] = null;
-	$config['db']['password'] = null;
+	$config['db']['username'] = $request->getParam('db_user');
+	$config['db']['password'] = $request->getParam('db_password');
 
 
 
 
 	file_put_contents(__DIR__ . '/../config.php', '<?php' . PHP_EOL . 'return ' . var_export($config, true) . ';');
 	file_put_contents(__DIR__ . '/../config.php', '<?php' . PHP_EOL . 'return ' . var_export($config, true) . ';');
@@ -130,7 +130,6 @@ $app->post('/', function (Request $request, Response $response) use (&$config) {
 
 
 	DB::query("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES (?, 'admin', ?, 1, ?)", [$request->getParam('email'), password_hash($request->getParam('password'), PASSWORD_DEFAULT), substr(md5(microtime()), rand(0, 26), 5)]);
 	DB::query("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES (?, 'admin', ?, 1, ?)", [$request->getParam('email'), password_hash($request->getParam('password'), PASSWORD_DEFAULT), substr(md5(microtime()), rand(0, 26), 5)]);
 
 
-	Session::alert('Installation completed successfully!', 'success');
 	return $response->withRedirect('../?afterInstall=true');
 	return $response->withRedirect('../?afterInstall=true');
 });
 });
 
 

+ 32 - 6
install/templates/install.twig

@@ -51,7 +51,8 @@
                         <div class="form-group row">
                         <div class="form-group row">
                             <label for="base_url" class="col-sm-3 col-form-label">Base URL</label>
                             <label for="base_url" class="col-sm-3 col-form-label">Base URL</label>
                             <div class="col-sm-9">
                             <div class="col-sm-9">
-                                <input type="text" class="form-control" id="base_url" name="base_url" value="{{ config.base_url }}" autocomplete="off" required>
+                                <input type="text" class="form-control" id="base_url" name="base_url"
+                                       value="{{ config.base_url }}" autocomplete="off" required>
                             </div>
                             </div>
                         </div>
                         </div>
                         <hr>
                         <hr>
@@ -60,6 +61,7 @@
                             <div class="col-sm-9">
                             <div class="col-sm-9">
                                 <select name="connection" id="connection" required class="form-control">
                                 <select name="connection" id="connection" required class="form-control">
                                     <option value="sqlite" selected>SQLite</option>
                                     <option value="sqlite" selected>SQLite</option>
+                                    <option value="mysql">MySQL</option>
                                 </select>
                                 </select>
                             </div>
                             </div>
                         </div>
                         </div>
@@ -67,7 +69,8 @@
                         <div class="form-group row">
                         <div class="form-group row">
                             <label for="dsn" class="col-sm-3 col-form-label">Database Source Name (DSN)</label>
                             <label for="dsn" class="col-sm-3 col-form-label">Database Source Name (DSN)</label>
                             <div class="col-sm-9">
                             <div class="col-sm-9">
-                                <input type="text" class="form-control" id="dsn" name="dsn" value="{{ config.db.dsn }}" autocomplete="off" required>
+                                <input type="text" class="form-control" id="dsn" name="dsn" value="{{ config.db.dsn }}"
+                                       autocomplete="off" required>
                             </div>
                             </div>
                         </div>
                         </div>
 
 
@@ -81,28 +84,33 @@
                         <div class="form-group row">
                         <div class="form-group row">
                             <label for="db_password" class="col-sm-3 col-form-label">Database Password</label>
                             <label for="db_password" class="col-sm-3 col-form-label">Database Password</label>
                             <div class="col-sm-9">
                             <div class="col-sm-9">
-                                <input type="password" class="form-control" id="db_password" name="db_password" autocomplete="off" disabled>
+                                <input type="password" class="form-control" id="db_password" name="db_password"
+                                       autocomplete="off" disabled>
                             </div>
                             </div>
                         </div>
                         </div>
                         <hr>
                         <hr>
                         <div class="form-group row">
                         <div class="form-group row">
                             <label for="storage_dir" class="col-sm-3 col-form-label">Storage Directory</label>
                             <label for="storage_dir" class="col-sm-3 col-form-label">Storage Directory</label>
                             <div class="col-sm-9">
                             <div class="col-sm-9">
-                                <input type="text" class="form-control" id="storage_dir" name="storage_dir" value="{{ config.storage_dir }}" autocomplete="off" required>
+                                <input type="text" class="form-control" id="storage_dir" name="storage_dir"
+                                       value="{{ config.storage_dir }}" autocomplete="off" required>
+                                <small>Must be a writable directory</small>
                             </div>
                             </div>
                         </div>
                         </div>
                         <hr>
                         <hr>
                         <div class="form-group row">
                         <div class="form-group row">
                             <label for="email" class="col-sm-3 col-form-label">Admin email</label>
                             <label for="email" class="col-sm-3 col-form-label">Admin email</label>
                             <div class="col-sm-9">
                             <div class="col-sm-9">
-                                <input type="email" class="form-control" id="email" placeholder="email@example.com" name="email" autocomplete="off" required>
+                                <input type="email" class="form-control" id="email" placeholder="email@example.com"
+                                       name="email" autocomplete="off" required>
                             </div>
                             </div>
                         </div>
                         </div>
 
 
                         <div class="form-group row">
                         <div class="form-group row">
                             <label for="password" class="col-sm-3 col-form-label">Admin password</label>
                             <label for="password" class="col-sm-3 col-form-label">Admin password</label>
                             <div class="col-sm-9">
                             <div class="col-sm-9">
-                                <input type="password" class="form-control" id="password" placeholder="Password" name="password" autocomplete="off" required>
+                                <input type="password" class="form-control" id="password" placeholder="Password"
+                                       name="password" autocomplete="off" required>
                             </div>
                             </div>
                         </div>
                         </div>
 
 
@@ -119,6 +127,24 @@
         </div>
         </div>
     </div>
     </div>
 </div>
 </div>
+<script>
+    $(document).ready(function () {
+        $('#connection').change(function () {
+            switch ($(this).val()) {
+                case 'sqlite':
+                    $('#dsn').val('resources/database/xbackbone.db');
+                    $('#db_user').val('').prop('disabled', 'disabled');
+                    $('#db_password').val('').prop('disabled', 'disabled');
+                    break;
+                case 'mysql':
+                    $('#dsn').val('host=localhost;port=3306;dbname=xbackbone');
+                    $('#db_user').val('db_user').prop('disabled', '');
+                    $('#db_password').val('').prop('disabled', '');
+                    break;
+            }
+        });
+    })
+</script>
 {% include 'footer.twig' %}
 {% include 'footer.twig' %}
 </body>
 </body>
 </html>
 </html>

+ 26 - 0
resources/schemas/mysql/mysql.1.sql

@@ -0,0 +1,26 @@
+CREATE TABLE IF NOT EXISTS `users` (
+  `id`        INTEGER           PRIMARY KEY      AUTO_INCREMENT,
+  `email`     VARCHAR(30)       NOT NULL,
+  `username`  VARCHAR(30)       NOT NULL,
+  `password`  VARCHAR(256)      NOT NULL,
+  `user_code` VARCHAR(5),
+  `token`     VARCHAR(256),
+  `active`    BOOLEAN           NOT NULL         DEFAULT 1,
+  `is_admin`  BOOLEAN           NOT NULL         DEFAULT 0,
+  `registration_date` TIMESTAMP NOT NULL         DEFAULT CURRENT_TIMESTAMP,
+  INDEX (`username`, `user_code`, `token`)
+);
+
+CREATE TABLE IF NOT EXISTS `uploads` (
+  `id`           INTEGER PRIMARY KEY           AUTO_INCREMENT,
+  `user_id`      INTEGER(20),
+  `code`         VARCHAR(64)  NOT NULL,
+  `filename`     VARCHAR(128) NOT NULL,
+  `storage_path` VARCHAR(256) NOT NULL,
+  `published`    BOOLEAN      NOT NULL         DEFAULT 1,
+  `timestamp`    TIMESTAMP    NOT NULL         DEFAULT CURRENT_TIMESTAMP,
+  INDEX (`code`),
+  FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
+    ON UPDATE CASCADE
+    ON DELETE SET NULL
+);

+ 8 - 8
resources/schemas/sqlite/sqlite.1.sql

@@ -1,13 +1,13 @@
 CREATE TABLE IF NOT EXISTS `users` (
 CREATE TABLE IF NOT EXISTS `users` (
-  `id`        INTEGER PRIMARY KEY           AUTOINCREMENT,
-  `email`     VARCHAR(30)  NOT NULL,
-  `username`  VARCHAR(30)  NOT NULL,
-  `password`  VARCHAR(256) NOT NULL,
+  `id`        INTEGER           PRIMARY KEY      AUTOINCREMENT,
+  `email`     VARCHAR(30)       NOT NULL,
+  `username`  VARCHAR(30)       NOT NULL,
+  `password`  VARCHAR(256)      NOT NULL,
   `user_code` VARCHAR(5),
   `user_code` VARCHAR(5),
   `token`     VARCHAR(256),
   `token`     VARCHAR(256),
-  `active`    BOOLEAN      NOT NULL         DEFAULT 1,
-  `is_admin`  BOOLEAN      NOT NULL         DEFAULT 0,
-  `registration_date`      NOT NULL         DEFAULT CURRENT_TIMESTAMP
+  `active`    BOOLEAN           NOT NULL         DEFAULT 1,
+  `is_admin`  BOOLEAN           NOT NULL         DEFAULT 0,
+  `registration_date` TIMESTAMP NOT NULL         DEFAULT CURRENT_TIMESTAMP
 );
 );
 
 
 CREATE TABLE IF NOT EXISTS `uploads` (
 CREATE TABLE IF NOT EXISTS `uploads` (
@@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS `uploads` (
   `filename`     VARCHAR(128) NOT NULL,
   `filename`     VARCHAR(128) NOT NULL,
   `storage_path` VARCHAR(256) NOT NULL,
   `storage_path` VARCHAR(256) NOT NULL,
   `published`    BOOLEAN      NOT NULL         DEFAULT 1,
   `published`    BOOLEAN      NOT NULL         DEFAULT 1,
-  `timestamp`                 NOT NULL         DEFAULT CURRENT_TIMESTAMP,
+  `timestamp`    TIMESTAMP    NOT NULL         DEFAULT CURRENT_TIMESTAMP,
   FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
   FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
     ON UPDATE CASCADE
     ON UPDATE CASCADE
     ON DELETE SET NULL
     ON DELETE SET NULL

+ 2 - 0
resources/templates/comp/navbar.twig

@@ -30,6 +30,8 @@
                         <i class="fas fa-fw fa-user"></i> {{ session.username }}
                         <i class="fas fa-fw fa-user"></i> {{ session.username }}
                     </a>
                     </a>
                     <div class="dropdown-menu" aria-labelledby="userDropdown">
                     <div class="dropdown-menu" aria-labelledby="userDropdown">
+                        <a class="dropdown-item disabled" href="javascript:void(0)">Used: {{ session.used_space }}</a>
+                        <div class="dropdown-divider"></div>
                         <a class="dropdown-item" href="{{ config.base_url }}/profile"><i class="fas fa-fw fa-user"></i> Profile</a>
                         <a class="dropdown-item" href="{{ config.base_url }}/profile"><i class="fas fa-fw fa-user"></i> Profile</a>
                         <a class="dropdown-item" href="{{ config.base_url }}/logout"><i class="fas fa-fw fa-sign-out-alt"></i> Logout</a>
                         <a class="dropdown-item" href="{{ config.base_url }}/logout"><i class="fas fa-fw fa-sign-out-alt"></i> Logout</a>
                     </div>
                     </div>

+ 29 - 5
resources/templates/dashboard/system.twig

@@ -54,14 +54,38 @@
             </div>
             </div>
         </div>
         </div>
         <div class="row">
         <div class="row">
-            <div class="col-md-6 offset-md-3">
+            <div class="col-md-8">
+                <div class="card">
+                    <div class="card-header"><i class="fas fa-paint-brush fa-fw"></i> Theme</div>
+                    <div class="card-body">
+                        <form method="post" action="{{ config.base_url }}/system/theme/apply">
+                            <div class="form-group row">
+                                <div class="col-sm-12">
+                                    <select class="form-control" id="themes" name="css">
+                                        <option id="theme-load" selected disabled hidden>Click to load...</option>
+                                    </select>
+                                </div>
+                            </div>
+                            <div class="form-group row">
+                                <div class="col-sm-12">
+                                    <button type="submit" class="btn btn-outline-success" id="themes-apply" disabled>
+                                        <i class="fas fa-save fa-fw"></i> Apply
+                                    </button>
+                                </div>
+                            </div>
+                        </form>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-4">
                 <div class="card">
                 <div class="card">
                     <div class="card-header"><i class="fas fa-cog fa-fw"></i> System Information</div>
                     <div class="card-header"><i class="fas fa-cog fa-fw"></i> System Information</div>
                     <div class="card-body">
                     <div class="card-body">
-                        <p>
-                            <strong>Max upload size (<code>max_post_size/upload_max_filesize</code>):</strong>
-                            {{ max_filesize }}
-                        </p>
+                        <strong>Max upload size:</strong>
+                        <ul>
+                            <li><code>post_max_size</code>: {{ post_max_size }}</li>
+                            <li><code>upload_max_filesize</code>: {{ upload_max_filesize }}</li>
+                        </ul>
                     </div>
                     </div>
                 </div>
                 </div>
             </div>
             </div>

+ 16 - 0
src/js/app.js

@@ -7,6 +7,7 @@ var app = {
         $('.media-delete').click(app.mediaDelete);
         $('.media-delete').click(app.mediaDelete);
         $('.publish-toggle').click(app.publishToggle);
         $('.publish-toggle').click(app.publishToggle);
         $('.refresh-token').click(app.refreshToken);
         $('.refresh-token').click(app.refreshToken);
+        $('#themes').mousedown(app.loadThemes);
 
 
         $('.alert').fadeTo(2000, 500).slideUp(500, function () {
         $('.alert').fadeTo(2000, 500).slideUp(500, function () {
             $('.alert').slideUp(500);
             $('.alert').slideUp(500);
@@ -64,6 +65,21 @@ var app = {
         $.post(window.AppConfig.base_url + '/user/' + id + '/refreshToken', function (data) {
         $.post(window.AppConfig.base_url + '/user/' + id + '/refreshToken', function (data) {
             $('#token').val(data);
             $('#token').val(data);
         });
         });
+    },
+    loadThemes: function (e) {
+        e.preventDefault();
+        var $themes = $('#themes');
+        $.get(window.AppConfig.base_url + '/system/themes', function (data) {
+            $themes.empty();
+            Object.keys(data).forEach(function (key) {
+                var opt = document.createElement('option');
+                opt.value = data[key];
+                opt.innerHTML = key;
+                $themes.append(opt);
+            });
+            $('#themes-apply').prop('disabled', false);
+        });
+        $themes.unbind('mousedown');
     }
     }
 };
 };