Add visual editor for services.yaml

Some adjust CSS
This commit is contained in:
Knah Tsaeb 2024-07-16 10:19:59 +02:00
parent 58ec0cb655
commit 0d1b22d2f9
13 changed files with 637 additions and 71 deletions

View file

@ -2,7 +2,7 @@ FROM debian:stable-slim
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"
ENV DEBIAN_FRONTEND=noninteractive
@ -30,7 +30,7 @@ WORKDIR /var/www/
ENTRYPOINT "start.sh"
# Build image
# docker buildx build -t nofu:0.2.1 .
# docker buildx build -t nofu:0.3.0 .
# 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 /opt/nofu:/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.3.0

View file

@ -1,6 +1,6 @@
# 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
@ -67,13 +67,13 @@ wget https://forge.leslibres.org/Knah-Tsaeb/Nofu/raw/branch/main/Dockerfile
```
```shell
docker buildx build -t nofu:0.2.1 .
docker buildx build -t nofu:0.3.0 .
```
#### Run
```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
@ -105,7 +105,7 @@ You can find many icon for your app in [Dashboard-Icons](https://github.com/walk
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
@ -151,7 +151,11 @@ Replace the default data direcory by your backup, go to settings pages and check
## 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/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/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>
@ -162,6 +166,17 @@ Replace the default data direcory by your backup, go to settings pages and check
* <a href="https://www.svgrepo.com/svg/306622/qemu"><img alt="docker icon" src="public/assets/icons/vm.svg" width="24"> svgrepo.com (Logo 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>
### 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 :-)

View file

@ -3,6 +3,7 @@
namespace KTH;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;
class App {
@ -30,7 +31,11 @@ class App {
* @return array An array containing the unique types and locations of the services.
*/
public function makeMenu(array $services): array {
$menu = [];
foreach ($services as $service) {
if(!is_array($service)){
return $menu;
}
$menu['type'][] = $service['type'];
$menu['location'][] = $service['location'];
}
@ -47,11 +52,11 @@ class App {
*
* @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';
$permitType = ['favicons', 'big_favicons', 'screenshots', 'thumbs'];
if (empty($file)) {
if (empty($file) || is_null($file)) {
return $defaultFile;
}
@ -118,6 +123,24 @@ class App {
$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.
*
@ -172,4 +195,35 @@ class App {
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
*/
function n_print(mixed $data, string $name = ''): void {
static function n_print(mixed $data, string $name = ''): void {
error_reporting(-1);
$aBackTrace = debug_backtrace();
echo '<h2>', $name, '</h2>';
@ -21,4 +21,4 @@ class Debug {
echo '<pre>', htmlentities(print_r($data, 1)), '</pre>';
echo '</fieldset><br />';
}
}
}

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

@ -40,6 +40,18 @@
color: var(--light-color);
border: 1px solid var(--light-color);
}
.titleBar .linkList {
float: right;
svg {
fill: var(--nav-background-color);
}
svg:hover {
fill: var(--light-color);
}
}
}
*,
@ -58,31 +70,6 @@ body {
margin: var(--margin);
}
.titleBar {
text-align: center;
}
.titleBar .linkList {
float: right;
svg {
height: 2.5em;
width: 2.5em;
margin: var(--margin);
}
a {
text-align: none;
}
}
.titleBar div {
margin: 0;
padding: 0;
line-height: 2.5em;
height: 2.5em;
}
h1 {
color: var(--light-color);
font-size: 2.5rem;
@ -101,6 +88,33 @@ svg:hover {
fill: var(--light-color);
}
.titleBar {
text-align: center;
}
.titleBar .linkList {
float: right;
svg {
height: 2.5em;
width: 2.5em;
margin: var(--margin);
fill: var(--nav-background-color);
}
svg:hover {
background-color: inherit;
fill: var(--text-color);
}
}
.titleBar div {
margin: 0;
padding: 0;
line-height: 2.5em;
height: 2.5em;
}
.card-container {
display: flex;
flex-flow: row wrap;
@ -426,7 +440,7 @@ nav .active::before {
gap: 1rem;
margin: var(--margin) auto 0;
padding: 1rem;
width: 30vw;
width: 40vw;
}
input[type=text],
@ -452,17 +466,42 @@ button[type=submit] {
justify-content: center;
}
.table-list {
font-size: 1.7rem;
margin: auto;
width: 80vw;
color: var(--light-color);
border-collapse: collapse;
th,
td {
padding: 0.5rem;
border: 1px solid var(--light-color);
}
th {
background-color: var(--nav-background-color);
}
}
@media only screen and (max-width: 600px) {
.card-container:not(.icons) .card {
width: calc(100% / 1 - 20px);
}
.modal-content {
width: 90%;
}
.login {
width: 80vw;
width: 90vw;
}
.table-list {
margin: auto;
width: 100%;
}
}
@ -476,7 +515,7 @@ button[type=submit] {
}
.login {
width: 70vw;
width: 80vw;
}
}
@ -490,7 +529,12 @@ button[type=submit] {
}
.login {
width: 60vw;
width: 70vw;
}
.table-list {
margin: auto;
width: 100%;
}
}
@ -504,7 +548,7 @@ button[type=submit] {
}
.login {
width: 50vw;
width: 60vw;
}
}
@ -518,7 +562,7 @@ button[type=submit] {
}
.login {
width: 40vw;
width: 50vw;
}
}
@ -532,8 +576,14 @@ button[type=submit] {
}
.login {
width: 30vw;
width: 40vw;
}
.table-list {
margin: auto;
width: 70vw;
}
}
.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

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,7 @@ use Symfony\Component\Yaml\Yaml;
use KTH\App;
use KTH\HTMLGenerator\HTMLGenerator;
use Login\Login;
use Utils\CsrfToken;
$defConfig['title'] = 'KT-HomePage';
$defConfig['desc'] = 'Dashboard de Knah Tsaeb';
@ -38,44 +39,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'])) {
require('../template/default/settings.php');
exit();
}
if (isset($_POST['settings'])) {
$KTH->saveUserConfig();
header("Location: /");
if (isset($_GET['edit'])) {
$services = $KTH->getServices();
require('../template/default/edit.php');
exit();
}
if (isset($_GET['editImg'])) {
require('../template/default/editImg.php');
exit();
}
if ($KTH->cacheExist()) {
echo file_get_contents('../cache/index.html');
} else {
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'
]];
}
$services = $KTH->getServices();
$generator = new HTMLGenerator($services);
$userDoc = $generator->genUserDoc();
$menuData = $KTH->makeMenu($services);

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

@ -0,0 +1,110 @@
<?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"><?= $content; ?></textarea>
<input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>">
<input type="hidden" name="edit" value="1" />
<button type="submit">Save</button>
</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

@ -23,6 +23,25 @@
</g>
</g>
</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')">
<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>