more tests and some fixes

This commit is contained in:
Dennis Eichhorn 2024-04-24 03:20:46 +00:00
parent e76984148b
commit c04df74e70
25 changed files with 7 additions and 1733 deletions

View File

@ -13,10 +13,15 @@ on:
- cron: '0 0 1,15 * *'
jobs:
general_module_workflow:
general_module_workflow_php:
uses: Karaka-Management/Karaka/.github/workflows/php_template.yml@develop
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_PAT: ${{ secrets.GH_PAT }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
general_module_workflow_js:
uses: Karaka-Management/Karaka/.github/workflows/js_template.yml@develop
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_PAT: ${{ secrets.GH_PAT }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,461 +0,0 @@
<?php
/**
* Jingga
*
* PHP Version 8.2
*
* @package Web\Timerecording
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace Web\Timerecording;
use Model\CoreSettings;
use Modules\Admin\Models\AccountMapper;
use Modules\Admin\Models\LocalizationMapper;
use Modules\Admin\Models\SettingsEnum;
use Modules\Organization\Models\UnitMapper;
use Modules\Profile\Models\ProfileMapper;
use phpOMS\Account\Account;
use phpOMS\Account\AccountManager;
use phpOMS\Account\PermissionType;
use phpOMS\Asset\AssetType;
use phpOMS\Auth\Auth;
use phpOMS\DataStorage\Cache\CachePool;
use phpOMS\DataStorage\Cookie\CookieJar;
use phpOMS\DataStorage\Database\Connection\ConnectionAbstract;
use phpOMS\DataStorage\Database\DatabasePool;
use phpOMS\DataStorage\Database\DatabaseStatus;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\DataStorage\Session\HttpSession;
use phpOMS\Dispatcher\Dispatcher;
use phpOMS\Event\EventManager;
use phpOMS\Localization\L11nManager;
use phpOMS\Message\Http\HttpRequest;
use phpOMS\Message\Http\HttpResponse;
use phpOMS\Message\Http\RequestMethod;
use phpOMS\Message\Http\RequestStatusCode;
use phpOMS\Model\Html\Head;
use phpOMS\Module\ModuleManager;
use phpOMS\Router\RouteVerb;
use phpOMS\Router\WebRouter;
use phpOMS\System\File\PathException;
use phpOMS\Uri\UriFactory;
use phpOMS\Views\View;
use Web\WebApplication;
/**
* Application class.
*
* @package Web\Timerecording
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
* @codeCoverageIgnore
*/
final class Application
{
/**
* WebApplication.
*
* @var WebApplication
* @since 1.0.0
*/
private WebApplication $app;
/**
* Temp config.
*
* @var array{db:array{core:array{masters:array{select:array{db:string, host:string, port:int, login:string, password:string, database:string}}}}, log:array{file:array{path:string}}, app:array{path:string, default:string, domains:array}, page:array{root:string, https:bool}, language:string[]}
* @since 1.0.0
*/
private array $config;
/**
* Constructor.
*
* @param WebApplication $app WebApplication
* @param array{db:array{core:array{masters:array{select:array{db:string, host:string, port:int, login:string, password:string, database:string}}}}, log:array{file:array{path:string}}, app:array{path:string, default:string, domains:array}, page:array{root:string, https:bool}, language:string[]} $config Application config
*
* @since 1.0.0
*/
public function __construct(WebApplication $app, array $config)
{
$this->app = $app;
$this->app->appName = 'Timerecording';
$this->config = $config;
UriFactory::setQuery('/app', \strtolower($this->app->appName));
}
/**
* Rendering timerecording.
*
* @param HttpRequest $request Request
* @param HttpResponse $response Response
*
* @return void
*
* @since 1.0.0
*/
public function run(HttpRequest $request, HttpResponse $response) : void
{
$this->app->l11nManager = new L11nManager();
$pageView = new TimerecordingView($this->app->l11nManager, $request, $response);
$head = new Head();
$pageView->head = $head;
$response->set('Content', $pageView);
/* Timerecording only allows GET */
if ($request->getMethod() !== RequestMethod::GET) {
$this->create406Response($response, $pageView);
return;
}
$this->app->dbPool = new DatabasePool();
$this->app->router = new WebRouter();
$this->app->router->importFromFile(__DIR__ . '/Routes.php');
$this->app->router->add(
'/timerecording/e403',
function() use ($request, $response) {
$view = new View($this->app->l11nManager, $request, $response);
$view->setTemplate('/Web/Timerecording/Error/403_inline');
$response->header->status = RequestStatusCode::R_403;
return $view;
},
RouteVerb::GET
);
$this->app->sessionManager = new HttpSession(60);
$this->app->cookieJar = new CookieJar();
$this->app->moduleManager = new ModuleManager($this->app, __DIR__ . '/../../Modules/');
$this->app->dispatcher = new Dispatcher($this->app);
$this->app->dbPool->create('select', $this->config['db']['core']['masters']['select']);
/* Database OK? */
if ($this->app->dbPool->get()->getStatus() !== DatabaseStatus::OK) {
$this->create503Response($response, $pageView);
return;
}
/* CSRF token OK? */
if ($request->hasData('CSRF')
&& !\hash_equals($this->app->sessionManager->data['CSRF'] ?? '', $request->getDataString('CSRF'))
) {
$response->header->status = RequestStatusCode::R_403;
return;
}
/** @var ConnectionAbstract $con */
$con = $this->app->dbPool->get();
DataMapperFactory::db($con);
$this->app->cachePool = new CachePool();
$this->app->appSettings = new CoreSettings();
$this->app->eventManager = new EventManager($this->app->dispatcher);
$this->app->accountManager = new AccountManager($this->app->sessionManager);
$this->app->l11nServer = LocalizationMapper::get()->where('id', 1)->execute();
$this->app->unitId = $this->getApplicationOrganization($request, $this->config);
$pageView->setData('unitId', $this->app->unitId);
$aid = Auth::authenticate($this->app->sessionManager);
$request->header->account = $aid;
$response->header->account = $aid;
$account = $this->loadAccount($request);
if ($account->id > 0) {
$response->header->l11n = $account->l11n;
} elseif (isset($this->app->sessionManager->data['language'])
&& $response->header->l11n->language !== $this->app->sessionManager->data['language']
) {
$response->header->l11n
->loadFromLanguage(
$this->app->sessionManager->data['language'],
$this->app->sessionManager->data['country'] ?? '*'
);
} else {
$this->app->setResponseLanguage($request, $response, $this->config);
}
UriFactory::setQuery('/lang', $response->header->l11n->language);
$this->loadLanguageFromPath(
$response->header->l11n->language,
__DIR__ . '/lang/' . $response->header->l11n->language . '.lang.php'
);
$response->header->set('content-language', $response->header->l11n->language, true);
/* Create html head */
$this->initResponseHead($head, $request, $response);
/* Handle not logged in */
if ($account->id < 1) {
$this->createLoggedOutResponse($response, $head, $pageView);
return;
}
/* No reading permission */
if (!$account->hasPermission(PermissionType::READ, $this->app->unitId, $this->app->appId, 'Dashboard')) {
$this->create403Response($response, $pageView);
return;
}
$this->app->moduleManager->initRequestModules($request);
$this->createDefaultPageView($request, $response, $pageView);
$dispatched = $this->app->dispatcher->dispatch(
$this->app->router->route(
$request->uri->getRoute(),
$request->getDataString('CSRF'),
$request->getRouteVerb(),
$this->app->appId,
$this->app->unitId,
$account,
$request->getData()
),
$request,
$response
);
$pageView->addData('dispatch', $dispatched);
}
/**
* Get application organization
*
* @param HttpRequest $request Client request
* @param array $config App config
*
* @return int Organization id
*
* @since 1.0.0
*/
private function getApplicationOrganization(HttpRequest $request, array $config) : int
{
return (int) (
$request->getData('u') ?? (
$config['domains'][$request->uri->host]['org'] ?? $this->app->appSettings->get(
SettingsEnum::DEFAULT_UNIT
) ?? 1
)
);
}
/**
* Create 406 response.
*
* @param HttpResponse $response Response
* @param View $pageView View
*
* @return void
*
* @since 1.0.0
*/
private function create406Response(HttpResponse $response, View $pageView) : void
{
$response->header->status = RequestStatusCode::R_406;
$pageView->setTemplate('/Web/Timerecording/Error/406');
$this->loadLanguageFromPath(
$response->header->l11n->language,
__DIR__ . '/Error/lang/' . $response->header->l11n->language . '.lang.php'
);
}
/**
* Create 406 response.
*
* @param HttpResponse $response Response
* @param View $pageView View
*
* @return void
*
* @since 1.0.0
*/
private function create503Response(HttpResponse $response, View $pageView) : void
{
$response->header->status = RequestStatusCode::R_503;
$pageView->setTemplate('/Web/Timerecording/Error/503');
$this->loadLanguageFromPath(
$response->header->l11n->language,
__DIR__ . '/Error/lang/' . $response->header->l11n->language . '.lang.php'
);
}
/**
* Load theme language from path
*
* @param string $language Language name
* @param string $path Language path
*
* @return void
*
* @throws PathException
*
* @since 1.0.0
*/
private function loadLanguageFromPath(string $language, string $path) : void
{
/* Load theme language */
if (($absPath = \realpath($path)) === false) {
throw new PathException($path);
}
/** @noinspection PhpIncludeInspection */
$themeLanguage = include $absPath;
$this->app->l11nManager->loadLanguage($language, '0', $themeLanguage);
}
/**
* Load permission
*
* @param HttpRequest $request Current request
*
* @return Account
*
* @since 1.0.0
*/
private function loadAccount(HttpRequest $request) : Account
{
$account = AccountMapper::getWithPermissions($request->header->account);
$this->app->accountManager->add($account);
return $account;
}
/**
* Create 406 response.
*
* @param HttpResponse $response Response
* @param View $pageView View
*
* @return void
*
* @since 1.0.0
*/
private function create403Response(HttpResponse $response, View $pageView) : void
{
$response->header->status = RequestStatusCode::R_403;
$pageView->setTemplate('/Web/Timerecording/Error/403');
$this->loadLanguageFromPath(
$response->header->l11n->language,
__DIR__ . '/Error/lang/' . $response->header->l11n->language . '.lang.php'
);
}
/**
* Initialize response head
*
* @param Head $head Head to fill
* @param HttpRequest $request Request
* @param HttpResponse $response Response
*
* @return void
*
* @since 1.0.0
*/
private function initResponseHead(Head $head, HttpRequest $request, HttpResponse $response) : void
{
/* Load assets */
$head->addAsset(AssetType::CSS, 'Resources/fonts/googleicons/styles.css', ['defer']);
$head->addAsset(AssetType::CSS, 'cssOMS/styles.css?v=1.0.0', ['defer']);
$head->addAsset(AssetType::CSS, 'Web/{APPNAME}/css/frontend.css?v=1.0.0', ['defer']);
// Framework
$head->addAsset(AssetType::JS, 'jsOMS/UnhandledException.js?v=1.0.0');
$head->addAsset(AssetType::JS, 'Web/Timerecording/js/timerecording.js?v=1.0.0', ['type' => 'module']);
$head->addAsset(AssetType::JSLATE, 'Modules/Navigation/Controller.js?v=1.0.0', ['type' => 'module']);
$script = '';
$response->header->set(
'content-security-policy',
'base-uri \'self\'; script-src \'self\' blob: \'sha256-'
. \base64_encode(\hash('sha256', $script, true))
. '\'; worker-src \'self\'',
true
);
if ($request->hasData('debug')) {
$head->addAsset(AssetType::CSS, 'cssOMS/debug.css?v=1.0.0');
}
$css = \file_get_contents(__DIR__ . '/css/timerecording-small.css');
if ($css === false) {
$css = '';
}
$css = \preg_replace('!\s+!', ' ', $css);
$head->setStyle('core', $css ?? '');
$head->title = 'Karaka Timerecording';
}
/**
* Create logged out response
*
* @param HttpResponse $response Response
* @param Head $head Head to fill
* @param View $pageView View
*
* @return void
*
* @since 1.0.0
*/
private function createLoggedOutResponse(HttpResponse $response, Head $head, View $pageView) : void
{
$response->header->status = RequestStatusCode::R_403;
$pageView->setTemplate('/Web/Timerecording/login');
$head->addAsset(AssetType::JS, 'Web/Timerecording/js/login.js', ['type' => 'module']);
}
/**
* Create default page view
*
* @param HttpRequest $request Request
* @param HttpResponse $response Response
* @param TimerecordingView $pageView View
*
* @return void
*
* @since 1.0.0
*/
private function createDefaultPageView(HttpRequest $request, HttpResponse $response, TimerecordingView $pageView) : void
{
$pageView->setOrganizations(UnitMapper::getAll()->execute());
$pageView->profile = ProfileMapper::get()->where('account', $request->header->account)->executeGetArray();
$pageView->setData('nav', $this->getNavigation($request, $response));
$pageView->setTemplate('/Web/Timerecording/index');
}
/**
* Create navigation
*
* @param HttpRequest $request Request
* @param HttpResponse $response Response
*
* @return View
*
* @since 1.0.0
*/
private function getNavigation(HttpRequest $request, HttpResponse $response) : View
{
/** @var \Modules\Navigation\Controller\TimerecordingController $navController */
$navController = $this->app->moduleManager->get('Navigation');
$navController->loadLanguage($request, $response);
return $navController->getView($request, $response);
}
}

View File

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
return [
'^/timerecording$' => [
0 => [
'dest' => '\Modules\HumanResourceTimeRecording\Controller\TimerecordingController:viewDashboard',
'verb' => 1,
'active' => true,
'permission' => [
'module' => 'HumanResourceTimeRecording',
'type' => 2,
'state' => 1,
],
],
],
'^.*/timerecording/dashboard(\?.*$|$)' => [
0 => [
'dest' => '\Modules\HumanResourceTimeRecording\Controller\TimerecordingController:viewDashboard',
'verb' => 1,
'active' => true,
'permission' => [
'module' => 'HumanResourceTimeRecording',
'type' => 2,
'state' => 1,
],
],
],
];

View File

@ -1,190 +0,0 @@
:root {
--main-bg: rgb(46, 26, 90);
--main-bg-hl: rgb(158, 81, 197);
--iborder: rgba(166, 135, 232, .4);
--iborder-active: rgba(166, 135, 232, .7);
--ipt-c: rgb(116, 67, 161);
--ipt-c-active: rgb(94, 52, 133);
--ipt-bg: rgba(255, 255, 255);
--ipt-bg-active: rgba(255, 255, 255);
--ipt-ico-c: rgba(166, 135, 232, .6);
--ipt-ico-c-active: rgba(166, 135, 232, 1);
--btn-main-bg: rgba(166, 135, 232, .6);
--btn-main-bg-active: rgba(166, 135, 232, .8);
--btn-main-c: rgba(255, 255, 255, .9);
--txt-on-bg-c: rgba(255, 255, 255, 0.7);
--txt-on-bg-c-2: rgba(255, 255, 255, 0.85);
--nav-cat-bg: rgb(43, 58, 101);
--nav-cat-bg-hl: rgb(113, 43, 145);
--nav-cat-bg-hover: rgb(120, 50, 153);
--nav-sub-bg: rgb(72, 39, 102);
--nav-sub-bg-hl: rgb(94, 52, 133);
--nav-sub-bg-hover: rgb(116, 67, 161);
--nav-head-bg: rgb(72, 39, 102);
--nav-head-bg-hl: rgb(94, 52, 133);
--nav-head-bg-hover: rgb(116, 67, 161);
--nav-content-hover: rgb(177, 97, 218);
--ff: 'Arial', Helvetica, sans-serif;
--btn-bg: rgb(158, 81, 197);
--btn-bg-hover: rgb(177, 97, 218);
--tcaption-bg: rgb(255, 255, 255);
--thead-bg: rgb(236, 232, 255);
--trow-bg: rgb(255, 255, 255);
--trow-bg-alt: rgb(255, 255, 255);
--trow-bg-hover: rgb(220, 211, 255);
--link-c: rgb(72, 39, 102);
--lhover: rgb(158, 81, 197);
--badge-size: .55rem;
--badge-c: rgb(255, 255, 255);
--badge-bg: rgb(158, 81, 197);
--bborder: rgb(218, 218, 218); }
html, body {
width: 100%;
height: 100%;
min-width: 100%;
max-width: 100%;
overflow: hidden;
font-family: var(--ff);
color: #000; }
body {
display: flex; flex-direction: column;}
header {
background: #f8f8f8;
border-bottom: 1px solid var(--bborder);
padding: 1rem;
box-sizing: border-box;
display: flex;
align-items: center;
flex-flow: row;
flex: 0; }
header > form {
display: flex;
flex: 1;
padding: 0 5px 0 5px;
max-width: 800px; }
header .inputWrapper {
flex: 1; }
header input[type=text] {
width: 100%;
background: white;
border: 1px solid var(--iborder);
text-shadow: none;
box-shadow: none;
transition: border 500ms ease-out;
outline: none;
box-sizing: border-box;
padding-left: 2rem; }
#logo {
flex: 1;
text-align: right; }
#logo select {
background: none;
color: rgba(255, 255, 255, 0.8);
font-size: .8rem; }
.ham-trigger {
display: flex;
color: #000;
align-items: center;
flex: 0;
margin-right: 5px; }
.ham-trigger i {
font-size: 1.5rem; }
nav .ham-trigger {
color: var(--txt-on-bg-c-2);
margin: 0 0 0 5px;
display: none; }
#t-nav-container {
margin-left: auto; }
#content {
flex: 1;
padding-left: 1rem;
overflow-y: auto; }
#t-nav {
font-size: .8rem;
color: #000;
font-weight: bold; }
#t-nav a {
padding: 0 5px 0 5px;
line-height: 25px; }
#t-nav i {
margin-right: 5px; }
#t-nav li {
display: inline; }
#t-nav li:hover {
color: var(--lhover); }
main {
display: flex;
flex-direction: column;
height: 100%;
background: #f1f1f1;
flex: 1;
box-sizing: border-box; }
#dim {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
opacity: 0.5;
z-index: 5; }
#u-box {
display: flex;
align-items: center;
padding: 0 1rem 0 1rem;
height: 60px;
border-bottom: 1px solid var(--iborder);
box-sizing: border-box; }
#u-box img {
width: 40px;
height: 40px;
border-radius: 50%; }
#u-box a {
display: inline-block; }
#app-message-container {
position: absolute;
margin: 0 auto;
right: 0;
top: 0;
padding: 85px 10px 0 0;
}
.log-msg {
z-index: 11;
position: relative;
margin: 0 auto;
right: 0;
top: 0;
margin-bottom: 10px;
}
@media only screen and (max-width: 500px) {
nav .ham-trigger {
display: flex; } }
@media only screen and (max-width: 600px) {
header {
flex-flow: column;
height: auto;
padding: 1rem; }
header form {
width: 100%; }
#t-nav-container {
order: -1;
margin-bottom: .5rem; } }
@media only screen and (max-width: 1000px) {
#t-nav .link {
display: none; } }

View File

@ -1,153 +0,0 @@
@import "backend_vars";
html, body {
width: 100%;
height: 100%;
min-width: 100%;
max-width: 100%;
overflow: hidden;
font-family: var(--ff);
color: #000;
}
body {
display: flex;
flex-direction: column;
}
header {
background: rgb(248, 248, 248);
border-bottom: 1px solid var(--bborder);
padding: 1rem;
box-sizing: border-box;
display: flex;
align-items: center;
flex-flow: row;
flex: 0;
> form {
display: flex;
flex: 1;
padding: 0 5px 0 5px;
max-width: 800px;
}
.inputWrapper {
flex: 1;
}
input[type=text] {
width: 100%;
background: rgba(255, 255, 255, 1);
border: 1px solid var(--iborder);
text-shadow: none;
box-shadow: none;
transition : border 500ms ease-out;
outline: none;
box-sizing: border-box;
padding-left: 2rem;
}
}
#logo {
flex: 1;
text-align: right;
select {
background: none;
color: rgba(255, 255, 255, 0.8);
font-size: .8rem;
}
}
#t-nav-container {
margin-left: auto;
}
#content {
flex: 1;
padding-left: 1rem;
overflow-y: auto;
}
#t-nav {
font-size: .8rem;
color: #000;
font-weight: bold;
a {
padding: 0 5px 0 5px;
line-height: 25px;
}
i {
margin-right: 5px;
}
li {
display: inline;
&:hover {
color: var(--lhover);
}
}
}
main {
display: flex;
flex-direction: column;
height: 100%;
background: #f1f1f1;
flex: 1;
box-sizing: border-box;
}
#dim {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
opacity: 0.5;
z-index: 5;
}
#u-box {
display: flex;
align-items: center;
padding: 0 1rem 0 1rem;
height: 60px;
border-bottom: 1px solid var(--iborder);
box-sizing: border-box;
img {
width: 40px;
height: 40px;
border-radius: 50%;
}
a {
display: inline-block;
}
}
#app-message-container {
position: absolute;
margin: 0 auto;
right: 0;
top: 0;
padding: 85px 10px 0 0;
.log-msg {
z-index: 11;
position: relative;
margin: 0 auto;
right: 0;
top: 0;
margin-bottom: 10px;
}
}
@import "timerecording_media";

View File

@ -1,39 +0,0 @@
@media only screen and (min-width: 1101px) {
.b-1 {
width: 31.5%;
width: calc(33.3% - 10px); }
.b-2 {
width: 48.5%;
width: calc(50% - 10px); }
.b-3 {
width: 63%;
width: calc(66.6% - 10px); } }
@media only screen and (min-width: 801px) and (max-width: 1100px) {
.b-1 {
width: 48.5%;
width: calc(50% - 10px); } }
@media only screen and (max-width: 1100px) {
.b-2, .b-3 {
width: 100%;
width: calc(100% - 10px); } }
@media only screen and (min-width: 801px) {
.b-4 {
width: 48.5%;
width: calc(50% - 10px); } }
@media only screen and (max-width: 800px) {
.b-1, .b-3 {
width: 100%;
width: calc(100% - 10px); }
#logo {
display: none; }
#s-nav {
width: 100%; }
#s-bar input[type=text] {
margin-left: 35px;
width: 80%;
width: calc(100% - 90px); } }

View File

@ -1,22 +0,0 @@
@media only screen and (max-width: 600px) {
header {
flex-flow: column;
height: auto;
padding: 1rem;
form {
width: 100%;
}
}
nav {
order: -1;
margin-bottom: .5rem;
}
}
@media only screen and (max-width: 1000px) {
nav .link {
display: none;
}
}

View File

@ -1,24 +0,0 @@
:root {
--main-bg: rgb(46, 26, 90);
--main-bg-hl: rgb(158, 81, 197);
--iborder: rgba(166, 135, 232, .4);
--iborder-active: rgba(166, 135, 232, .7);
--ipt-c: rgba(166, 135, 232, .6);
--ipt-c-active: rgba(166, 135, 232, .8);
--ipt-ico-c: rgba(166, 135, 232, .6);
--ipt-ico-c-active: rgba(166, 135, 232, 1);
--btn-main-bg: rgba(166, 135, 232, .6);
--btn-main-bg-active: rgba(166, 135, 232, .8);
--btn-main-c: rgba(255, 255, 255, .9);
--txt-on-bg-c: rgba(255, 255, 255, 0.7);
--nav-cat-bg: rgb(43, 58, 101);
--nav-cat-bg-hl: rgb(113, 43, 145);
--nav-cat-bg-hover: rgb(120, 50, 153);
--nav-sub-bg: rgb(72, 39, 102);
--nav-sub-bg-hl: rgb(94, 52, 133);
--nav-sub-bg-hover: rgb(116, 67, 161);
--nav-head-bg: rgb(72, 39, 102);
--nav-head-bg-hl: rgb(94, 52, 133);
--nav-head-bg-hover: rgb(116, 67, 161);
--ff: 'Arial', Helvetica, sans-serif;
--bborder-color: rgb(202, 202, 202); }

View File

@ -1,55 +0,0 @@
:root {
--main-bg: rgb(46, 26, 90);
--main-bg-hl: rgb(158, 81, 197);
--iborder: rgba(166, 135, 232, .4);
--iborder-active: rgba(166, 135, 232, .7);
--ipt-c: rgb(116, 67, 161);
--ipt-c-active: rgb(94, 52, 133);
--ipt-bg: rgba(255, 255, 255);
--ipt-bg-active: rgba(255, 255, 255);
--ipt-ico-c: rgba(166, 135, 232, .6);
--ipt-ico-c-active: rgba(166, 135, 232, 1);
--btn-main-bg: rgba(166, 135, 232, .6);
--btn-main-bg-active: rgba(166, 135, 232, .8);
--btn-main-c: rgba(255, 255, 255, .9);
--txt-on-bg-c: rgba(255, 255, 255, 0.7);
--txt-on-bg-c-2: rgba(255, 255, 255, 0.85);
--nav-cat-bg: rgb(43, 58, 101);
--nav-cat-bg-hl: rgb(113, 43, 145);
--nav-cat-bg-hover: rgb(120, 50, 153);
--nav-sub-bg: rgb(72, 39, 102);
--nav-sub-bg-hl: rgb(94, 52, 133);
--nav-sub-bg-hover: rgb(116, 67, 161);
--nav-head-bg: rgb(72, 39, 102);
--nav-head-bg-hl: rgb(94, 52, 133);
--nav-head-bg-hover: rgb(116, 67, 161);
--nav-content-hover: rgb(177, 97, 218);
--ff: 'Arial', Helvetica, sans-serif;
--btn-bg: rgb(158, 81, 197);
--btn-bg-hover: rgb(177, 97, 218);
--tcaption-bg: rgb(255, 255, 255);
--thead-bg: rgb(236, 232, 255);
--trow-bg: rgb(255, 255, 255);
--trow-bg-alt: rgb(255, 255, 255);
--trow-bg-hover: rgb(220, 211, 255);
--link-c: rgb(72, 39, 102);
--lhover: rgb(158, 81, 197);
--badge-size: .55rem;
--badge-c: rgb(255, 255, 255);
--badge-bg: rgb(158, 81, 197);
--bborder: rgb(218, 218, 218);
}

View File

@ -1,102 +0,0 @@
<?php
/**
* Jingga
*
* PHP Version 8.2
*
* @package Web\Timerecording
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace Web\Timerecording;
use Modules\Organization\Models\Unit;
use Modules\Profile\Models\Profile;
use phpOMS\Uri\UriFactory;
use phpOMS\Views\View;
/**
* Main view.
*
* @package Web\Timerecording
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
* @codeCoverageIgnore
*/
class TimerecordingView extends View
{
/**
* Navigation view
*
* @var View
* @since 1.0.0
*/
protected $nav = null;
/**
* User profile.
*
* @var Profile
* @since 1.0.0
*/
public $profile = null;
/**
* Organizations.
*
* @var Unit[]
* @since 1.0.0
*/
protected $organizations = null;
/**
* Set navigation view.
*
* @param View $nav Navigation view
*
* @return void
*
* @since 1.0.0
* @codeCoverageIgnore
*/
public function setNavigation(View $nav) : void
{
$this->nav = $nav;
}
/**
* Get profile image
*
* @return string Profile image link
*
* @since 1.0.0
*/
public function getProfileImage() : string
{
if ($this->profile === null || $this->profile->image->getPath() === '') {
return UriFactory::build('Web/Timerecording/img/user_default_' . \mt_rand(1, 6) . '.png');
}
return UriFactory::build($this->profile->image->getPath());
}
/**
* Set organizations
*
* @param Unit[] $organizations Organizations
*
* @return void
*
* @since 1.0.0
* @codeCoverageIgnore
*/
public function setOrganizations(array $organizations) : void
{
$this->organizations = $organizations;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -1,69 +0,0 @@
<?php
/**
* Jingga
*
* PHP Version 8.2
*
* @package Web\Timerecording
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
$nav = $this->getData('nav');
$nav->setTemplate('/Modules/Navigation/Theme/Backend/top');
$top = $nav->render();
/** @var phpOMS\Model\Html\Head $head */
$head = $this->head;
/** @var array $dispatch */
$dispatch = $this->getData('dispatch') ?? [];
?>
<!DOCTYPE HTML>
<html lang="<?= $this->printHtml($this->response->header->l11n->language); ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#712b91">
<meta name="msapplication-navbutton-color" content="#712b91">
<meta name="apple-mobile-web-app-status-bar-style" content="#712b91">
<meta name="description" content="<?= $this->getHtml(':meta', '0', '0'); ?>">
<?= $head->meta->render(); ?>
<base href="<?= \phpOMS\Uri\UriFactory::build('{/base}'); ?>/">
<link rel="manifest" href="<?= \phpOMS\Uri\UriFactory::build('Web/Timerecording/manifest.json'); ?>">
<link rel="shortcut icon" href="<?= \phpOMS\Uri\UriFactory::build('Web/Timerecording/img/favicon.ico'); ?>" type="image/x-icon">
<title><?= $this->printHtml($head->title); ?></title>
<?= $head->renderAssets(); ?>
<style><?= $head->renderStyle(); ?></style>
<script><?= $head->renderScript(); ?></script>
</head>
<body>
<div class="vh" id="dim"></div>
<header><div id="t-nav-container"><?= $top; ?></div></header>
<main id="content" class="container-fluid" role="main">
<?php
foreach ($dispatch as $view) {
if ($view instanceof \phpOMS\Contract\RenderableInterface) {
echo $view->render();
}
}
?>
</main>
<div id="app-message-container">
<template id="app-message-tpl">
<div class="log-msg">
<h1 class="log-msg-title"></h1>
<div class="log-msg-content"></div>
</div>
</template>
</div>
<?= $head->renderAssetsLate(); ?>

View File

@ -1,5 +0,0 @@
import { redirectMessage } from '../../../../jsOMS/Model/Action/Dom/Redirect.js';
export const ACTION_EVENTS = {
'redirect': redirectMessage, /** global: redirectMessage */
};

View File

@ -1,3 +0,0 @@
/** global: jsOMS */
export const KEYBOARD_EVENTS = [
];

View File

@ -1,3 +0,0 @@
/** global: jsOMS */
export const MOUSE_EVENTS = [
];

View File

@ -1,12 +0,0 @@
import { notifyMessage } from '../../../../jsOMS/Model/Message/Notify.js';
import { formValidationMessage } from '../../../../jsOMS/Model/Message/FormValidation.js';
import { redirectMessage } from '../../../../jsOMS/Model/Action/Dom/Redirect.js';
import { reloadButtonAction } from '../../../../jsOMS/Model/Action/Dom/Reload.js';
/** global: jsOMS */
export const RESPONSE_EVENTS = {
'notify': notifyMessage, /** global: notifyMessage */
'validation': formValidationMessage, /** global: formValidationMessage */
'redirect': redirectMessage, /** global: redirectMessage */
'reload': reloadButtonAction /** global: reloadButtonAction */
};

View File

@ -1,2 +0,0 @@
/** global: jsOMS */
export const TOUCH_EVENTS = {};

View File

@ -1,6 +0,0 @@
import { Logger } from '../../../../jsOMS/Log/Logger.js';
/** global: jsOMS */
export const VOICE_EVENTS = {
'login': function() { document.getElementById('iCameraLoginButton').click(); },
};

View File

@ -1,90 +0,0 @@
jsOMS.ready(function ()
{
"use strict";
const logo = document.getElementById('login-logo');
const cLogin = document.getElementById('iCameraLoginButton');
const pLogin = document.getElementById('iPasswordLoginButton');
const cancel = document.getElementsByClassName('cancelButton');
const cancelLength = cancel.length;
let timer = 10000;
cLogin.addEventListener('click', function() {
if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
const clock = document.getElementById('iCameraCountdownClock');
const countdown = document.getElementById('iCameraCountdown');
const video = document.getElementById('iVideoCanvas');
navigator.mediaDevices.getUserMedia({ video: true, audio: false }).then(function(stream) {
video.srcObject = stream;
video.play();
jsOMS.addClass(logo, 'vh');
jsOMS.addClass(cLogin, 'vh');
jsOMS.addClass(pLogin, 'vh');
jsOMS.removeClass(document.getElementById('cameraLogin'), 'vh');
timer = 10000;
clock.innerHTML = timer / 1000;
jsOMS.removeClass(countdown, 'vh');
const cameraTimer = setInterval(function() {
timer -= 100;
if (timer % 1000 === 0) {
clock.innerHTML = timer / 1000;
}
if (timer <= 0) {
jsOMS.addClass(document.getElementById('cameraLogin'), 'vh');
jsOMS.removeClass(cLogin, 'vh');
jsOMS.removeClass(pLogin, 'vh');
jsOMS.removeClass(logo, 'vh');
video.pause();
stream.getVideoTracks()[0].stop();
clearInterval(cameraTimer);
}
}, 100);
});
}
});
pLogin.addEventListener('click', function() {
const clock = document.getElementById('iPasswordCountdownClock');
const countdown = document.getElementById('iPasswordCountdown');
jsOMS.addClass(pLogin, 'vh');
jsOMS.addClass(cLogin, 'vh');
jsOMS.removeClass(document.getElementById('passwordLogin'), 'vh');
timer = 10000;
clock.innerHTML = timer / 1000;
jsOMS.removeClass(countdown, 'vh');
const passwordTimer = setInterval(function() {
timer -= 100;
if (timer % 1000 === 0) {
clock.innerHTML = timer / 1000;
}
if (timer <= 0) {
jsOMS.addClass(document.getElementById('passwordLogin'), 'vh');
jsOMS.removeClass(pLogin, 'vh');
jsOMS.removeClass(cLogin, 'vh');
clearInterval(passwordTimer);
}
}, 100);
});
for (let i = 0; i < cancelLength; ++i) {
cancel[i].addEventListener('click', function() {
timer = 0;
});
}
});

View File

@ -1,139 +0,0 @@
import { AssetManager } from '../../../jsOMS/Asset/AssetManager.js';
import { Logger } from '../../../jsOMS/Log/Logger.js';
import { CacheManager } from '../../../jsOMS/DataStorage/CacheManager.js';
import { StorageManager } from '../../../jsOMS/DataStorage/StorageManager.js';
import { EventManager } from '../../../jsOMS/Event/EventManager.js';
import { ResponseManager } from '../../../jsOMS/Message/Response/ResponseManager.js';
import { Dispatcher } from '../../../jsOMS/Dispatcher/Dispatcher.js';
import { AccountManager } from '../../../jsOMS/Account/AccountManager.js';
import { UIManager } from '../../../jsOMS/UI/UIManager.js';
import { InputManager } from '../../../jsOMS/UI/Input/InputManager.js';
import { ModuleManager } from '../../../jsOMS/Module/ModuleManager.js';
import { ReadManager } from '../../../jsOMS/UI/Input/Voice/ReadManager.js';
import { VoiceManager } from '../../../jsOMS/UI/Input/Voice/VoiceManager.js';
import { NotificationManager } from '../../../jsOMS/Message/Notification/NotificationManager.js';
import { HttpUri } from '../../../jsOMS/Uri/HttpUri.js';
import { UriFactory } from '../../../jsOMS/Uri/UriFactory.js';
import { ACTION_EVENTS } from './global/ActionEvents.js';
import { KEYBOARD_EVENTS } from './global/KeyboardEvents.js';
import { MOUSE_EVENTS } from './global/MouseEvents.js';
import { RESPONSE_EVENTS } from './global/ResponseEvents.js';
import { TOUCH_EVENTS } from './global/TouchEvents.js';
import { VOICE_EVENTS } from './global/VoiceEvents.js';
export class Application {
constructor ()
{
//jsOMS.Autoloader.initPreloaded();
this.logger = Logger.getInstance();
window.logger = this.logger;
this.cacheManager = new CacheManager();
this.storageManager = new StorageManager();
this.eventManager = new EventManager();
this.responseManager = new ResponseManager();
this.dispatcher = new Dispatcher();
this.assetManager = new AssetManager();
this.accountManager = new AccountManager();
this.uiManager = new UIManager(this);
this.inputManager = new InputManager(this);
this.moduleManager = new ModuleManager(this);
this.readManager = new ReadManager();
this.voiceManager = new VoiceManager(this);
this.notifyManager = new NotificationManager();
this.request = new HttpUri(window.location.href);
this.request.setRootPath(
HttpUri.parseUrl(
typeof document.getElementsByTagName('base')[0] !== 'undefined'
? document.getElementsByTagName('base')[0].href
: ''
).path
);
this.setResponseMessages();
this.setActions();
this.setKeyboardActions();
this.setMouseActions();
this.setVoiceActions();
UriFactory.setupUriBuilder(this.request);
UriFactory.setQuery('/lang', window.location.href.substr(this.request.getBase().length).split('/')[0]);
this.uiManager.bind();
};
setResponseMessages ()
{
/** global: RESPONSE_EVENTS */
for (const key in RESPONSE_EVENTS) {
if (RESPONSE_EVENTS.hasOwnProperty(key)) {
this.responseManager.add(key, RESPONSE_EVENTS[key]);
}
}
};
setActions ()
{
/** global: ACTION_EVENTS */
for (const key in ACTION_EVENTS) {
if (ACTION_EVENTS.hasOwnProperty(key)) {
this.uiManager.getActionManager().add(key, ACTION_EVENTS[key]);
}
}
};
setKeyboardActions ()
{
/** global: KEYBOARD_EVENTS */
let length = KEYBOARD_EVENTS.length;
for (let i = 0; i < length; i++) {
this.inputManager.getKeyboardManager().add(
KEYBOARD_EVENTS[i]['element'],
KEYBOARD_EVENTS[i]['keys'],
KEYBOARD_EVENTS[i]['callback']
);
}
};
setMouseActions ()
{
/** global: MOUSE_EVENTS */
let length = MOUSE_EVENTS.length;
for (let i = 0; i < length; i++) {
this.inputManager.getMouseManager().add(
MOUSE_EVENTS[i]['element'],
MOUSE_EVENTS[i]['type'],
MOUSE_EVENTS[i]['button'],
MOUSE_EVENTS[i]['callback'],
MOUSE_EVENTS[i]['exact']
);
}
};
setVoiceActions ()
{
/** global: VOICE_EVENTS */
for (const key in VOICE_EVENTS) {
if (VOICE_EVENTS.hasOwnProperty(key)) {
this.voiceManager.add(key, VOICE_EVENTS[key]);
}
}
this.voiceManager.setup();
this.voiceManager.start();
};
};
jsOMS.ready(function ()
{
"use strict";
/** global: jsOMS */
window.omsApp = new Application();
});

View File

@ -1,27 +0,0 @@
<?php
/**
* Jingga
*
* PHP Version 8.2
*
* @package Web\Timerecording
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
return [[
'CameraLogin' => 'Camera Login',
'Cancel' => 'Cancel',
'IDCard' => 'ID Card',
'Login' => 'Login',
'Logo' => 'Logo',
'Password' => 'Password',
'PasswordLogin' => 'Password Login',
'TimerCamera' => 'Camera turns off in: %s (s)',
'TimerLogin' => 'Login turns off in: %s (s)',
'Username' => 'Username',
':meta' => 'Time recording application',
]];

View File

@ -1,267 +0,0 @@
<?php
/**
* Jingga
*
* PHP Version 8.2
*
* @package Template
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
/**
* Jingga
*
* PHP Version 8.2
*
* @package Web\Timerecording
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
$head = $this->head;
?>
<!DOCTYPE HTML>
<html lang="<?= $this->printHtml($this->response->header->l11n->language); ?>">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<base href="<?= \phpOMS\Uri\UriFactory::build('{/base}'); ?>/">
<meta name="theme-color" content="#9e51c5">
<meta name="msapplication-navbutton-color" content="#9e51c5">
<meta name="theme-color" content="#9e51c5">
<meta name="description" content="<?= $this->getHtml(':meta', '0', '0'); ?>">
<link rel="manifest" href="<?= \phpOMS\Uri\UriFactory::build('Web/Timerecording/manifest.json'); ?>">
<link rel="shortcut icon" href="<?= \phpOMS\Uri\UriFactory::build('Web/Timerecording/img/favicon.ico'); ?>" type="image/x-icon">
<?= $head->meta->render(); ?>
<title><?= $this->printHtml($head->title); ?></title>
<style><?= $head->renderStyle(); ?></style>
<script><?= $head->renderScript(); ?></script>
<?= $head->renderAssets(); ?>
<style type="text/css">
:root {
--main-bg: #2e1a5a;
--main-bg-hl: #9e51c5;
--iborder: rgba(166, 135, 232, .4);
--iborder-active: rgba(166, 135, 232, .7);
--ipt-c: rgba(166, 135, 232, .6);
--ipt-c-active: rgba(166, 135, 232, .8);
--ipt-ico-c: rgba(166, 135, 232, .6);
--ipt-ico-c-active: rgba(166, 135, 232, 1);
--btn-main-bg: rgba(166, 135, 232, .6);
--btn-main-bg-active: rgba(166, 135, 232, .8);
--btn-main-c: rgba(255, 255, 255, .9);
--txt-on-bg-c: rgba(255, 255, 255, 0.7);
}
html, body {
height: 100%;
font-family: 'Roboto', sans-serif;
background-image: linear-gradient(var(--main-bg-hl), var(--main-bg));
color: var(--txt-on-bg-c);
padding: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
flex-direction: column;
font-weight: 300;
}
#login-container {
width: 90%;
max-width: 800px;
margin: 0 auto;
}
#login-logo {
height: 185px;
}
#login-logo img {
width: 20%;
min-width: 200px;
}
h1 {
text-shadow: 2px 2px 3px rgba(0,0,0,0.3);
}
#login-logo {
margin-bottom: 2rem;
}
#login-logo, #login-form {
text-align: center;
}
#passwordLogin, #cameraLogin {
text-align: left;
}
form {
margin-bottom: 1rem;
display: inline-block;
text-align: center;
width: 100%;
}
form label {
text-shadow: none;
color: var(--txt-on-bg-c);
cursor: pointer;
}
form input[type=text],
form input[type=password] {
margin-bottom: .5rem;
background: rgba(0, 0, 0, .15);
border: 1px solid var(--iborder);
text-shadow: none;
box-shadow: none;
color: var(--txt-on-bg-c);
width: 100%;
transition : border 500ms ease-out;
outline: none;
box-sizing: border-box;
line-height: 1rem;
}
.inputWithIcon {
position: relative;
}
.inputWithIcon input {
padding-left: 2.5rem;
}
.inputWithIcon .frontIco {
color: var(--ipt-ico-c);
font-size: 1rem;
position: absolute;
left: 0;
top: 0;
padding: .65rem;
}
.inputWithIcon .endIco {
color: var(--ipt-ico-c);
font-size: 1rem;
position: absolute;
right: 0;
top: 0;
padding: .65rem;
}
form input[type=text]:active, form input[type=text]:focus,
form input[type=password]:active, form input[type=password]:focus {
border: 1px solid var(--iborder-active);
color: var(--txt-on-bg-c);
}
form input[type=text]:active~.frontIco, form input[type=text]:focus~.frontIco,
form input[type=password]:active~.frontIco, form input[type=password]:focus~.frontIco,
form input[type=text]:active~.endIco, form input[type=text]:focus~.endIco,
form input[type=password]:active~.endIco, form input[type=password]:focus~.endIco {
color: var(--ipt-ico-c-active);
}
form input[type=text]~.endIco, form input[type=text]~.endIco,
form input[type=password]~.endIco, form input[type=password]~.endIco {
cursor: pointer;
}
form input[type=submit], button {
width: calc(50% - 10px);
background-color: var(--btn-main-bg);
border: none;
text-shadow: none;
box-shadow: none;
color: var(--btn-main-c);
cursor: pointer;
transition : background-color 500ms ease-out;
margin-bottom: 1rem;
white-space: nowrap;
}
button+button, input+button {
margin-left: 14px;
}
form input[type=submit]:hover, button:hover,
form input[type=submit]:focus, button:focus {
background-color: var(--btn-main-bg-active);
border: none;
text-shadow: none;
box-shadow: none;
}
#forgot-password {
text-align: center;
}
#forgot-password a {
padding-bottom: .5rem;
cursor: pointer;
transition : border-bottom 100ms ease-out;
}
#forgot-password a:hover,
#forgot-password a:focus {
color: rgba(255, 255, 255, .8);
border-bottom: 1px solid rgba(255, 255, 255, .6);
}
video {
width: 100%;
height: 100%;
border: 1px solid var(--iborder);
}
</style>
</head>
<body>
<div id="login-container">
<div id="login-logo">
<img class="animated infinte pulse" alt="<?= $this->getHtml('Logo', '0', '0'); ?>" src="<?= \phpOMS\Uri\UriFactory::build('Web/Backend/img/logo.png'); ?>">
</div>
<div id="login-form">
<form id="login" method="POST" action="<?= \phpOMS\Uri\UriFactory::build('{/api}login?{?}'); ?>">
<button id="iCameraLoginButton" name="cameraLoginButton" type="button" tabindex="1"><?= $this->getHtml('CameraLogin', '0', '0'); ?></button>
<button id="iPasswordLoginButton" name="passwordLoginButton" type="button" tabindex="2"><?= $this->getHtml('PasswordLogin', '0', '0'); ?></button>
<div id="cameraLogin" class="vh">
<h1><?= $this->getHtml('IDCard', '0', '0'); ?>:</h1>
<video id="iVideoCanvas"></video>
<button class="cancelButton" name="cancelButton" type="button" tabindex="6"><?= $this->getHtml('Cancel', '0', '0'); ?></button>
<div id="iCameraCountdown"><?php \printf($this->getHtml('TimerCamera', '0', '0'), '<span id="iCameraCountdownClock"></span>'); ?></div>
</div>
<div id="passwordLogin" class="vh">
<h1><?= $this->getHtml('Login', '0', '0'); ?>:</h1>
<label for="iName"><?= $this->getHtml('Username', '0', '0'); ?>:</label>
<div class="inputWithIcon">
<i class="frontIco g-icon" aria-hidden="true">person</i>
<input id="iName" type="text" name="user" tabindex="3" value="admin" autofocus>
<i class="endIco g-icon close" aria-hidden="true">close</i>
</div>
<label for="iPassword"><?= $this->getHtml('Password', '0', '0'); ?>:</label>
<div class="inputWithIcon">
<i class="frontIco g-icon" aria-hidden="true">lock</i>
<input id="iPassword" type="password" name="pass" tabindex="4" value="orange">
<i class="endIco g-icon close" aria-hidden="true">close</i>
</div>
<input id="iLoginButton" name="loginButton" type="submit" value="<?= $this->getHtml('Login', '0', '0'); ?>" tabindex="5">
<button class="cancelButton" name="cancelButton" type="button" tabindex="6"><?= $this->getHtml('Cancel', '0', '0'); ?></button>
<div id="iPasswordCountdown"><?php \printf($this->getHtml('TimerLogin', '0', '0'), '<span id="iPasswordCountdownClock"></span>'); ?></div>
</div>
</form>
</div>
</div>
<?= $head->renderAssetsLate(); ?>

View File

@ -1,33 +0,0 @@
{
"lang": "en",
"dir": "ltr",
"start_url": "../../en/timerecording",
"type": "privileged",
"name": "Jingga Time Recording",
"description": "OMS timerecording application.",
"short_name": "OMS Backend",
"icons": [
{
"src": "/Web/Timerecording/img/logo.png",
"sizes": "64x64 128x128 256x256 512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"scope": "/",
"display": "standalone",
"orientation": "any",
"theme_color": "#712b91",
"background_color": "white",
"permissions": {
"audio-capture" : {
"description" : "Audio capture"
},
"video-capture": {
"description": "Video capture"
},
"speech-recognition" : {
"description" : "Speech recognition"
}
}
}