Compare commits

...

3 commits
dev ... main

Author SHA1 Message Date
166cf410fb Update defaut theme 2024-09-12 16:21:52 +02:00
0d1b22d2f9 Add visual editor for services.yaml
Some adjust CSS
2024-07-16 10:21:19 +02:00
58ec0cb655 Add url for download favicon 2024-07-05 09:34:07 +02:00
16 changed files with 831 additions and 145 deletions

View file

@ -2,7 +2,7 @@ FROM debian:stable-slim
MAINTAINER Knah Tsaeb <knah-tsaeb_soshot@knah-tsaeb.org> MAINTAINER Knah Tsaeb <knah-tsaeb_soshot@knah-tsaeb.org>
LABEL version="0.2.1" LABEL version="0.3.0"
LABEL description="Apache 2 / PHP / Nofu" LABEL description="Apache 2 / PHP / Nofu"
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
@ -30,7 +30,7 @@ WORKDIR /var/www/
ENTRYPOINT "start.sh" ENTRYPOINT "start.sh"
# Build image # Build image
# docker buildx build -t nofu:0.2.1 . # docker buildx build -t nofu:0.3.0 .
# Run container # Run container
# docker run -d --restart unless-stopped -v nofu_data:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.2.1 # docker run -d --restart unless-stopped -v nofu_data:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.3.0
# docker run -d --restart unless-stopped -v /opt/nofu:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.2.1 # docker run -d --restart unless-stopped -v /opt/nofu:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.3.0

View file

@ -1,6 +1,6 @@
# Nofu # Nofu
Nofu for **N**ot **O**nly **F**or **U**s is personal dashboard Nofu for **N**ot **O**nly **F**or **U**s is personal dashboard (V0.3.0)
## Table of Contents ## Table of Contents
@ -67,13 +67,13 @@ wget https://forge.leslibres.org/Knah-Tsaeb/Nofu/raw/branch/main/Dockerfile
``` ```
```shell ```shell
docker buildx build -t nofu:0.2.1 . docker buildx build -t nofu:0.3.0 .
``` ```
#### Run #### Run
```shell ```shell
docker run -d --restart unless-stopped -v nofu_data:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.2.1 docker run -d --restart unless-stopped -v nofu_data:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.3.0
``` ```
Open http://127.0.0.1:8189 Open http://127.0.0.1:8189
@ -99,12 +99,13 @@ Put screenshot of your service.
#### Favicons #### Favicons
Put favicon of your service. If you can prefer 128x128 favicon size (or higher). Put favicon of your service. If you can prefer 128x128 favicon size (or higher).
You can find many icon for your app in [Dashboard-Icons](https://github.com/walkxcode/Dashboard-Icons).
### Services file ### Services file
The services file contain a list of your service. Is simple text file, you can edit it with simple text editor (notepad, Pluma, Kate, Vim, Nano....). Nofu use[YAML](https://en.wikipedia.org/wiki/YAML) markup. The services file contain a list of your service. Is simple text file, you can edit it with simple text editor (notepad, Pluma, Kate, Vim, Nano....). Nofu use[YAML](https://en.wikipedia.org/wiki/YAML) markup.
Create new file "/data/services.yaml" and edit it or create it localy and upload after on your server. Create new file "/data/services.yaml" and edit it or create it localy and upload after on your server. You can use a basic editor include in Nofu, <img alt="docker icon" src="public/assets/icons/edit.svg" width="20">.
Example Example
@ -150,7 +151,11 @@ Replace the default data direcory by your backup, go to settings pages and check
## Ressources ## Ressources
### Images
* <a href="https://www.svgrepo.com/svg/448401/docker"><img alt="docker icon" src="public/assets/icons/docker.svg" width="24"> svgrepo.com (MLP licence)</a> * <a href="https://www.svgrepo.com/svg/448401/docker"><img alt="docker icon" src="public/assets/icons/docker.svg" width="24"> svgrepo.com (MLP licence)</a>
* <a href="https://www.svgrepo.com/svg/448409/edit"><img alt="docker icon" src="public/assets/icons/edit.svg" width="24"> svgrepo.com Logo (MLP License)</a>
* <a href="https://www.svgrepo.com/svg/308923/visual-interface-image-picture-tap"><img alt="docker icon" src="public/assets/icons/editImage.svg" width="24"> svgrepo.com Logo (CC0 License)</a>
* <a href="https://www.svgrepo.com/svg/375064/question-mark"><img alt="docker icon" src="public/assets/icons/help.svg" width="24"> svgrepo.com (CC Attribution License)</a> * <a href="https://www.svgrepo.com/svg/375064/question-mark"><img alt="docker icon" src="public/assets/icons/help.svg" width="24"> svgrepo.com (CC Attribution License)</a>
* <a href="https://www.svgrepo.com/svg/56164/logout"><img alt="logout icon" src="public/assets/icons/logout.svg" width="24"> svgrepo.com (CCO licence)</a> * <a href="https://www.svgrepo.com/svg/56164/logout"><img alt="logout icon" src="public/assets/icons/logout.svg" width="24"> svgrepo.com (CCO licence)</a>
* <a href="https://www.svgrepo.com/svg/195725/moon"><img alt="logout icon" src="public/assets/icons/moon.svg" width="24"> svgrepo.com (CCO licence)</a> * <a href="https://www.svgrepo.com/svg/195725/moon"><img alt="logout icon" src="public/assets/icons/moon.svg" width="24"> svgrepo.com (CCO licence)</a>
@ -162,6 +167,17 @@ Replace the default data direcory by your backup, go to settings pages and check
* <a href="https://www.svgrepo.com/svg/521261/web"><img alt="docker icon" src="public/assets/icons/web.svg" width="24"> svgrepo.com Logo (CC Attribution License)</a> * <a href="https://www.svgrepo.com/svg/521261/web"><img alt="docker icon" src="public/assets/icons/web.svg" width="24"> svgrepo.com Logo (CC Attribution License)</a>
* <a href="https://www.svgrepo.com/svg/105529/hard-drive-interior"><img alt="docker icon" src="public/assets/icons/webapp.svg" width="24"> svgrepo.com Logo (CC0 License)</a> * <a href="https://www.svgrepo.com/svg/105529/hard-drive-interior"><img alt="docker icon" src="public/assets/icons/webapp.svg" width="24"> svgrepo.com Logo (CC0 License)</a>
### Js libs
* <a href="https://github.com/nodeca/js-yaml">js-yaml (MIT licence)</a>
### PHP libs
* <a href="https://github.com/symfony/yaml">symfony/yaml (MIT License)</a>
* <a href="https://github.com/stefangabos/zebra_image">stefangabos/zebra_image (GNU License)</a>
* <a href="https://github.com/thephpleague/commonmark">thephpleague/commonmark (BSD 3-Clause "New" or "Revised" License)</a>
* <a href="https://github.com/stefangabos/zebra_image">stefangabos/zebra_image (GNU License)</a>
And some piece of code from Stack Overflow :-) And some piece of code from Stack Overflow :-)
## Licence ## Licence

View file

@ -3,6 +3,7 @@
namespace KTH; namespace KTH;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;
class App { class App {
@ -30,7 +31,11 @@ class App {
* @return array An array containing the unique types and locations of the services. * @return array An array containing the unique types and locations of the services.
*/ */
public function makeMenu(array $services): array { public function makeMenu(array $services): array {
$menu = [];
foreach ($services as $service) { foreach ($services as $service) {
if(!is_array($service)){
return $menu;
}
$menu['type'][] = $service['type']; $menu['type'][] = $service['type'];
$menu['location'][] = $service['location']; $menu['location'][] = $service['location'];
} }
@ -47,11 +52,11 @@ class App {
* *
* @return string The path to the image file or a default image. * @return string The path to the image file or a default image.
*/ */
public function returnImg(string $file, string $type): string { public function returnImg(string|null $file, string $type): string {
$defaultFile = 'assets/icons/missing.svg'; $defaultFile = 'assets/icons/missing.svg';
$permitType = ['favicons', 'big_favicons', 'screenshots', 'thumbs']; $permitType = ['favicons', 'big_favicons', 'screenshots', 'thumbs'];
if (empty($file)) { if (empty($file) || is_null($file)) {
return $defaultFile; return $defaultFile;
} }
@ -118,6 +123,24 @@ class App {
$this->clearCache(); $this->clearCache();
} }
/**
* Save services list
*
* @return void
*/
function saveServices(): array {
try {
$value = Yaml::parse(trim($_POST['services'], 2));
} catch (ParseException $exception) {
return ['status' => 'error', 'message' => $exception->getMessage(), 'content' => $_POST['services']];
}
file_put_contents('../data/services.yaml', $_POST['services']);
$this->clearCache();
return ['status' => 'success', 'message' => ''];
}
/** /**
* Get the configuration settings. * Get the configuration settings.
* *
@ -172,4 +195,35 @@ class App {
mkdir('../data/imgs/screenshots', 0775, true); mkdir('../data/imgs/screenshots', 0775, true);
} }
} }
public function getServices() {
if (file_exists('../data/services.yaml')) {
$services = Yaml::parseFile('../data/services.yaml');
} else {
$services = [
0 => [
'title' => 'Wikipedia',
'screenshot' => 'wikipedia.png',
'favicon' => 'wikipedia.png',
'link' => 'https://en.wikipedia.org/wiki/Dashboard_(computing)',
'appHome' => 'https://www.mediawiki.org/wiki/MediaWiki',
'location' => 'web',
'desc' => 'Wikipedia, the free encyclopedia',
'type' => 'webapp'
],
[
'title' => 'Awesome-Selfhosted',
'screenshot' => 'Awesome-Selfhosted.png',
'favicon' => 'Awesome-Selfhosted.ico',
'link' => 'https://awesome-selfhosted.net/',
'appHome' => 'https://www.mediawiki.org/wiki/MediaWiki',
'location' => 'web',
'desc' => 'A list of Free Software network services and web applications which can be hosted on your own servers',
'type' => 'webapp'
]
];
}
return $services;
}
} }

View file

@ -12,7 +12,7 @@ class Debug {
* *
* @return void * @return void
*/ */
function n_print(mixed $data, string $name = ''): void { static function n_print(mixed $data, string $name = ''): void {
error_reporting(-1); error_reporting(-1);
$aBackTrace = debug_backtrace(); $aBackTrace = debug_backtrace();
echo '<h2>', $name, '</h2>'; echo '<h2>', $name, '</h2>';

175
app/utils/File.php Normal file
View file

@ -0,0 +1,175 @@
<?php
namespace Utils;
use Utils\SanitizeName;
use Utils\Debug;
class File {
private $path = __DIR__ . '/../../data/';
public $subPath;
/**
* Define a subpath for $path
* example for file
* - imilo
* - region
* - local
*
* @var string
*/
public $type;
public $filterType = '*';
private $mimePermit = [
'png' => 'png',
'jpg' => 'jpg',
'bmp' => 'bmp',
'gif' => 'gif',
'webp' => 'webp'
];
public function __construct() {
return $this;
}
/*
* @todo define list of authorized path
*/
public function setPath($path) {
$this->path = $path;
}
public function getFile($filename) {
ob_end_clean();
$path = realpath($this->path . $this->type);
$file = $path . '/' . $filename;
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: ' . mime_content_type($file));
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
}
}
public function getFileList(): array {
$path = realpath($this->path . $this->type);
foreach (glob($path . '/' . $this->filterType) as $filename) {
$path_parts = pathinfo($filename);
$fileList[] = [
'file' => basename($filename),
'path' => $this->subPath . '/' . $this->type . '/' . basename($filename),
'type' => mime_content_type($filename),
'ext' => $path_parts['extension'],
'icon' => $this->defineIcon($path_parts['extension']),
'size' => $this->human_filesize(filesize($filename)),
'addTime' => date("d-m-Y H:i", filectime($filename))
];
}
if (empty($fileList)) {
$fileList = [];
}
return $fileList;
}
private function defineIcon(string $mimeFile): string {
if (array_key_exists($mimeFile, $this->mimePermit)) {
return $this->mimePermit[$mimeFile];
} else {
return 'raw';
}
}
private function human_filesize(int $bytes, int $decimals = 2): string {
$factor = floor((strlen($bytes) - 1) / 3);
if ($factor > 0) $sz = 'KMGT';
return sprintf("%.{$decimals}f ", $bytes / pow(1024, $factor)) . @$sz[$factor - 1] . 'B';
}
public function saveFile($file, $replace = true): array {
$fileType = strtolower(pathinfo($_FILES["fileName"]["name"], PATHINFO_EXTENSION));
$fileName = strtolower(pathinfo($_FILES["fileName"]["name"], PATHINFO_FILENAME));
$file = SanitizeName::sanitizeName($fileName);
$cleanName = $file . '.' . $fileType;
$target_file = $this->path . '/' . $this->type . '/' . $cleanName;
/*
* @todo permit replace
*/
if (file_exists($target_file) && $replace === false) {
return [
'status' => 'danger',
'msg' => 'Le fichier existe déjà.'
];
}
$upload_max_size = ini_get('upload_max_filesize');
/*
* @Todo return max upload
*/
if ($_FILES["fileName"]["size"] > $upload_max_size) {
return [
'status' => 'danger',
'msg' => 'Le fichier soumis est trop volumineux.'
];
}
if (!array_key_exists($fileType, $this->mimePermit)) {
return [
'status' => 'danger',
'msg' => 'Les fichiers ' . $fileType . ' ne sont pas autorisés.'
];
}
if (move_uploaded_file($_FILES["fileName"]["tmp_name"], $target_file)) {
return [
'status' => 'success',
'msg' => 'Le fichier ' . htmlspecialchars($cleanName) . ' à bien été envoyé.'
];
} else {
return [
'status' => 'danger',
'msg' => 'Une erreur c\'est produite.'
];
}
}
public function deleteFile($filename) {
$path = realpath($this->path . $this->type);
$file = $path . '/' . $filename;
if (file_exists($file)) {
if (unlink($file)) {
return [
'status' => 'success',
'msg' => 'Le fichier a été supprimé avec succès.'
];
} else {
return [
'status' => 'danger',
'msg' => 'Une erreur c\'est produite lors de la suppression du fichier.'
];
}
} else {
return [
'status' => 'danger',
'msg' => 'Le fichier n\'existe pas.'
];
}
}
}

104
app/utils/SanitizeName.php Normal file
View file

@ -0,0 +1,104 @@
<?php
namespace Utils;
class SanitizeName {
private static $reservedWindowsNames = [
'con',
'prn',
'aux',
'nul',
'com1',
'com2',
'com3',
'com4',
'com5',
'com6',
'com7',
'com8',
'com9',
'lpt1',
'lpt2',
'lpt3',
'lpt4',
'lpt5',
'lpt6',
'lpt7',
'lpt8',
'lpt9',
];
/**
* https://github.com/GravityPDF/Upload
* Set file name (without extension)
*
* Sanitize the filename (if outputting the filename to HTML you still need to escape)
*
* @param string $name
* @return FileInfo Self
*
* @link https://stackoverflow.com/a/42058764
* @internal 1. file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
* phpcs:ignore
* @internal 2. control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
* @internal 3. URI reserved https://www.rfc-editor.org/rfc/rfc3986#section-2.2
* @internal 4. URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
*/
static function sanitizeName(string $name): string {
$name = str_replace(['%20', '+', '.'], '-', $name); //replaces encoded space, +, or .
$name = (string) preg_replace('/[\r\n\t-]+/', '-', $name); //replace tab or new line characters
$name = (string) preg_replace(
'~
[%<>:"/\\\|?*]| # @internal 1.
[\x00-\x1F]| # @internal 2.
[#\[\]@!$&\'()+,;=]| # @internal 3.
[{}^\~`] # @internal 4.
~x',
'-',
$name
);
// reduce consecutive characters
$name = (string) preg_replace(
[
'/ +/', // "file name.zip" becomes "file name.zip"
'/_+/', // "file___name.zip" becomes "file_name.zip"
'/ - -+/', // "file - -name.zip" becomes "file--name.zip"
'/-+/', // "file--name.zip" becomes "file-name.zip"
],
[
' ',
'_',
'-',
'-',
],
$name
);
$name = trim((string)$name, '.-_ '); //remove dot, hyphen, underscore, or space from start and end of string
/* Ensure filename is not a reserved Windows name, otherwise remove */
if (in_array(strtolower($name), self::$reservedWindowsNames, true)) {
$name = '';
}
/*
* Ensure filename is not longer than 255 bytes http://serverfault.com/a/9548/44086, otherwise shorten
*/
$extension = 'php';
$maxLength = 255 - ($extension ? strlen($extension) + 1 : 0);
/* Use multibyte aware functions, if the server supports it */
if (function_exists('mb_strcut') && function_exists('mb_detect_encoding')) {
$name = mb_strcut($name, 0, $maxLength, (string)mb_detect_encoding($name));
} else {
$name = substr($name, 0, $maxLength);
}
if (empty($name)) {
$name = 'unnamed-file';
}
return $name;
}
}

View file

@ -6,40 +6,85 @@
*/ */
:root { :root {
--background-color: #dadada; color-scheme: dark light;
--border-radius: 10px;
--light-color: #ececec;
--nav-background-color: #34495e;
--text-color: #1d1e22;
--margin: .7em;
h1 { --primary: #cc2027;
color: var(--text-color); --primary-darken: #8E161B;
} --primary-lighten: #D64C52;
--primary-text-contrast: #FFF;
.login { --secondary: #20ccc5;
color: var(--text-color); --secondary-darken: #168E89;
border: 1px solid var(--text-color); --secondary-lighten: #4CD6D0;
} --secondary-text-contrast: #000;
--error: #c43933;
--error-darken: #892723;
--error-lighten: #CF605B;
--error-text-contrast: #FFF;
--info: #206ccc;
--info-darken: #164B8E;
--info-lighten: #4C89D6;
--info-text-contrast: #FFF;
--success: #7dcc20;
--success-darken: #578E16;
--success-lighten: #97D64C;
--success-text-contrast: #000;
--warning: #cc5e20;
--warning-darken: #8E4116;
--warning-lighten: #D67E4C;
--warning-text-contrast: #FFF;
--background-color: light-dark(#fffbfb, #171414);
--background-color-darken: light-dark(#B2AFAF, #100E0E);
--background-color-lighten: light-dark(#FFFBFB, #454343);
--header-background-color: light-dark(#171414, #fffbfb);
--header-background-color-darken: light-dark(#100E0E, #B2AFAF);
--header-background-color-lighten: light-dark(#454343, #FFFBFB);
--header-text-color: light-dark(#fffbfb, #171414);
--header-text-color-secondary: #ffffffb3;
--header-text-color-disable: light-dark(#ffffff80, #454343);
--text-color: light-dark(#171414, #fffbfb);
--text-color-secondary: #ffffffb3;
--text-color-disable: light-dark(#454343, #ffffff80);
--text-color-inverse: light-dark(#fffbfb, #171414);
--text-color-secondary-inverse: #ffffffb3;
--text-color-disable-inverse: light-dark(#ffffff80, #454343);
--h1-color: var(--primary);
--h2-color: #c33d35;
--h3-color: #b94f44;
--h4-color: #ae5e52;
--h5-color: #a16a61;
--h6-color: #927671;
} }
[data-theme="dark"] { [data-theme="dark"] {
--background-color: #171414;
--text-color: #fffbfb;
--text-color-inverse: #171414;
}
--background-color: #1d1e22; [data-theme="light"] {
--border-radius: 10px; --background-color: #fffbfb;
--light-color: #ececec; --text-color: #171414;
--nav-background-color: #34495e; --text-color-inverse: #fffbfb;
--text-color: #1d1e22;
.card, .modal-content, .modal-header {
background: var(--background-color) !important;
}
}
:root {
--margin: .7em; --margin: .7em;
--border-radius: 10px;
h1 {
color: var(--light-color);
}
.login {
color: var(--light-color);
border: 1px solid var(--light-color);
}
} }
*, *,
@ -58,6 +103,24 @@ body {
margin: var(--margin); margin: var(--margin);
} }
h1 {
color: var(--h1-color);
font-size: 2.5rem;
margin: 0;
}
svg {
fill: var(--primary);
height: 1.2em;
transition: .3s all ease;
vertical-align: text-bottom;
}
svg:hover {
background-color: var(--primary-lighten);
fill: var(--light-color);
}
.titleBar { .titleBar {
text-align: center; text-align: center;
} }
@ -69,10 +132,12 @@ body {
height: 2.5em; height: 2.5em;
width: 2.5em; width: 2.5em;
margin: var(--margin); margin: var(--margin);
fill: var(--primary);
} }
a { svg:hover {
text-align: none; background-color: inherit;
fill: var(--primary-lighten);
} }
} }
@ -83,24 +148,6 @@ body {
height: 2.5em; height: 2.5em;
} }
h1 {
color: var(--light-color);
font-size: 2.5rem;
margin: 0;
}
svg {
fill: var(--nav-background-color);
height: 1.2em;
transition: .3s all ease;
vertical-align: text-bottom;
}
svg:hover {
background-color: var(--nav-background-color);
fill: var(--light-color);
}
.card-container { .card-container {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
@ -110,18 +157,19 @@ svg:hover {
padding: 0 .5em; padding: 0 .5em;
} }
.card a {
display: block;
}
.card-container h3 a { .card-container h3 a {
color: var(--text-color); color: var(--text-color);
text-decoration: none; text-decoration: none;
} }
.card-container .card { .card-container .card {
cursor: pointer; background: var(--background-color-lighten);
}
.card-container .card {
background: var(--light-color);
box-shadow: 0 0 2px 2px rgba(0, 0, 0, .05); box-shadow: 0 0 2px 2px rgba(0, 0, 0, .05);
cursor: pointer;
margin: var(--margin); margin: var(--margin);
transition: .3s all ease; transition: .3s all ease;
width: calc(100% / 7 - 20px); width: calc(100% / 7 - 20px);
@ -213,7 +261,7 @@ svg:hover {
.modal { .modal {
background-color: rgb(0, 0, 0); background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.6);
display: none; display: none;
height: 100%; height: 100%;
left: 0; left: 0;
@ -228,7 +276,7 @@ svg:hover {
.modal-content { .modal-content {
animation-duration: 0.3s; animation-duration: 0.3s;
animation-name: animatetop; animation-name: animatetop;
background-color: var(--light-color); background: var(--background-color-lighten);
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
font-size: 150%; font-size: 150%;
@ -250,12 +298,12 @@ svg:hover {
.modal-header a, .modal-header a,
.modal-header a:visited { .modal-header a:visited {
color: var(--nav-background-color); color: var(--secondary);
} }
.modal-header a:hover, .modal-header a:hover,
.modal-header a:visited:hover { .modal-header a:visited:hover {
filter: brightness(1.5); color: var(--secondary-lighten);
} }
.userDoc { .userDoc {
@ -286,7 +334,7 @@ svg:hover {
.close:hover, .close:hover,
.close:focus { .close:focus {
color: var(--nav-background-color); color: var(--text-color-disable);
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
transform: rotate(90deg); transform: rotate(90deg);
@ -301,22 +349,27 @@ svg:hover {
} }
.modal-header { .modal-header {
background-color: var(--light-color);
color: var(--text-color); color: var(--text-color);
padding: 1px 15px; padding: 1px 15px;
background: var(--background-color-lighten);
} }
.modal-body { .modal-body {
padding: 2px 16px; padding: 2px 16px;
} }
.modal-body a {
display: block;
}
.modal-body img { .modal-body img {
position: relative; position: relative;
} }
nav { nav {
background-color: var(--nav-background-color); background-color: var(--primary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
color: var(--primary-text-contrast);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
height: auto; height: auto;
@ -349,33 +402,15 @@ nav span {
width: fit-content; width: fit-content;
} }
.button {
background-color: var(--nav-background-color);
border: 1px solid var(--light-color);
border-radius: var(--border-radius);
color: var(--light-color);
padding: .2em .3em;
transition: all 0.3s ease-in-out;
}
.button:hover {
background-color: var(--light-color);
border: 1px solid var(--nav-background-color);
color: var(--text-color);
}
nav a { nav a {
background-clip: text; background-clip: text;
background-image: linear-gradient(to right,
var(--background-color),
var(--background-color) 50%,
var(--light-color) 50%);
background-position: -100%; background-position: -100%;
background-size: 200% 100%; background-size: 200% 100%;
color: transparent; color: var(--text-color-inverse);
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold;
line-height: 1.5rem; line-height: 1.5rem;
margin: auto var(--margin); margin: auto var(--margin);
padding: 5px 0; padding: 5px 0;
@ -399,16 +434,10 @@ nav .active::before {
width: 0; width: 0;
} }
nav .active {
background-image: linear-gradient(to right,
var(--background-color),
var(--background-color) 50%,
var(--light-color) 50%);
}
nav a:hover, nav a:hover,
nav .active { nav .active {
background-position: 0; background-position: 0;
color: var(--text-color);
} }
nav a:hover::before, nav a:hover::before,
@ -417,22 +446,22 @@ nav .active::before {
} }
.login { .login {
border: 1px solid var(--light-color); border: 1px solid var(--background-color-lighten);
border-radius: var(--border-radius); border-radius: var(--border-radius);
color: var(--light-color); color: var(--text-color);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-size: 2rem; font-size: 2rem;
gap: 1rem; gap: 1rem;
margin: var(--margin) auto 0; margin: var(--margin) auto 0;
padding: 1rem; padding: 1rem;
width: 30vw; width: 40vw;
} }
input[type=text], input[type=text],
input[type=password], input[type=password],
select { select {
border: 1px solid var(--nav-background-color); border: 1px solid var(--background-color-lighten);
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
margin: 0 0 var(--margin) 0; margin: 0 0 var(--margin) 0;
@ -440,29 +469,121 @@ select {
width: 100%; width: 100%;
} }
button[type=submit] { input[type=text]:active,
height: 3em; input[type=password]:active,
margin: auto; select:active,
width: 30%; input[type=text]:focus,
input[type=password]:focus,
select:focus,
input[type=text]:focus-visible,
input[type=password]:focus-visible,
select:focus-visible,
textarea:focus-visible {
border: 1px solid red;
outline: none;
} }
button,
.button {
border-radius: var(--border-radius);
line-height: 2.5em;
height: 2.5em;
margin: 0 auto;
border-style: solid;
border-width: 1px;
width: 30%;
font-size: 2rem;
}
button[type=submit] {
background-color: var(--success);
border-color: var(--success-lighten);
color: var(--success-text-contrast);
}
button[type=submit]:hover {
background-color: var(--success-lighten);
border-color: var(--success-darken);
color: var(--success-text-contrast);
}
.button {
background-color: var(--primary-darken);
border-color: var(--primary-lighten);
color: var(--primary-text-contrast);
transition: all 0.3s ease-in-out;
text-decoration: none;
text-align: center;
}
.button:hover {
background-color: var(--primary-lighten);
border-color: var(--primary-darken);
}
.button-error {
background-color: var(--error);
border-color: var(--error-lighten);
color: var(--error-text-contrast);
}
.flex {
display: flex;
flex-flow: row wrap;
}
.readMore .button {
width: auto;
padding: .2em .3em;
line-height: inherit;
height: auto;
}
.checkbox { .checkbox {
display: flex; display: flex;
gap: 10px; gap: 10px;
justify-content: center; justify-content: center;
} }
.table-list {
font-size: 1.7rem;
margin: auto;
width: 80vw;
color: var(--background-color-lighten);
border-collapse: collapse;
th,
td {
padding: 0.5rem;
border: 1px solid var(--background-color-lighten);
}
th {
background-color: var(--background-color-lighten);
}
}
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.card-container:not(.icons) .card { .card-container:not(.icons) .card {
width: calc(100% / 1 - 20px); width: calc(100% / 1 - 20px);
} }
.modal-content { .modal-content {
width: 90%; width: 90%;
} }
.login { .login {
width: 80vw; width: 90vw;
}
.table-list {
margin: auto;
width: 100%;
} }
} }
@ -476,7 +597,7 @@ button[type=submit] {
} }
.login { .login {
width: 70vw; width: 80vw;
} }
} }
@ -490,7 +611,12 @@ button[type=submit] {
} }
.login { .login {
width: 60vw; width: 70vw;
}
.table-list {
margin: auto;
width: 100%;
} }
} }
@ -504,7 +630,7 @@ button[type=submit] {
} }
.login { .login {
width: 50vw; width: 60vw;
} }
} }
@ -518,7 +644,7 @@ button[type=submit] {
} }
.login { .login {
width: 40vw; width: 50vw;
} }
} }
@ -532,8 +658,14 @@ button[type=submit] {
} }
.login { .login {
width: 30vw; width: 40vw;
} }
.table-list {
margin: auto;
width: 70vw;
}
} }
.hide { .hide {

View file

@ -0,0 +1,14 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g>
<path fill-rule="evenodd"
d="M11.436 1.005A1.75 1.75 0 0113.902.79l.702.589a1.75 1.75 0 01.216 2.465l-5.704 6.798a4.75 4.75 0 01-1.497 1.187l-2.572 1.299a.75.75 0 01-1.056-.886l.833-2.759a4.75 4.75 0 01.908-1.68l5.704-6.798zm1.502.934a.25.25 0 00-.353.03l-.53.633 1.082.914.534-.636a.25.25 0 00-.031-.352l-.703-.59zm-.765 2.726l-1.082-.914-4.21 5.016a3.25 3.25 0 00-.621 1.15L5.933 11l1.01-.51a3.249 3.249 0 001.024-.812l4.206-5.013z"
clip-rule="evenodd"></path>
<path
d="M3.25 3.5a.75.75 0 00-.75.75v9.5c0 .414.336.75.75.75h9.5a.75.75 0 00.75-.75V9A.75.75 0 0115 9v4.75A2.25 2.25 0 0112.75 16h-9.5A2.25 2.25 0 011 13.75v-9.5A2.25 2.25 0 013.25 2H6a.75.75 0 010 1.5H3.25z">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,11 @@
<svg fill="#000000" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 260 260" enable-background="new 0 0 260 260"
xml:space="preserve">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
d="M65.829,33.826c8.837-0.019,16.016,7.128,16.035,15.965s-7.128,16.016-15.965,16.035s-16.016-7.128-16.035-15.965 S56.992,33.845,65.829,33.826z M208.061,183.361L178,170v-38c0-5.523-4.477-10-10-10s-10,4.477-10,10v58c0,2.209-1.791,4-4,4 s-4-1.791-4-4v-14c0-5.523-4.477-10-10-10s-10,4.477-10,10v31.496c0,4.227,1.339,8.345,3.825,11.763L162,258h52v-65.501 C214,188.547,211.673,184.966,208.061,183.361z M258,2H2v192h120v-18c0-9.925,8.075-18,18-18c3.697,0,7.138,1.121,10,3.04V132 c0-9.925,8.075-18,18-18s18,8.075,18,18v32.801l25.31,11.249c6.494,2.886,10.69,9.342,10.69,16.449V194h36V2z M242,161.302 L161.861,49.62l-38.659,57.238L98.026,81.762L18.036,162H18V18h224V161.302z">
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -21,10 +21,22 @@ function showReadMore(modal) {
divModal.style.display = "none"; divModal.style.display = "none";
} }
} }
window.onclick = function (event) {
if (event.target == divModal) {
divModal.style.display = "none";
}
}
const input = document.body;
input.onkeydown = function (event) {
if (event.key == "Escape") {
divModal.style.display = "none";
}
};
} }
function toggleFilter(element, filter) { function toggleFilter(element, filter) {
let filterListNav = document.getElementsByTagName('nav') let filterListNav = document.getElementsByTagName('nav')
for (var i = 0; i < filterListNav.length; i++) { for (var i = 0; i < filterListNav.length; i++) {
for (var i2 = 0; i2 < filterListNav[i].children.length; i2++) { for (var i2 = 0; i2 < filterListNav[i].children.length; i2++) {
@ -56,7 +68,15 @@ function switchTheme(e) {
let actualTheme = document.documentElement.getAttribute('data-theme'); let actualTheme = document.documentElement.getAttribute('data-theme');
if (actualTheme === null || actualTheme === 'light') { if (actualTheme === null || actualTheme === 'light') {
document.documentElement.setAttribute('data-theme', 'dark'); document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem('theme', 'dark');
} else { } else {
document.documentElement.setAttribute('data-theme', 'light'); document.documentElement.setAttribute('data-theme', 'light');
localStorage.setItem('theme', 'light');
} }
return false;
}
const currentTheme = localStorage.getItem('theme') ? localStorage.getItem('theme') : null;
if (currentTheme) {
document.documentElement.setAttribute('data-theme', currentTheme);
} }

2
public/assets/js/js-yaml.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,15 @@ use Symfony\Component\Yaml\Yaml;
use KTH\App; use KTH\App;
use KTH\HTMLGenerator\HTMLGenerator; use KTH\HTMLGenerator\HTMLGenerator;
use Login\Login; use Login\Login;
use Utils\CsrfToken;
/*
#############################################################
################ DO NOT EDIT THIS FILE ######################
################ USE data/config.yaml ######################
################ OR USE CONFIG PAGE ######################
#############################################################
*/
$defConfig['title'] = 'KT-HomePage'; $defConfig['title'] = 'KT-HomePage';
$defConfig['desc'] = 'Dashboard de Knah Tsaeb'; $defConfig['desc'] = 'Dashboard de Knah Tsaeb';
@ -38,44 +47,55 @@ if ($config['visibility'] === 'private') {
} }
} }
if (isset($_POST['settings'])) {
if (CsrfToken::validateToken($_POST['token'])) {
$KTH->saveUserConfig();
header("Location: /");
} else {
session_destroy();
header("Location: /");
}
exit();
}
if (isset($_POST['edit'])) {
if (CsrfToken::validateToken($_POST['token'])) {
$status = $KTH->saveServices();
if ($status['status'] === 'error') {
$error = $status['message'];
$content = $status['content'];
//header("Location: ?edit=1");
$_GET['edit']=1;
} else {
header("Location: /");
}
} else {
session_destroy();
header("Location: /");
}
}
if (isset($_GET['settings'])) { if (isset($_GET['settings'])) {
require('../template/default/settings.php'); require('../template/default/settings.php');
exit(); exit();
} }
if (isset($_POST['settings'])) { if (isset($_GET['edit'])) {
$KTH->saveUserConfig(); $services = $KTH->getServices();
header("Location: /"); require('../template/default/edit.php');
exit();
}
if (isset($_GET['editImg'])) {
require('../template/default/editImg.php');
exit(); exit();
} }
if ($KTH->cacheExist()) { if ($KTH->cacheExist()) {
echo file_get_contents('../cache/index.html'); echo file_get_contents('../cache/index.html');
} else { } else {
if (file_exists('../data/services.yaml')) { $services = $KTH->getServices();
$services = Yaml::parseFile('../data/services.yaml');
} else {
$services = [0 => [
'title' => 'Wikipedia',
'screenshot' => 'wikipedia.png',
'favicon' => 'wikipedia.png',
'link' => 'https://en.wikipedia.org/wiki/Dashboard_(computing)',
'appHome' => 'https://www.mediawiki.org/wiki/MediaWiki',
'location' => 'web',
'desc' => 'Wikipedia, the free encyclopedia',
'type' => 'webapp'
],
[
'title' => 'Awesome-Selfhosted',
'screenshot' => 'Awesome-Selfhosted.png',
'favicon' => 'Awesome-Selfhosted.ico',
'link' => 'https://awesome-selfhosted.net/',
'appHome' => 'https://www.mediawiki.org/wiki/MediaWiki',
'location' => 'web',
'desc' => 'A list of Free Software network services and web applications which can be hosted on your own servers',
'type' => 'webapp'
]];
}
$generator = new HTMLGenerator($services); $generator = new HTMLGenerator($services);
$userDoc = $generator->genUserDoc(); $userDoc = $generator->genUserDoc();
$menuData = $KTH->makeMenu($services); $menuData = $KTH->makeMenu($services);

View file

@ -22,7 +22,7 @@
</h3> </h3>
<?php if ($config['view'] !== 'icons') : ?> <?php if ($config['view'] !== 'icons') : ?>
<p class="readMore"> <p class="readMore">
<a class="button" onclick="showReadMore('<?= md5($service['link']); ?>')">More info</a> <a tabindex=0 class="button" onkeydown="showReadMore('<?= md5($service['link']); ?>')" onclick="showReadMore('<?= md5($service['link']); ?>')">More info</a>
</p> </p>
<?php endif; ?> <?php endif; ?>
</div> </div>
@ -31,7 +31,7 @@
<!-- Modal content --> <!-- Modal content -->
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<span class="close" id="close-<?= md5($service['link']); ?>">&times;</span> <a class="close" id="close-<?= md5($service['link']); ?>" tabindex=0 >&times;</a>
<h2> <h2>
<img class="favicon" loading="lazy" src="<?= $KTH->returnImg($service['favicon'], 'favicons'); ?>" alt="Favicon" height="32px" /> <img class="favicon" loading="lazy" src="<?= $KTH->returnImg($service['favicon'], 'favicons'); ?>" alt="Favicon" height="32px" />
<?= $service['title']; ?> <?= $service['title']; ?>

114
template/default/edit.php Normal file
View file

@ -0,0 +1,114 @@
<?php
use Symfony\Component\Yaml\Yaml;
use Utils\CsrfToken;
use Utils\Select;
use Utils\Debug;
if (!isset($error)) {
$error = null;
}
if (!isset($content)) {
$content = Yaml::dump($services, 2);
}
$breadcrumbs = ' / Edit';
$debug = new Debug;
require 'header.php';
require 'titleBar.php';
?>
<?php
/*<table class="table-list">
<thead>
<tr>
<th>Title</th>
<th>Link</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<?php foreach ($services as $service) : ?>
<tr>
<td><?= $service['title']; ?></td>
<td><?= $service['link']; ?></td>
<td>Delete</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>*/
?>
<form action="index.php" class="login" method="post">
<div class="alert" style="color: red;" id="alert">
<?= $error; ?>
</div>
<textarea rows="50" name="services" id="services" spellcheck="false"><?= $content; ?></textarea>
<input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>">
<input type="hidden" name="edit" value="1" />
<div class="flex">
<a class="button" href="?">Cancel</a>
<button type="submit">Save</button>
</div>
</form>
<!--
<form action="index.php" class="login" method="post">
<label for="title">Title</label>
<input type="text" name="title" id="title">
<label for="screenshot">Screenshot name</label>
<input type="text" name="screenshot" id="screenshot">
<label for="favicon">Favicon name</label>
<input type="text" name="favicon" id="favicon">
<label for="link">Link</label>
<input type="text" name="link" id="link">
<label for="appHome">Home application</label>
<input type="text" name="appHome" id="appHome">
<label for="desc">Desc</label>
<input type="text" name="desc" id="desc">
<label for="location">Location</label>
<input type="text" name="location" id="location">
<label for="type">Install instalation type</label>
<select name="type" id="type">
<option value="docker">Docker</option>
<option value="vm">Virtual machine</option>
<option value="webapp">Webapp</option>
<option value="redirection">Redirection</option>
</select>
<input type="hidden" name="token" value="">
<input type="hidden" name="settings" value="1" />
<button type="submit">Add</button>
</form>
!-->
<script src="assets/js/js-yaml.min.js"></script>
<script>
function validateYAML() {
console.log('here')
const inputText = document.getElementById('services').value;
try {
jsyaml.load(inputText);
document.getElementById('alert').textContent = "Your YAML is valid!";
} catch (e) {
document.getElementById('alert').textContent = "Your YAML is not valid!" + "\n\n" + "Error message: " + e.message;
}
}
document.getElementById('services').addEventListener('keyup', validateYAML);
</script>
<?php
require('footer.php');
?>

View file

@ -29,6 +29,7 @@ require 'titleBar.php';
<label for="colorScheme">Color scheme</label> <label for="colorScheme">Color scheme</label>
<select name="colorScheme" id="colorScheme"> <select name="colorScheme" id="colorScheme">
<option value="auto" <?= Select::isSelected('auto', $config['colorScheme']); ?>>Auto</option>
<option value="light" <?= Select::isSelected('light', $config['colorScheme']); ?>>Light</option> <option value="light" <?= Select::isSelected('light', $config['colorScheme']); ?>>Light</option>
<option value="dark" <?= Select::isSelected('dark', $config['colorScheme']); ?>>Dark</option> <option value="dark" <?= Select::isSelected('dark', $config['colorScheme']); ?>>Dark</option>
</select> </select>
@ -42,12 +43,16 @@ require 'titleBar.php';
<p class="checkbox"> <p class="checkbox">
<label for="reimport">Reimport images and user files</label> <label for="reimport">Reimport images and user files</label>
<input type="checkbox" name="reimport" id="reimport" value="1"/> <input type="checkbox" name="reimport" id="reimport" value="1" />
</p> </p>
<input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>"> <input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>">
<input type="hidden" name="settings" value="1" /> <input type="hidden" name="settings" value="1" />
<button type="submit">Save</button>
<div class="flex">
<a class="button" href="?">Cancel</a>
<button type="submit">Save</button>
</div>
</form> </form>
<?php <?php

View file

@ -23,6 +23,25 @@
</g> </g>
</g> </g>
</svg></a> </svg></a>
<a title="Services" href="index.php?edit=1">
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g>
<path fill-rule="evenodd" d="M11.436 1.005A1.75 1.75 0 0113.902.79l.702.589a1.75 1.75 0 01.216 2.465l-5.704 6.798a4.75 4.75 0 01-1.497 1.187l-2.572 1.299a.75.75 0 01-1.056-.886l.833-2.759a4.75 4.75 0 01.908-1.68l5.704-6.798zm1.502.934a.25.25 0 00-.353.03l-.53.633 1.082.914.534-.636a.25.25 0 00-.031-.352l-.703-.59zm-.765 2.726l-1.082-.914-4.21 5.016a3.25 3.25 0 00-.621 1.15L5.933 11l1.01-.51a3.249 3.249 0 001.024-.812l4.206-5.013z" clip-rule="evenodd"></path>
<path d="M3.25 3.5a.75.75 0 00-.75.75v9.5c0 .414.336.75.75.75h9.5a.75.75 0 00.75-.75V9A.75.75 0 0115 9v4.75A2.25 2.25 0 0112.75 16h-9.5A2.25 2.25 0 011 13.75v-9.5A2.25 2.25 0 013.25 2H6a.75.75 0 010 1.5H3.25z"></path>
</g>
</g>
</svg></a>
<!--<a title="Images" href="index.php?editImg=1">
<svg fill="#000000" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 260 260" enable-background="new 0 0 260 260" xml:space="preserve">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M65.829,33.826c8.837-0.019,16.016,7.128,16.035,15.965s-7.128,16.016-15.965,16.035s-16.016-7.128-16.035-15.965 S56.992,33.845,65.829,33.826z M208.061,183.361L178,170v-38c0-5.523-4.477-10-10-10s-10,4.477-10,10v58c0,2.209-1.791,4-4,4 s-4-1.791-4-4v-14c0-5.523-4.477-10-10-10s-10,4.477-10,10v31.496c0,4.227,1.339,8.345,3.825,11.763L162,258h52v-65.501 C214,188.547,211.673,184.966,208.061,183.361z M258,2H2v192h120v-18c0-9.925,8.075-18,18-18c3.697,0,7.138,1.121,10,3.04V132 c0-9.925,8.075-18,18-18s18,8.075,18,18v32.801l25.31,11.249c6.494,2.886,10.69,9.342,10.69,16.449V194h36V2z M242,161.302 L161.861,49.62l-38.659,57.238L98.026,81.762L18.036,162H18V18h224V161.302z"></path>
</g>
</svg></a>-->
<a title="Help" href="#" onclick="showReadMore('userDoc')"> <a title="Help" href="#" onclick="showReadMore('userDoc')">
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve"> <svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g> <g id="SVGRepo_bgCarrier" stroke-width="0"></g>