Merge commit '752d8d679f8cf075ed2d8608071dad654c293f93'

This commit is contained in:
Buster Neece 2023-08-07 05:10:49 -05:00
parent 4c64842553
commit 85214e6d2d
No known key found for this signature in database
103 changed files with 1279 additions and 961 deletions

View File

@ -14,50 +14,8 @@ return static function (RouteCollectorProxy $app) {
$group->get('', Controller\Admin\IndexAction::class)
->setName('admin:index:index');
$group->group(
'/debug',
function (RouteCollectorProxy $group) {
$group->get('', Controller\Admin\Debug\IndexAction::class)
->setName('admin:debug:index');
$group->get('/clear-cache', Controller\Admin\Debug\ClearCacheAction::class)
->setName('admin:debug:clear-cache');
$group->get(
'/clear-queue[/{queue}]',
Controller\Admin\Debug\ClearQueueAction::class
)->setName('admin:debug:clear-queue');
$group->get('/sync/{task}', Controller\Admin\Debug\SyncAction::class)
->setName('admin:debug:sync');
$group->group(
'/station/{station_id}',
function (RouteCollectorProxy $group) {
$group->map(
['GET', 'POST'],
'/nowplaying',
Controller\Admin\Debug\NowPlayingAction::class
)->setName('admin:debug:nowplaying');
$group->map(
['GET', 'POST'],
'/nextsong',
Controller\Admin\Debug\NextSongAction::class
)->setName('admin:debug:nextsong');
$group->map(
['GET', 'POST'],
'/clearqueue',
Controller\Admin\Debug\ClearStationQueueAction::class
)->setName('admin:debug:clear-station-queue');
$group->post('/telnet', Controller\Admin\Debug\TelnetAction::class)
->setName('admin:debug:telnet');
}
)->add(Middleware\GetStation::class);
}
)->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/debug', Controller\Admin\DebugAction::class)
->setName('admin:debug:index');
$group->group(
'/install',
@ -127,6 +85,7 @@ return static function (RouteCollectorProxy $app) {
}
)
->add(Middleware\Module\Admin::class)
->add(Middleware\Module\PanelLayout::class)
->add(Middleware\EnableView::class)
->add(new Middleware\Permissions(GlobalPermissions::View))
->add(Middleware\RequireLogin::class);

View File

@ -57,6 +57,45 @@ return static function (RouteCollectorProxy $group) {
}
)->add(new Middleware\Permissions(GlobalPermissions::Backups));
$group->group(
'/debug',
function (RouteCollectorProxy $group) {
$group->put('/clear-cache', Controller\Api\Admin\Debug\ClearCacheAction::class)
->setName('api:admin:debug:clear-cache');
$group->put(
'/clear-queue[/{queue}]',
Controller\Api\Admin\Debug\ClearQueueAction::class
)->setName('api:admin:debug:clear-queue');
$group->put('/sync/{task}', Controller\Api\Admin\Debug\SyncAction::class)
->setName('api:admin:debug:sync');
$group->group(
'/station/{station_id}',
function (RouteCollectorProxy $group) {
$group->put(
'/nowplaying',
Controller\Api\Admin\Debug\NowPlayingAction::class
)->setName('api:admin:debug:nowplaying');
$group->put(
'/nextsong',
Controller\Api\Admin\Debug\NextSongAction::class
)->setName('api:admin:debug:nextsong');
$group->put(
'/clearqueue',
Controller\Api\Admin\Debug\ClearStationQueueAction::class
)->setName('api:admin:debug:clear-station-queue');
$group->put('/telnet', Controller\Api\Admin\Debug\TelnetAction::class)
->setName('api:admin:debug:telnet');
}
)->add(Middleware\GetStation::class);
}
)->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/server/stats', Controller\Api\Admin\ServerStatsAction::class)
->setName('api:admin:server:stats')
->add(new Middleware\Permissions(GlobalPermissions::View));

View File

@ -4,43 +4,38 @@ declare(strict_types=1);
use App\Controller;
use App\Enums\GlobalPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Slim\Routing\RouteCollectorProxy;
return static function (RouteCollectorProxy $app) {
$app->get('/', Controller\Frontend\IndexAction::class)
->setName('home');
$app->get('/logout', Controller\Frontend\Account\LogoutAction::class)
->setName('account:logout')
->add(Middleware\RequireLogin::class);
$app->get('/login-as/{id}/{csrf}', Controller\Frontend\Account\MasqueradeAction::class)
->setName('account:masquerade')
->add(new Middleware\Permissions(GlobalPermissions::All))
->add(Middleware\RequireLogin::class);
$app->get('/endsession', Controller\Frontend\Account\EndMasqueradeAction::class)
->setName('account:endmasquerade')
->add(Middleware\RequireLogin::class);
$app->group(
'',
function (RouteCollectorProxy $group) {
$group->get('/dashboard', Controller\Frontend\DashboardAction::class)
->setName('dashboard');
$group->get('/logout', Controller\Frontend\Account\LogoutAction::class)
->setName('account:logout');
$group->get('/login-as/{id}/{csrf}', Controller\Frontend\Account\MasqueradeAction::class)
->setName('account:masquerade')
->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/endsession', Controller\Frontend\Account\EndMasqueradeAction::class)
->setName('account:endmasquerade');
$group->get(
'/api_keys',
function (ServerRequest $request, Response $response): ResponseInterface {
return $response->withRedirect('/profile');
}
);
$group->get('/profile', Controller\Frontend\Profile\IndexAction::class)
->setName('profile:index');
}
)->add(Middleware\EnableView::class)
)->add(Middleware\Module\PanelLayout::class)
->add(Middleware\EnableView::class)
->add(Middleware\RequireLogin::class);
$app->map(['GET', 'POST'], '/login', Controller\Frontend\Account\LoginAction::class)
@ -72,10 +67,12 @@ return static function (RouteCollectorProxy $app) {
->setName('setup:register');
$group->map(['GET', 'POST'], '/station', Controller\Frontend\SetupController::class . ':stationAction')
->setName('setup:station');
->setName('setup:station')
->add(Middleware\Module\PanelLayout::class);
$group->map(['GET', 'POST'], '/settings', Controller\Frontend\SetupController::class . ':settingsAction')
->setName('setup:settings');
->setName('setup:settings')
->add(Middleware\Module\PanelLayout::class);
}
)->add(Middleware\EnableView::class);
};

View File

@ -142,6 +142,7 @@ return static function (RouteCollectorProxy $app) {
}
)
->add(Middleware\Module\Stations::class)
->add(Middleware\Module\PanelLayout::class)
->add(new Middleware\Permissions(StationPermissions::View, true))
->add(Middleware\RequireStation::class)
->add(Middleware\GetStation::class)

View File

@ -43,21 +43,24 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ()
}
});
export function switchTheme() {
const currentTheme = getPreferredTheme();
if (currentTheme === 'light') {
setStoredTheme('dark');
setTheme('dark');
} else {
setStoredTheme('light');
setTheme('light');
}
}
ready(() => {
// Theme switcher
document.querySelectorAll('.theme-switcher').forEach(
toggle => {
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const currentTheme = getPreferredTheme();
if (currentTheme === 'light') {
setStoredTheme('dark');
setTheme('dark');
} else {
setStoredTheme('light');
setTheme('light');
}
switchTheme();
});
});

View File

@ -35,6 +35,7 @@
"vue/**/*.ts",
"vue/**/*.d.ts",
"vue/**/*.tsx",
"vue/**/*.vue"
"vue/**/*.vue",
"vue/**/*.js"
]
}

View File

@ -16,13 +16,13 @@
</div>
<template #footer_actions>
<a
<button
type="button"
class="btn btn-sm btn-primary"
role="button"
:href="clearCacheUrl"
@click="makeDebugCall(clearCacheUrl)"
>
{{ $gettext('Clear Cache') }}
</a>
</button>
</template>
</card-page>
</div>
@ -38,13 +38,13 @@
</div>
<template #footer_actions>
<a
<button
type="button"
class="btn btn-sm btn-primary"
role="button"
:href="clearQueuesUrl"
@click="makeDebugCall(clearQueuesUrl)"
>
{{ $gettext('Clear All Message Queues') }}
</a>
</button>
</template>
</card-page>
</div>
@ -66,13 +66,13 @@
{{ row.item.pattern }}
</template>
<template #cell(actions)="row">
<a
<button
type="button"
class="btn btn-sm btn-primary"
role="button"
:href="row.item.url"
@click="makeDebugCall(row.item.url)"
>
{{ $gettext('Run Task') }}
</a>
</button>
</template>
</data-table>
</card-page>
@ -103,13 +103,13 @@
</p>
<div class="buttons">
<a
<button
type="button"
class="btn btn-sm btn-primary"
role="button"
:href="row.url"
@click="makeDebugCall(row.url)"
>
{{ $gettext('Clear Queue') }}
</a>
</button>
</div>
</div>
</div>
@ -137,33 +137,33 @@
<h5>{{ $gettext('AutoDJ Queue') }}</h5>
<div class="buttons">
<a
<button
type="button"
class="btn btn-sm btn-primary"
role="button"
:href="station.clearQueueUrl"
@click="makeDebugCall(station.clearQueueUrl)"
>
{{ $gettext('Clear Queue') }}
</a>
<a
</button>
<button
type="button"
class="btn btn-sm btn-primary"
role="button"
:href="station.getNextSongUrl"
@click="makeDebugCall(station.getNextSongUrl)"
>
{{ $gettext('Get Next Song') }}
</a>
</button>
</div>
</div>
<div class="col-md-4">
<h5>{{ $gettext('Get Now Playing') }}</h5>
<div class="buttons">
<a
<button
type="button"
class="btn btn-sm btn-primary"
role="button"
:href="station.getNowPlayingUrl"
@click="makeDebugCall(station.getNowPlayingUrl)"
>
{{ $gettext('Run Task') }}
</a>
</button>
</div>
</div>
</div>
@ -171,6 +171,8 @@
</o-tabs>
</div>
</card-page>
<task-output-modal ref="$modal" />
</template>
<script setup>
@ -180,6 +182,9 @@ import DataTable from "~/components/Common/DataTable.vue";
import {useTranslate} from "~/vendor/gettext";
import CardPage from "~/components/Common/CardPage.vue";
import {useLuxon} from "~/vendor/luxon";
import TaskOutputModal from "~/components/Admin/Debug/TaskOutputModal.vue";
import {useAxios} from "~/vendor/axios";
import {useNotify} from "~/functions/useNotify";
const props = defineProps({
clearCacheUrl: {
@ -229,4 +234,18 @@ const syncTaskFields = [
const $datatable = ref(); // Template Ref
useHasDatatable($datatable);
const $modal = ref();
const {axios} = useAxios();
const {notifySuccess} = useNotify();
const makeDebugCall = (url) => {
axios.put(url).then((resp) => {
if (resp.data.logs) {
$modal.value.open(resp.data.logs);
} else {
notifySuccess(resp.data.message);
}
});
}
</script>

View File

@ -0,0 +1,109 @@
<template>
<div
v-for="(row, id) in logs"
id="log-view"
:key="id"
class="card mb-3"
>
<div
:id="'log-row-'+id"
class="card-header"
>
<h4 class="mb-0">
<span
class="badge"
:class="getBadgeClass(row.level)"
>{{ getBadgeLabel(row.level) }}</span>
{{ row.message }}
</h4>
<div
v-if="row.context || row.extra"
class="buttons mt-3"
>
<button
class="btn btn-sm btn-bg"
type="button"
data-bs-toggle="collapse"
:data-bs-target="'#detail-row-'+id"
:aria-controls="'detail-row-'+id"
>
{{ $gettext('Details') }}
</button>
</div>
</div>
<div
v-if="row.context || row.extra"
:id="'detail-row-'+id"
class="collapse"
:aria-labelledby="'log-row-'+id"
data-parent="#log-view"
>
<div class="card-body pb-0">
<dl>
<template
v-for="(context_value, context_header) in row.context"
:key="context_header"
>
<dt>{{ context_header }}</dt>
<dd>{{ dump(context_value) }}</dd>
</template>
<template
v-for="(context_value, context_header) in row.extra"
:key="context_header"
>
<dt>{{ context_header }}</dt>
<dd>{{ dump(context_value) }}</dd>
</template>
</dl>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {useTranslate} from "~/vendor/gettext.ts";
import {get} from 'lodash';
const props = defineProps({
logs: {
type: Array,
required: true
}
});
const badgeClasses = {
100: 'text-bg-info',
200: 'text-bg-info',
250: 'text-bg-info',
300: 'text-bg-warning',
400: 'text-bg-danger',
500: 'text-bg-danger',
550: 'text-bg-danger',
600: 'text-bg-danger'
};
const getBadgeClass = (logLevel) => {
return get(badgeClasses, logLevel, badgeClasses[100]);
};
const {$gettext} = useTranslate();
const badgeLabels = {
100: $gettext('Debug'),
200: $gettext('Info'),
250: $gettext('Notice'),
300: $gettext('Warning'),
400: $gettext('Error'),
500: $gettext('Critical'),
550: $gettext('Alert'),
600: $gettext('Emergency')
};
const getBadgeLabel = (logLevel) => {
return get(badgeLabels, logLevel, badgeLabels[100]);
};
const dump = (value) => {
return JSON.stringify(value);
}
</script>

View File

@ -0,0 +1,29 @@
<template>
<modal
ref="$modal"
:title="$gettext('Log Output')"
size="lg"
>
<div style="max-height: 300px; overflow-y: scroll">
<task-output :logs="logOutput" />
</div>
</modal>
</template>
<script setup lang="ts">
import Modal from "~/components/Common/Modal.vue";
import {ref, Ref} from "vue";
import TaskOutput from "~/components/Admin/Debug/TaskOutput.vue";
const $modal: Ref<Modal> = ref();
const logOutput: Ref<array> = ref([]);
const open = (newLogOutput: array) => {
logOutput.value = newLogOutput;
$modal.value.show();
}
defineExpose({
open
});
</script>

View File

@ -415,12 +415,9 @@ import RunningBadge from "~/components/Common/Badges/RunningBadge.vue";
import {onMounted, ref, shallowRef} from "vue";
import {useAxios} from "~/vendor/axios";
import {useNotify} from "~/functions/useNotify";
import {useAzuraCast} from "~/vendor/azuracast";
const props = defineProps({
adminPanels: {
type: Object,
required: true
},
statsUrl: {
type: String,
required: true
@ -431,6 +428,9 @@ const props = defineProps({
}
});
const {sidebarProps} = useAzuraCast();
const adminPanels = sidebarProps.menu;
const stats = shallowRef({
cpu: {
total: {

View File

@ -0,0 +1,34 @@
<template>
<div class="navdrawer-header">
<a
class="navbar-brand px-0"
:href="homeUrl"
>
{{ $gettext('Administration') }}
</a>
</div>
<sidebar-menu
:menu="menu"
:active="active"
/>
</template>
<script setup>
import SidebarMenu from "~/components/Common/SidebarMenu.vue";
const props = defineProps({
homeUrl: {
type: String,
required: true
},
menu: {
type: Object,
required: true
},
active: {
type: String,
default: null
}
});
</script>

View File

@ -0,0 +1,96 @@
<template>
<ul class="offcanvas-body navdrawer-nav">
<li
v-for="(category, category_id) in menu"
:key="category_id"
class="nav-item"
>
<a
v-bind="getCategoryLink(category, category_id)"
class="nav-link"
>
<icon
class="navdrawer-nav-icon"
:icon="category.icon"
/>
{{ category.label }}
<icon
v-if="category.external"
class="sm ms-2"
icon="open_in_new"
:aria-label="$gettext('External')"
/>
</a>
<div
v-if="category.items"
:id="'sidebar-submenu-'+category_id"
class="collapse pb-2"
>
<ul class="navdrawer-nav">
<li
v-for="(item, item_id) in category.items"
:key="item_id"
class="nav-item"
>
<a
class="nav-link ps-4 py-2"
:class="item.class"
:href="item.url"
:target="(item.external) ? '_blank' : ''"
:title="item.title"
>
{{ item.label }}
<icon
v-if="item.external"
class="sm ms-2"
icon="open_in_new"
:aria-label="$gettext('External')"
/>
</a>
</li>
</ul>
</div>
</li>
</ul>
</template>
<script setup>
import Icon from "~/components/Common/Icon.vue";
const props = defineProps({
menu: {
type: Object,
required: true
},
active: {
type: String,
default: null
}
});
const getCategoryLink = (category, category_id) => {
const linkAttrs = {
class: [
category.class,
(props.active === category_id) ? 'active' : ''
]
};
if (category.items) {
linkAttrs['data-bs-toggle'] = 'collapse';
linkAttrs.href = '#sidebar-submenu-' + category_id;
} else {
linkAttrs.href = category.url;
}
if (category.external) {
linkAttrs.target = '_blank';
}
if (category.title) {
linkAttrs.title = category.title;
}
return linkAttrs;
}
</script>

View File

@ -0,0 +1,224 @@
<template>
<a
class="visually-hidden-focusable"
href="#content"
>
{{ $gettext('Skip to main content') }}
</a>
<header class="navbar bg-primary-dark shadow-sm fixed-top">
<template v-if="slots.sidebar">
<button
id="navbar-toggle"
data-bs-toggle="offcanvas"
data-bs-target="#sidebar"
aria-controls="sidebar"
aria-expanded="false"
:aria-label="$gettext('Toggle Sidebar')"
class="navbar-toggler d-inline-flex d-lg-none me-3"
>
<icon
icon="menu"
class="lg"
/>
</button>
</template>
<a
class="navbar-brand ms-0 me-auto"
:href="homeUrl"
>
azura<strong>cast</strong>
<small v-if="instanceName">{{ instanceName }}</small>
</a>
<div id="radio-player-controls" />
<div class="dropdown ms-3 d-inline-flex align-items-center">
<div class="me-2">
{{ userDisplayName }}
</div>
<button
aria-expanded="false"
aria-haspopup="true"
class="navbar-toggler"
:aria-label="$gettext('Toggle Menu')"
data-bs-toggle="dropdown"
type="button"
>
<icon
icon="menu_open"
class="lg"
/>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a
class="dropdown-item"
:href="homeUrl"
>
<icon icon="home" />
{{ $gettext('Dashboard') }}
</a>
</li>
<li class="dropdown-divider">
&nbsp;
</li>
<li v-if="showAdmin">
<a
class="dropdown-item"
:href="adminUrl"
>
<icon icon="settings" />
{{ $gettext('System Administration') }}
</a>
</li>
<li>
<a
class="dropdown-item"
:href="profileUrl"
>
<icon icon="account_circle" />
{{ $gettext('My Account') }}
</a>
</li>
<li>
<a
class="dropdown-item theme-switcher"
href="#"
@click.prevent="switchTheme"
>
<icon icon="invert_colors" />
{{ $gettext('Switch Theme') }}
</a>
</li>
<li>
<a
class="dropdown-item"
href="https://docs.azuracast.com/en/user-guide/troubleshooting"
target="_blank"
>
<i
class="material-icons"
aria-hidden="true"
>help</i>
{{ $gettext('Help') }}
</a>
</li>
<li class="dropdown-divider">
&nbsp;
</li>
<li>
<a
class="dropdown-item"
:href="logoutUrl"
>
<icon icon="exit_to_app" />
{{ $gettext('Sign Out') }}
</a>
</li>
</ul>
</div>
</header>
<nav
v-if="slots.sidebar"
id="sidebar"
class="navdrawer offcanvas offcanvas-start"
tabindex="-1"
:aria-label="$gettext('Sidebar')"
>
<slot name="sidebar" />
</nav>
<section id="main">
<main
id="content"
:class="[(slots.sidebar) ? 'content-alt' : '']"
>
<div class="container">
<slot />
</div>
</main>
</section>
<footer
id="footer"
:class="[(slots.sidebar) ? 'footer-alt' : '']"
>
{{ $gettext('Powered by') }}
<a
href="https://www.azuracast.com/"
target="_blank"
>AzuraCast</a>
&bull;
<span v-html="version" />
&bull;
<span v-html="platform" /><br>
{{ $gettext('Like our software?') }}
<a
href="https://docs.azuracast.com/en/contribute/donate"
target="_blank"
>
{{ $gettext('Donate to support AzuraCast!') }}
</a>
</footer>
</template>
<script setup>
import {onMounted, onUnmounted, useSlots} from "vue";
import Icon from "~/components/Common/Icon.vue";
import {switchTheme} from "!/js/layout";
const props = defineProps({
instanceName: {
type: String,
required: true
},
userDisplayName: {
type: String,
required: true
},
homeUrl: {
type: String,
required: true,
},
profileUrl: {
type: String,
required: true,
},
adminUrl: {
type: String,
required: true
},
logoutUrl: {
type: String,
required: true
},
showAdmin: {
type: Boolean,
default: false
},
version: {
type: String,
required: true
},
platform: {
type: String,
required: true
}
});
const slots = useSlots();
onMounted(() => {
if (slots.sidebar) {
document.body.classList.add('has-sidebar');
}
});
onUnmounted(() => {
document.body.classList.remove('has-sidebar');
});
</script>

View File

@ -0,0 +1,92 @@
<template>
<div class="navdrawer-header offcanvas-header">
<div class="d-flex align-items-center">
<a
class="navbar-brand px-0 flex-fill"
:href="profileUrl"
>
<div>{{ name }}</div>
<div
id="station-time"
class="fs-6"
:title="$gettext('Station Time')"
>
{{ clock }}
</div>
</a>
<a
v-if="showEditProfile"
class="navbar-brand ms-0 flex-shrink-0"
:href="editProfileUrl"
>
<icon icon="edit" />
<span class="visually-hidden">{{ $gettext('Edit Profile') }}</span>
</a>
</div>
</div>
<sidebar-menu
:menu="menu"
:active="active"
/>
</template>
<script setup>
import {onMounted, ref} from "vue";
import Icon from "~/components/Common/Icon.vue";
import SidebarMenu from "~/components/Common/SidebarMenu.vue";
import {useAzuraCast, useAzuraCastStation} from "~/vendor/azuracast";
import {useIntervalFn} from "@vueuse/core";
const props = defineProps({
profileUrl: {
type: String,
required: true
},
editProfileUrl: {
type: String,
required: true
},
showEditProfile: {
type: Boolean,
default: false
},
menu: {
type: Object,
required: true
},
active: {
type: String,
default: null
}
});
const {timeConfig, localeWithDashes} = useAzuraCast();
const {name, timezone} = useAzuraCastStation();
const clock = ref('');
useIntervalFn(() => {
const d = new Date();
clock.value = d.toLocaleString(
localeWithDashes,
{
timeConfig,
timeZone: timezone,
timeStyle: 'long'
}
);
}, 1000, {
immediate: true,
immediateCallback: true
});
onMounted(() => {
document.addEventListener('station-needs-restart', () => {
document.querySelectorAll('.btn-restart-station').forEach((el) => {
el.classList.remove('d-none');
});
});
});
</script>

View File

@ -1,27 +1,18 @@
import {createApp, h} from "vue";
import {createApp} from "vue";
import installAxios from "~/vendor/axios";
import {installPinia} from '~/vendor/pinia';
import {installTranslate} from "~/vendor/gettext";
import Oruga from "@oruga-ui/oruga-next";
import {bootstrapConfig} from "@oruga-ui/theme-bootstrap";
import {installCurrentVueInstance} from "~/vendor/vueInstance";
import {installGlobalProps} from "~/vendor/azuracast";
export default function (component) {
const vueApp = createApp({
render() {
return h(component, this.$appProps)
},
});
export default function initApp(appConfig = {}) {
const vueApp = createApp(appConfig);
/* Track current instance (for programmatic use). */
installCurrentVueInstance(vueApp);
/* Gettext */
installTranslate(vueApp);
/* Axios */
installAxios(vueApp);
/* Pinia */
installPinia(vueApp);
@ -52,11 +43,19 @@ export default function (component) {
}
});
const vueComponent = (el, props) => {
vueApp.config.globalProperties.$appProps = props;
vueApp.mount(el);
}
window.vueComponent = (el, globalProps) => {
installGlobalProps(vueApp, globalProps);
window.vueComponent = vueComponent;
return vueComponent;
/* Gettext */
installTranslate(vueApp, globalProps.locale ?? 'en_US');
/* Axios */
installAxios(vueApp, globalProps.apiCsrf ?? null);
vueApp.mount(el);
};
return {
vueApp
};
}

View File

@ -0,0 +1,28 @@
import {useAzuraCast} from "~/vendor/azuracast";
import {Component, h} from "vue";
import PanelLayoutComponent from "~/components/PanelLayout.vue";
import Sidebar from "~/components/Admin/Sidebar.vue";
export default function useAdminPanelLayout(component: string | Component) {
return {
setup() {
const {panelProps, sidebarProps, componentProps} = useAzuraCast();
return {
panelProps,
sidebarProps,
componentProps
}
},
render() {
return h(
PanelLayoutComponent,
this.panelProps,
{
sidebar: () => h(Sidebar, this.sidebarProps),
default: () => h(component, this.componentProps)
}
);
}
}
}

View File

@ -0,0 +1,16 @@
import {useAzuraCast} from "~/vendor/azuracast";
import {Component, h} from "vue";
export default function useMinimalLayout(component: string | Component) {
return {
setup() {
const {componentProps} = useAzuraCast();
return {
componentProps
}
},
render() {
return h(component, this.componentProps);
}
}
}

View File

@ -0,0 +1,25 @@
import {useAzuraCast} from "~/vendor/azuracast";
import {Component, h} from "vue";
import PanelLayoutComponent from "~/components/PanelLayout.vue";
export default function usePanelLayout(component: string | Component) {
return {
setup() {
const {panelProps, componentProps} = useAzuraCast();
return {
panelProps,
componentProps
}
},
render() {
return h(
PanelLayoutComponent,
this.panelProps,
{
default: () => h(component, this.componentProps)
}
);
}
}
}

View File

@ -0,0 +1,28 @@
import {useAzuraCast} from "~/vendor/azuracast";
import {Component, h} from "vue";
import PanelLayoutComponent from "~/components/PanelLayout.vue";
import Sidebar from "~/components/Stations/Sidebar.vue";
export default function useStationPanelLayout(component: string | Component) {
return {
setup() {
const {panelProps, sidebarProps, componentProps} = useAzuraCast();
return {
panelProps,
sidebarProps,
componentProps
}
},
render() {
return h(
PanelLayoutComponent,
this.panelProps,
{
sidebar: () => h(Sidebar, this.sidebarProps),
default: () => h(component, this.componentProps)
}
);
}
}
}

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Account from '~/components/Account';
import initApp from "~/layout";
import usePanelLayout from "~/layouts/PanelLayout";
export default initBase(Account);
initApp(usePanelLayout(Account));

View File

@ -1,4 +1,5 @@
import initBase from '~/base.js';
import AdminApiKeys from '~/components/Admin/ApiKeys.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminApiKeys);
initApp(useAdminPanelLayout(AdminApiKeys));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AuditLog from '~/components/Admin/AuditLog.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AuditLog);
initApp(useAdminPanelLayout(AuditLog));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminBackups from '~/components/Admin/Backups.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminBackups);
initApp(useAdminPanelLayout(AdminBackups));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminBranding from '~/components/Admin/Branding.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminBranding);
initApp(useAdminPanelLayout(AdminBranding));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminCustomFields from '~/components/Admin/CustomFields.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminCustomFields);
initApp(useAdminPanelLayout(AdminCustomFields));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminDebug from '~/components/Admin/Debug.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminDebug);
initApp(useAdminPanelLayout(AdminDebug));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminGeoLite from '~/components/Admin/GeoLite.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminGeoLite);
initApp(useAdminPanelLayout(AdminGeoLite));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminIndex from '~/components/Admin/Index.vue';
import initApp from "~/layout";
import usePanelLayout from "~/layouts/PanelLayout";
export default initBase(AdminIndex);
initApp(usePanelLayout(AdminIndex));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminLogs from '~/components/Admin/Logs.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminLogs);
initApp(useAdminPanelLayout(AdminLogs));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminPermissions from '~/components/Admin/Permissions.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminPermissions);
initApp(useAdminPanelLayout(AdminPermissions));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminRelays from '~/components/Admin/Relays.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminRelays);
initApp(useAdminPanelLayout(AdminRelays));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminSettings from '~/components/Admin/Settings.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminSettings);
initApp(useAdminPanelLayout(AdminSettings));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminShoutcast from '~/components/Admin/Shoutcast.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminShoutcast);
initApp(useAdminPanelLayout(AdminShoutcast));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminStations from '~/components/Admin/Stations.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminStations);
initApp(useAdminPanelLayout(AdminStations));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminStereoTool from '~/components/Admin/StereoTool.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminStereoTool);
initApp(useAdminPanelLayout(AdminStereoTool));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import StorageLocations from '~/components/Admin/StorageLocations.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(StorageLocations);
initApp(useAdminPanelLayout(StorageLocations));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminUpdates from '~/components/Admin/Updates.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminUpdates);
initApp(useAdminPanelLayout(AdminUpdates));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import AdminUsers from '~/components/Admin/Users.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
export default initBase(AdminUsers);
initApp(useAdminPanelLayout(AdminUsers));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import initApp from "~/layout";
import usePanelLayout from "~/layouts/PanelLayout";
import Dashboard from "~/components/Dashboard.vue";
import Dashboard from '~/components/Dashboard';
export default initBase(Dashboard);
initApp(usePanelLayout(Dashboard));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import FullPlayer from '~/components/Public/FullPlayer.vue';
import initApp from "~/layout";
import useMinimalLayout from "~/layouts/MinimalLayout";
export default initBase(FullPlayer);
initApp(useMinimalLayout(FullPlayer));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import History from '~/components/Public/History.vue';
import initApp from "~/layout";
import useMinimalLayout from "~/layouts/MinimalLayout";
export default initBase(History);
initApp(useMinimalLayout(History));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import OnDemand from '~/components/Public/OnDemand.vue';
import initApp from "~/layout";
import useMinimalLayout from "~/layouts/MinimalLayout";
export default initBase(OnDemand);
initApp(useMinimalLayout(OnDemand));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Player from '~/components/Public/Player.vue';
import initApp from "~/layout";
import useMinimalLayout from "~/layouts/MinimalLayout";
export default initBase(Player);
initApp(useMinimalLayout(Player));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Requests from '~/components/Public/Requests.vue';
import initApp from "~/layout";
import useMinimalLayout from "~/layouts/MinimalLayout";
export default initBase(Requests);
initApp(useMinimalLayout(Requests));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Schedule from '~/components/Public/Schedule.vue';
import initApp from "~/layout";
import useMinimalLayout from "~/layouts/MinimalLayout";
export default initBase(Schedule);
initApp(useMinimalLayout(Schedule));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import WebDJ from '~/components/Public/WebDJ.vue';
import initApp from "~/layout";
import useMinimalLayout from "~/layouts/MinimalLayout";
export default initBase(WebDJ);
initApp(useMinimalLayout(WebDJ));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Recover from '~/components/Recover.vue';
import initApp from "~/layout";
import useMinimalLayout from "~/layouts/MinimalLayout";
export default initBase(Recover);
initApp(useMinimalLayout(Recover));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import SetupRegister from '~/components/Setup/Register.vue';
import initApp from "~/layout";
import useMinimalLayout from "~/layouts/MinimalLayout";
export default initBase(SetupRegister);
initApp(useMinimalLayout(SetupRegister));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import SetupSettings from '~/components/Setup/Settings.vue';
import initApp from "~/layout";
import usePanelLayout from "~/layouts/PanelLayout";
export default initBase(SetupSettings);
initApp(usePanelLayout(SetupSettings));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import SetupStation from '~/components/Setup/Station.vue';
import initApp from "~/layout";
import usePanelLayout from "~/layouts/PanelLayout";
export default initBase(SetupStation);
initApp(usePanelLayout(SetupStation));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import StationsBranding from '~/components/Stations/Branding.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(StationsBranding);
initApp(useStationPanelLayout(StationsBranding));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import BulkMedia from '~/components/Stations/BulkMedia.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(BulkMedia);
initApp(useStationPanelLayout(BulkMedia));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Fallback from '~/components/Stations/Fallback.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Fallback);
initApp(useStationPanelLayout(Fallback));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Help from '~/components/Stations/Help.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Help);
initApp(useStationPanelLayout(Help));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import HlsStreams from '~/components/Stations/HlsStreams.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(HlsStreams);
initApp(useStationPanelLayout(HlsStreams));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import LiquidsoapConfig from '~/components/Stations/LiquidsoapConfig.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(LiquidsoapConfig);
initApp(useStationPanelLayout(LiquidsoapConfig));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Media from '~/components/Stations/Media.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Media);
initApp(useStationPanelLayout(Media));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Mounts from '~/components/Stations/Mounts.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Mounts);
initApp(useStationPanelLayout(Mounts));

View File

@ -1,7 +1,7 @@
import initBase from '~/base.js';
import '~/store';
import Playlists from '~/components/Stations/Playlists.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Playlists);
initApp(useStationPanelLayout(Playlists));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Podcasts from '~/components/Stations/Podcasts.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Podcasts);
initApp(useStationPanelLayout(Podcasts));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Profile from '~/components/Stations/Profile.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Profile);
initApp(useStationPanelLayout(Profile));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import ProfileEdit from '~/components/Stations/ProfileEdit.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(ProfileEdit);
initApp(useStationPanelLayout(ProfileEdit));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Queue from '~/components/Stations/Queue.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Queue);
initApp(useStationPanelLayout(Queue));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Remotes from '~/components/Stations/Remotes.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Remotes);
initApp(useStationPanelLayout(Remotes));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Listeners from '~/components/Stations/Reports/Listeners.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Listeners);
initApp(useStationPanelLayout(Listeners));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Overview from '~/components/Stations/Reports/Overview.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Overview);
initApp(useStationPanelLayout(Overview));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Requests from '~/components/Stations/Reports/Requests.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Requests);
initApp(useStationPanelLayout(Requests));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import SoundExchange from '~/components/Stations/Reports/SoundExchange.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(SoundExchange);
initApp(useStationPanelLayout(SoundExchange));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Timeline from '~/components/Stations/Reports/Timeline.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Timeline);
initApp(useStationPanelLayout(Timeline));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Restart from '~/components/Stations/Restart.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Restart);
initApp(useStationPanelLayout(Restart));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import SftpUsers from "~/components/Stations/SftpUsers";
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(SftpUsers);
initApp(useStationPanelLayout(SftpUsers));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import StereoToolConfig from '~/components/Stations/StereoToolConfig.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(StereoToolConfig);
initApp(useStationPanelLayout(StereoToolConfig));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Streamers from '~/components/Stations/Streamers.vue';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Streamers);
initApp(useStationPanelLayout(Streamers));

View File

@ -1,5 +1,5 @@
import initBase from '~/base.js';
import Webhooks from '~/components/Stations/Webhooks';
import initApp from "~/layout";
import useStationPanelLayout from "~/layouts/StationPanelLayout";
export default initBase(Webhooks);
initApp(useStationPanelLayout(Webhooks));

View File

@ -1,10 +1,8 @@
import axios, { AxiosStatic } from "axios";
import axios, {AxiosStatic} from "axios";
import VueAxios from "vue-axios";
import {App, inject} from "vue";
import {useAzuraCast} from "~/vendor/azuracast";
import {App, inject, InjectionKey} from "vue";
import {useTranslate} from "~/vendor/gettext";
import {useNotify} from "~/functions/useNotify";
import {InjectionKey} from "vue";
const injectKey: InjectionKey<AxiosStatic> = Symbol() as InjectionKey<AxiosStatic>;
@ -17,11 +15,9 @@ export const useAxios = (): UseAxios => ({
axios: inject<AxiosStatic>(injectKey)
});
export default function installAxios(vueApp: App) {
export default function installAxios(vueApp: App, apiCsrf: string | null) {
// Configure auto-CSRF on requests
const {apiCsrf} = useAzuraCast();
if (typeof apiCsrf !== 'undefined') {
if (apiCsrf) {
axios.defaults.headers.common['X-API-CSRF'] = apiCsrf;
}

View File

@ -1,23 +1,11 @@
/* eslint-disable no-undef */
interface AzuraCastConstants {
locale: string,
localeShort: string,
localeWithDashes: string,
timeConfig: object,
apiCsrf: string | null,
enableAdvancedFeatures: boolean
}
import {App, inject, InjectionKey} from "vue";
export function useAzuraCast(): AzuraCastConstants {
return {
locale: App.locale ?? 'en_US',
localeShort: App.locale_short ?? 'en',
localeWithDashes: App.locale_with_dashes ?? 'en-US',
timeConfig: App.time_config ?? {},
apiCsrf: App.api_csrf ?? null,
enableAdvancedFeatures: App.enable_advanced_features ?? true
}
const globalPropsKey: InjectionKey<AzuraCastConstants> = Symbol() as InjectionKey<AzuraCastConstants>;
export function installGlobalProps(vueApp: App, globalProps: AzuraCastConstants): void {
vueApp.provide(globalPropsKey, globalProps);
}
interface AzuraCastStationConstants {
@ -27,11 +15,24 @@ interface AzuraCastStationConstants {
timezone: string
}
export function useAzuraCastStation(): AzuraCastStationConstants {
return {
id: App.station?.id ?? null,
name: App.station?.name ?? null,
shortName: App.station?.shortName ?? null,
timezone: App.station?.timezone ?? 'UTC'
}
interface AzuraCastConstants {
locale: string,
localeShort: string,
localeWithDashes: string,
timeConfig: object,
apiCsrf: string | null,
enableAdvancedFeatures: boolean,
panelProps: object | null,
sidebarProps: object | null,
componentProps: object | null,
station: AzuraCastStationConstants | null,
}
export function useAzuraCast(): AzuraCastConstants {
return inject(globalPropsKey);
}
export function useAzuraCastStation(): AzuraCastStationConstants {
const {station} = useAzuraCast();
return station;
}

View File

@ -1,28 +1,27 @@
import {createGettext, Language} from "vue3-gettext";
import {useAzuraCast} from "~/vendor/azuracast";
import {App} from "vue";
const {locale} = useAzuraCast();
const gettext = createGettext({
defaultLanguage: locale,
translations: {},
silent: true
});
const translations = import.meta.glob('../../../translations/**/translations.json', {as: 'json'});
const localePath = '../../../translations/' + locale + '.UTF-8/translations.json';
if (localePath in translations) {
translations[localePath]().then((data) => {
gettext.translations = data;
});
}
let gettext;
export function useTranslate(): Language {
return gettext;
}
export function installTranslate(vueApp: App): void {
export function installTranslate(vueApp: App, locale: string): void {
gettext = createGettext({
defaultLanguage: locale,
translations: {},
silent: true
});
const translations = import.meta.glob('../../../translations/**/translations.json', {as: 'json'});
const localePath = '../../../translations/' + locale + '.UTF-8/translations.json';
if (localePath in translations) {
translations[localePath]().then((data) => {
gettext.translations = data;
});
}
vueApp.use(gettext);
}

View File

@ -1,8 +1,6 @@
import {DateTime, Duration, Settings} from 'luxon';
import {useAzuraCast} from "~/vendor/azuracast";
const {localeWithDashes, timeConfig} = useAzuraCast();
interface TimestampToRelative {
(timestamp: number | null | undefined): string;
}
@ -14,6 +12,7 @@ interface UseLuxon {
}
export function useLuxon(): UseLuxon {
const {localeWithDashes, timeConfig} = useAzuraCast();
Settings.defaultLocale = localeWithDashes;
const timestampToRelative: TimestampToRelative = (timestamp: number | null | undefined): string => {

View File

@ -2,22 +2,22 @@ import Swal from 'sweetalert2/dist/sweetalert2';
import {useTranslate} from "~/vendor/gettext";
import {Directive} from "vue";
const {$gettext} = useTranslate();
const swalCustom = Swal.mixin({
confirmButtonText: $gettext('Confirm'),
cancelButtonText: $gettext('Cancel'),
showCancelButton: true,
});
const swalConfirmDelete = swalCustom.mixin({
title: $gettext('Delete Record?'),
confirmButtonText: $gettext('Delete'),
confirmButtonColor: '#e64942',
focusCancel: true
});
export function useSweetAlert() {
const {$gettext} = useTranslate();
const swalCustom = Swal.mixin({
confirmButtonText: $gettext('Confirm'),
cancelButtonText: $gettext('Cancel'),
showCancelButton: true,
});
const swalConfirmDelete = swalCustom.mixin({
title: $gettext('Delete Record?'),
confirmButtonText: $gettext('Delete'),
confirmButtonColor: '#e64942',
focusCancel: true
});
const showAlert = (options = {}) => {
return swalCustom.fire(options);
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin\Debug;
namespace App\Controller\Admin;
use App\Cache\DatabaseCache;
use App\Console\Command\Sync\SingleTaskCommand;
@ -19,7 +19,7 @@ use DateTimeZone;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
final class IndexAction implements SingleActionInterface
final class DebugAction implements SingleActionInterface
{
public function __construct(
private readonly StationRepository $stationRepo,
@ -42,7 +42,7 @@ final class IndexAction implements SingleActionInterface
'name' => $queue->value,
'count' => $this->queueManager->getQueueCount($queue),
'url' => $router->named(
'admin:debug:clear-queue',
'api:admin:debug:clear-queue',
['queue' => $queue->value]
),
];
@ -65,7 +65,7 @@ final class IndexAction implements SingleActionInterface
'time' => $this->cache->getItem($cacheKey)->get() ?? 0,
'nextRun' => $cronExpression->getNextRunDate($now)->getTimestamp(),
'url' => $router->named(
'admin:debug:sync',
'api:admin:debug:sync',
['task' => rawurlencode($task)]
),
];
@ -77,15 +77,15 @@ final class IndexAction implements SingleActionInterface
'id' => $station['id'],
'name' => $station['name'],
'clearQueueUrl' => $router->named(
'admin:debug:clear-station-queue',
'api:admin:debug:clear-station-queue',
['station_id' => $station['id']]
),
'getNextSongUrl' => $router->named(
'admin:debug:nextsong',
'api:admin:debug:nextsong',
['station_id' => $station['id']]
),
'getNowPlayingUrl' => $router->named(
'admin:debug:nowplaying',
'api:admin:debug:nowplaying',
['station_id' => $station['id']]
),
];
@ -97,8 +97,8 @@ final class IndexAction implements SingleActionInterface
id: 'admin-debug',
title: __('System Debugger'),
props: [
'clearCacheUrl' => $router->named('admin:debug:clear-cache'),
'clearQueuesUrl' => $router->named('admin:debug:clear-queue'),
'clearCacheUrl' => $router->named('api:admin:debug:clear-cache'),
'clearQueuesUrl' => $router->named('api:admin:debug:clear-queue'),
'syncTasks' => $syncTasks,
'queueTotals' => $queueTotals,
'stations' => $stations,

View File

@ -2,10 +2,11 @@
declare(strict_types=1);
namespace App\Controller\Admin\Debug;
namespace App\Controller\Api\Admin\Debug;
use App\Console\Application;
use App\Controller\SingleActionInterface;
use App\Entity\Api\Status;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@ -26,9 +27,9 @@ final class ClearCacheAction implements SingleActionInterface
'cache:clear'
);
// Flash an update to ensure the session is recreated.
$request->getFlash()->success($resultOutput);
// TODO Flash an update to ensure the session is recreated.
// $request->getFlash()->success($resultOutput);
return $response->withRedirect($request->getRouter()->fromHere('admin:debug:index'));
return $response->withJson(Status::updated());
}
}

View File

@ -2,9 +2,10 @@
declare(strict_types=1);
namespace App\Controller\Admin\Debug;
namespace App\Controller\Api\Admin\Debug;
use App\Controller\SingleActionInterface;
use App\Entity\Api\Status;
use App\Http\Response;
use App\Http\ServerRequest;
use App\MessageQueue\QueueManagerInterface;
@ -34,9 +35,6 @@ final class ClearQueueAction implements SingleActionInterface
$this->queueManager->clearAllQueues();
}
// Flash an update to ensure the session is recreated.
$request->getFlash()->success(__('Message queue cleared.'));
return $response->withRedirect($request->getRouter()->fromHere('admin:debug:index'));
return $response->withJson(Status::updated());
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin\Debug;
namespace App\Controller\Api\Admin\Debug;
use App\Container\LoggerAwareTrait;
use App\Controller\SingleActionInterface;
@ -42,14 +42,8 @@ final class ClearStationQueueAction implements SingleActionInterface
$this->logger->popHandler();
}
return $request->getView()->renderToResponse(
$response,
'system/log_view',
[
'sidebar' => null,
'title' => __('Debug Output'),
'log_records' => $testHandler->getRecords(),
]
);
return $response->withJson([
'logs' => $testHandler->getRecords(),
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin\Debug;
namespace App\Controller\Api\Admin\Debug;
use App\Container\LoggerAwareTrait;
use App\Controller\SingleActionInterface;
@ -39,14 +39,8 @@ final class NextSongAction implements SingleActionInterface
]);
$this->logger->popHandler();
return $request->getView()->renderToResponse(
$response,
'system/log_view',
[
'sidebar' => null,
'title' => __('Debug Output'),
'log_records' => $testHandler->getRecords(),
]
);
return $response->withJson([
'logs' => $testHandler->getRecords(),
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin\Debug;
namespace App\Controller\Api\Admin\Debug;
use App\Container\LoggerAwareTrait;
use App\Controller\SingleActionInterface;
@ -37,14 +37,8 @@ final class NowPlayingAction implements SingleActionInterface
$this->logger->popHandler();
}
return $request->getView()->renderToResponse(
$response,
'system/log_view',
[
'sidebar' => null,
'title' => __('Debug Output'),
'log_records' => $testHandler->getRecords(),
]
);
return $response->withJson([
'logs' => $testHandler->getRecords(),
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin\Debug;
namespace App\Controller\Api\Admin\Debug;
use App\Console\Command\Sync\SingleTaskCommand;
use App\Container\LoggerAwareTrait;
@ -51,14 +51,8 @@ final class SyncAction implements SingleActionInterface
$this->logger->popHandler();
}
return $request->getView()->renderToResponse(
$response,
'system/log_view',
[
'sidebar' => null,
'title' => __('Debug Output'),
'log_records' => $testHandler->getRecords(),
]
);
return $response->withJson([
'logs' => $testHandler->getRecords(),
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin\Debug;
namespace App\Controller\Api\Admin\Debug;
use App\Container\LoggerAwareTrait;
use App\Controller\SingleActionInterface;
@ -50,14 +50,8 @@ final class TelnetAction implements SingleActionInterface
$this->logger->popHandler();
return $request->getView()->renderToResponse(
$response,
'system/log_view',
[
'sidebar' => null,
'title' => __('Debug Output'),
'log_records' => $testHandler->getRecords(),
]
);
return $response->withJson([
'logs' => $testHandler->getRecords(),
]);
}
}

View File

@ -42,22 +42,15 @@ final class Admin
$activeTab = $routeParts[1];
}
$view->addData(
[
'admin_panels' => $event->getFilteredMenu(),
]
);
$globalProps = $view->getGlobalProps();
// These two intentionally separated (the sidebar needs admin_panels).
$view->getSections()->set(
'sidebar',
$view->render(
'admin/sidebar',
[
'active_tab' => $activeTab,
]
)
);
$router = $request->getRouter();
$globalProps->set('sidebarProps', [
'homeUrl' => $router->named('admin:index:index'),
'menu' => $event->getFilteredMenu(),
'active' => $activeTab,
]);
return $handler->handle($request);
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Middleware\Module;
use App\Container\EnvironmentAwareTrait;
use App\Enums\GlobalPermissions;
use App\Http\ServerRequest;
use App\Middleware\Auth\ApiAuth;
use App\Version;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use const PHP_MAJOR_VERSION;
use const PHP_MINOR_VERSION;
final class PanelLayout
{
use EnvironmentAwareTrait;
public function __construct(
private readonly Version $version
) {
}
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
$view = $request->getView();
$customization = $request->getCustomization();
$user = $request->getUser();
$auth = $request->getAuth();
$acl = $request->getAcl();
$router = $request->getRouter();
$globalProps = $view->getGlobalProps();
$csrf = $request->getCsrf();
$globalProps->set('apiCsrf', $csrf->generate(ApiAuth::API_CSRF_NAMESPACE));
$globalProps->set('panelProps', [
'instanceName' => $customization->getInstanceName(),
'userDisplayName' => $user->getDisplayName(),
'homeUrl' => $router->named('dashboard'),
'adminUrl' => $router->named('admin:index:index'),
'profileUrl' => $router->named('profile:index'),
'logoutUrl' => ($auth->isMasqueraded())
? $router->named('account:endmasquerade')
: $router->named('account:logout'),
'showAdmin' => $acl->isAllowed(GlobalPermissions::View),
'version' => $this->version->getVersionText(),
'platform' => ($this->environment->isDocker() ? 'Docker' : 'Ansible')
. ' &bull; PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
]);
return $handler->handle($request);
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Middleware\Module;
use App\Container\SettingsAwareTrait;
use App\Enums\StationPermissions;
use App\Event;
use App\Http\ServerRequest;
use Psr\EventDispatcher\EventDispatcherInterface;
@ -48,16 +49,25 @@ final class Stations
$activeTab = $routeParts[1];
}
$view->getSections()->set(
'sidebar',
$view->render(
'stations/sidebar',
[
'menu' => $event->getFilteredMenu(),
'active' => $activeTab,
]
),
);
$globalProps = $view->getGlobalProps();
$globalProps->set('station', [
'id' => $station->getIdRequired(),
'name' => $station->getName(),
'shortName' => $station->getShortName(),
'timezone' => $station->getTimezone(),
]);
$router = $request->getRouter();
$acl = $request->getAcl();
$globalProps->set('sidebarProps', [
'profileUrl' => $router->fromHere('stations:profile:index'),
'editProfileUrl' => $router->fromHere('stations:profile:edit'),
'showEditProfile' => $acl->isAllowed(StationPermissions::Profile, $station),
'menu' => $event->getFilteredMenu(),
'active' => $activeTab,
]);
return $handler->handle($request);
}

View File

@ -4,16 +4,20 @@ declare(strict_types=1);
namespace App;
use App\Entity\User;
use App\Enums\SupportedLocales;
use App\Http\RouterInterface;
use App\Http\ServerRequest;
use App\Traits\RequestAwareTrait;
use App\Utilities\Json;
use App\View\GlobalSections;
use Doctrine\Common\Collections\ArrayCollection;
use League\Plates\Engine;
use League\Plates\Template\Data;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use stdClass;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
@ -21,7 +25,10 @@ final class View extends Engine
{
use RequestAwareTrait;
private readonly GlobalSections $sections;
private GlobalSections $sections;
/** @var ArrayCollection<string, array|object|string|int> */
private ArrayCollection $globalProps;
public function __construct(
Customization $customization,
@ -33,11 +40,13 @@ final class View extends Engine
parent::__construct($environment->getViewsDirectory(), 'phtml');
$this->sections = new GlobalSections();
$this->globalProps = new ArrayCollection();
// Add non-request-dependent content.
$this->addData(
[
'sections' => $this->sections,
'globalProps' => $this->globalProps,
'customization' => $customization,
'environment' => $environment,
'version' => $version,
@ -94,13 +103,6 @@ final class View extends Engine
'prefetch' => [],
];
$assetRoot = '/static/vite_dist';
$includes = [
'js' => $assetRoot . '/' . $vueComponents[$componentPath]['file'],
'css' => [],
'prefetch' => [],
];
$visitedNodes = [];
$fetchCss = function ($component) use (
$vueComponents,
@ -166,9 +168,40 @@ final class View extends Engine
$customization = $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION);
if (null !== $customization) {
$requestData['customization'] = $customization;
$this->globalProps->set(
'enableAdvancedFeatures',
$customization->enableAdvancedFeatures()
);
}
$this->addData($requestData);
$localeObj = $request->getAttribute(ServerRequest::ATTR_LOCALE);
if (!($localeObj instanceof SupportedLocales)) {
$localeObj = SupportedLocales::default();
}
$locale = $localeObj->getLocaleWithoutEncoding();
$localeShort = substr($locale, 0, 2);
$localeWithDashes = str_replace('_', '-', $locale);
$this->globalProps->set('locale', $locale);
$this->globalProps->set('localeShort', $localeShort);
$this->globalProps->set('localeWithDashes', $localeWithDashes);
// User profile-specific 24-hour display setting.
$userObj = $request->getAttribute(ServerRequest::ATTR_USER);
$show24Hours = ($userObj instanceof User)
? $userObj->getShow24HourTime()
: null;
$timeConfig = new stdClass();
if (null !== $show24Hours) {
$timeConfig->hour12 = !$show24Hours;
}
$this->globalProps->set('timeConfig', $timeConfig);
}
}
@ -177,8 +210,16 @@ final class View extends Engine
return $this->sections;
}
/** @return ArrayCollection<string, array|object|string|int> */
public function getGlobalProps(): ArrayCollection
{
return $this->globalProps;
}
public function reset(): void
{
$this->sections = new GlobalSections();
$this->globalProps = new ArrayCollection();
$this->data = new Data();
}
@ -213,7 +254,7 @@ final class View extends Engine
ResponseInterface $response,
string $component,
?string $id = null,
string $layout = 'main',
string $layout = 'panel',
?string $title = null,
array $layoutParams = [],
array $props = [],

View File

@ -1,15 +0,0 @@
<?php
/**
* @var App\Http\RouterInterface $router
* @var array $admin_panels
*/
?>
<div class="navdrawer-header">
<a class="navbar-brand px-0" href="<?= $router->named('admin:index:index') ?>">
<?= __('Administration') ?>
</a>
</div>
<?= $this->fetch('partials/sidebar_menu', ['menu' => $admin_panels]) ?>

View File

@ -1,196 +0,0 @@
<?php
/**
* @var League\Plates\Template\Template $this
* @var App\Auth $auth
* @var App\Acl $acl
* @var App\Http\Router $router
* @var App\Session\Flash $flash
* @var App\Customization $customization
* @var App\Version $version
* @var App\Http\ServerRequest $request
* @var App\Environment $environment
* @var App\Entity\User $user
* @var App\View\GlobalSections $sections
*/
$manual ??= false;
$title ??= null;
$header ??= null;
?>
<!DOCTYPE html>
<html lang="<?= $customization->getLocale()->getHtmlLang() ?>">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= $this->e($customization->getPageTitle($title)) ?></title>
<?= $this->fetch('partials/head') ?>
<?= $sections->get('head') ?>
<style>
<?=$customization->getCustomInternalCss() ?>
</style>
</head>
<body class="page-full <?= $page_class ?? '' ?> <?php
if ($sections->has('sidebar')): ?>has-sidebar<?php
endif; ?>">
<?= $this->fetch('partials/bodyjs', [
'include_csrf' => true,
]) ?>
<?= $sections->get('bodyjs') ?>
<a class="visually-hidden-focusable" href="#content"><?= __('Skip to main content') ?></a>
<header class="navbar bg-primary-dark shadow-sm fixed-top">
<?php
if ($sections->has('sidebar')): ?>
<button data-bs-toggle="offcanvas" data-bs-target="#sidebar"
aria-controls="sidebar" aria-expanded="false" aria-label="<?= __(
'Toggle Sidebar'
) ?>" id="navbar-toggle" class="navbar-toggler d-inline-flex d-lg-none me-3">
<i class="material-icons lg" aria-hidden="true">menu</i>
</button>
<?php
endif; ?>
<a class="navbar-brand ms-0 me-auto" href="<?= $router->named('dashboard') ?>">
azura<strong>cast</strong> <?php
if (!empty($customization->getInstanceName())): ?>
<small><?= $this->e($customization->getInstanceName()) ?></small><?php
endif; ?>
</a>
<div id="radio-player-controls"></div>
<div class="dropdown ms-3 d-inline-flex align-items-center">
<div class="me-2">
<?= $this->e($user->getDisplayName()) ?>
</div>
<button aria-expanded="false" aria-haspopup="true" class="navbar-toggler" aria-label="<?= __(
'Toggle Menu'
) ?>" data-bs-toggle="dropdown" type="button">
<i class="material-icons lg" aria-hidden="true">menu_open</i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="<?= $router->named('dashboard') ?>">
<i class="material-icons" aria-hidden="true">home</i>
<?= __('Dashboard') ?>
</a>
</li>
<li class="dropdown-divider">&nbsp;</li>
<?php
if ($acl->isAllowed(App\Enums\GlobalPermissions::View)): ?>
<li>
<a class="dropdown-item" href="<?= $router->named('admin:index:index') ?>">
<i class="material-icons" aria-hidden="true">settings</i>
<?= __('System Administration') ?>
</a>
</li>
<?php
endif; ?>
<li>
<a class="dropdown-item" href="<?= $router->named('profile:index') ?>">
<i class="material-icons" aria-hidden="true">account_circle</i>
<?= __('My Account') ?>
</a>
</li>
<li>
<a class="dropdown-item theme-switcher" href="javascript:">
<i class="material-icons" aria-hidden="true">invert_colors</i>
<?= __('Switch Theme') ?>
</a>
</li>
<li>
<a class="dropdown-item" href="https://docs.azuracast.com/en/user-guide/troubleshooting"
target="_blank">
<i class="material-icons" aria-hidden="true">help</i>
<?= __('Help') ?>
</a>
</li>
<li class="dropdown-divider">&nbsp;</li>
<?php
if ($auth->isMasqueraded()): ?>
<li>
<a class="dropdown-item" href="<?= $router->named('account:endmasquerade') ?>">
<i class="material-icons" aria-hidden="true">exit_to_app</i>
<?= __('End Session') ?>
</a>
</li>
<?php
else: ?>
<li>
<a class="dropdown-item" href="<?= $router->named('account:logout') ?>">
<i class="material-icons" aria-hidden="true">exit_to_app</i>
<?= __('Sign Out') ?></a></li>
<?php
endif; ?>
</ul>
</div>
</header>
<?php
if ($sections->has('sidebar')): ?>
<nav class="navdrawer offcanvas offcanvas-start" id="sidebar" tabindex="-1" aria-label="<?= __('Sidebar') ?>">
<?= $sections->get('sidebar') ?>
</nav>
<?php
endif; ?>
<section id="main">
<main id="content" <?php
if (!$sections->has('sidebar')): ?>class="content-alt"<?php
endif; ?>>
<div class="container">
<?php
if ($manual): ?>
<?= $this->section('content') ?>
<?php
else: ?>
<?php
if ($header): ?>
<div class="block-header">
<h2><?= $header ?></h2>
</div>
<?php
endif; ?>
<div class="card mb-3" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title"><?=$this->e($title) ?></h3>
</div>
<div class="card-body">
<?=$this->section('content')?>
</div>
</div>
<?php
endif; ?>
</div>
</main>
</section>
<footer id="footer" <?php
if (!$sections->has('sidebar')): ?>class="footer-alt"<?php
endif; ?> role="contentinfo" aria-label="<?= __('Footer') ?>">
<?= sprintf(
__('Powered by %s'),
'<a href="https://www.azuracast.com/" target="_blank">' . $environment->getAppName(
) . '</a> &bull; ' . $version->getVersionText() . ' &bull; ' . ($environment->isDocker(
) ? 'Docker' : 'Ansible') . ' &bull; PHP ' . \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION
) ?>
<br>
<?= __('Like our software?') ?> <a href="https://docs.azuracast.com/en/contribute/donate"><?= __(
'Donate to support AzuraCast!'
) ?></a>
</footer>
<div id="radio-player"></div>
<?= $this->fetch('partials/toasts') ?>
</body>
</html>

View File

@ -38,8 +38,6 @@ $hide_footer ??= false;
<body class="page-minimal <?= $page_class ?? '' ?>">
<?= $this->fetch('partials/bodyjs') ?>
<?= $sections->get('bodyjs') ?>
<script>

43
templates/panel.phtml Normal file
View File

@ -0,0 +1,43 @@
<?php
/**
* @var League\Plates\Template\Template $this
* @var App\Auth $auth
* @var App\Acl $acl
* @var App\Http\Router $router
* @var App\Session\Flash $flash
* @var App\Customization $customization
* @var App\Version $version
* @var App\Http\ServerRequest $request
* @var App\Environment $environment
* @var App\Entity\User $user
* @var App\View\GlobalSections $sections
*/
$manual ??= false;
$title ??= null;
$header ??= null;
?>
<!DOCTYPE html>
<html lang="<?= $customization->getLocale()->getHtmlLang() ?>">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= $this->e($customization->getPageTitle($title)) ?></title>
<?= $this->fetch('partials/head') ?>
<?= $sections->get('head') ?>
<style>
<?=$customization->getCustomInternalCss() ?>
</style>
</head>
<body class="page-full">
<?= $sections->get('bodyjs') ?>
<?= $this->section('content') ?>
<?= $this->fetch('partials/toasts') ?>
</body>
</html>

View File

@ -1,47 +0,0 @@
<?php
/** @var App\Customization $customization */
/** @var Psr\Http\Message\RequestInterface $request */
$localeObj = $request->getAttribute(App\Http\ServerRequest::ATTR_LOCALE);
if (!($localeObj instanceof App\Enums\SupportedLocales)) {
$localeObj = App\Enums\SupportedLocales::default();
}
$locale = $localeObj->getLocaleWithoutEncoding();
$localeShort = substr($locale, 0, 2);
$localeWithDashes = str_replace('_', '-', $locale);
// User profile-specific 24-hour display setting.
$userObj = $request->getAttribute(App\Http\ServerRequest::ATTR_USER);
$show24Hours = ($userObj instanceof App\Entity\User)
? $userObj->getShow24HourTime()
: null;
$timeConfig = new \stdClass();
if (null !== $show24Hours) {
$timeConfig->hour12 = !$show24Hours;
}
// CSRF token
$csrf = null;
if (($include_csrf ?? false) === true) {
$csrf = $request->getAttribute(App\Http\ServerRequest::ATTR_SESSION_CSRF);
if ($csrf instanceof App\Session\Csrf) {
$csrf = $csrf->generate(App\Middleware\Auth\ApiAuth::API_CSRF_NAMESPACE);
}
}
$app = [
'locale' => $locale,
'locale_short' => $localeShort,
'locale_with_dashes' => $localeWithDashes,
'time_config' => $timeConfig,
'api_csrf' => $csrf,
'enable_advanced_features' => $customization->enableAdvancedFeatures(),
];
?>
<script type="text/javascript">
var App = <?=json_encode($app, JSON_THROW_ON_ERROR) ?>;
</script>

View File

@ -1,67 +0,0 @@
<?php
/**
* @var array $menu
* @var string $active
* @var \App\Http\Router $router
* @var \App\View\GlobalSections $sections
*/
$active ??= null;
?>
<ul class="offcanvas-body navdrawer-nav">
<?php
foreach ($menu as $category_id => $category): ?>
<li class="nav-item">
<a class="nav-link <?= ($category['class'] ?? '') ?> <?php
if ($active === $category_id): ?>active<?php
endif; ?>"
<?php
if (empty($category['items'])): ?>href="<?= $category['url'] ?>" <?php
else: ?>data-bs-toggle="collapse" href="#sidebar-submenu-<?= $category_id ?>"<?php
endif; ?>
<?php
if ($category['external'] ?? false): ?>target="_blank"<?php
endif; ?>
<?php
if (isset($category['title'])): ?>title="<?= $this->e($category['title']) ?>"<?php
endif; ?>>
<i class="navdrawer-nav-icon material-icons" aria-hidden="true"><?= $category['icon'] ?></i>
<span>
<?= $category['label'] ?>
</span>
<?php
if ($category['external'] ?? false): ?>
<i class="material-icons sm ms-2" aria-label="<?= $this->e(__('External')) ?>">open_in_new</i>
<?php
endif; ?>
</a>
<?php
if (!empty($category['items'])): ?>
<div class="collapse pb-2" id="sidebar-submenu-<?= $category_id ?>">
<ul class="navdrawer-nav">
<?php
foreach ($category['items'] as $item_id => $item): ?>
<li class="nav-item">
<a class="nav-link ps-4 py-2 <?= ($item['class'] ?? '') ?>"
href="<?= $item['url'] ?>"
<?php
if ($item['external'] ?? false): ?>target="_blank"<?php
endif; ?>
<?php
if (isset($item['title'])): ?>title="<?= $this->e($item['title']) ?>"<?php
endif; ?>>
<?= $item['label'] ?>
<?php
if ($item['external'] ?? false): ?>
<i class="material-icons sm ms-2">open_in_new</i>
<?php
endif; ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>

View File

@ -4,11 +4,13 @@
* @var ?string $id
* @var array $props
* @var App\View\GlobalSections $sections
* @var Doctrine\Common\Collections\ArrayCollection $globalProps
*/
$componentDeps = $this->getVueComponentInfo('vue/pages/' . $component . '.js');
$propsJson = json_encode($props, JSON_THROW_ON_ERROR);
$globalProps->set('componentProps', $props);
$propsJson = json_encode($globalProps->toArray(), JSON_THROW_ON_ERROR);
$headScripts = [];

View File

@ -1,68 +0,0 @@
<?php
/**
* @var App\Entity\Station $station
* @var App\Acl $acl
* @var App\View\GlobalSections $sections
*/
$sections->appendStart('bodyjs');
?>
<script>
App.station = <?=$this->escapeJs([
'id' => $station->getIdRequired(),
'name' => $station->getName(),
'shortName' => $station->getShortName(),
'timezone' => $station->getTimezone(),
]); ?>;
ready(() => {
// Show the "Restart to Apply Changes" link
document.addEventListener('station-needs-restart', () => {
document.querySelectorAll('.btn-restart-station').forEach((el) => {
el.classList.remove('d-none');
});
});
// Update the clock in the header
const updateClock = () => {
let d = new Date();
document.querySelector("#station-time").textContent = d.toLocaleString(
App.locale_with_dashes,
{
...App.time_config,
timeZone: App.station.timezone,
timeStyle: 'long'
}
);
}
setInterval(updateClock, 1000);
updateClock();
});
</script>
<?php
$sections->end();
?>
<div class="navdrawer-header offcanvas-header">
<div class="d-flex align-items-center">
<a class="navbar-brand px-0 flex-fill" href="<?= $router->fromHere('stations:profile:index') ?>">
<div><?= $this->e($station->getName()) ?></div>
<div class="fs-6" id="station-time" title="<?= $this->e(__('Station Time')) ?>">
<?= date('H:i:s T') ?>
</div>
</a>
<?php
if ($acl->isAllowed(App\Enums\StationPermissions::Profile, $station)): ?>
<a class="navbar-brand ms-0 flex-shrink-0" href="<?= $router->fromHere('stations:profile:edit') ?>">
<i class="material-icons">edit</i>
<span class="visually-hidden"><?= __('Edit Profile') ?>
</a>
<?php
endif; ?>
</div>
</div>
<?php
echo $this->fetch('partials/sidebar_menu', ['menu' => $menu, 'active' => $active]);
?>

View File

@ -1,57 +0,0 @@
<?php
$this->layout('main', ['title' => $title, 'manual' => true]);
?>
<?php foreach ($log_records as $id => $row): ?>
<div class="card mb-3" id="log-view">
<div class="card-header" id="log-row-<?=$id?>">
<h4 class="mb-0">
<?php if ($row['level'] === \Monolog\Logger::DEBUG): ?>
<span class="badge text-bg-info">Debug</span>
<?php elseif ($row['level'] === \Monolog\Logger::INFO): ?>
<span class="badge text-bg-info">Info</span>
<?php elseif ($row['level'] === \Monolog\Logger::NOTICE): ?>
<span class="badge text-bg-info">Notice</span>
<?php elseif ($row['level'] === \Monolog\Logger::WARNING): ?>
<span class="badge text-bg-warning">Warning</span>
<?php elseif ($row['level'] === \Monolog\Logger::ERROR): ?>
<span class="badge text-bg-danger">Error</span>
<?php elseif ($row['level'] === \Monolog\Logger::CRITICAL): ?>
<span class="badge text-bg-danger">Critical</span>
<?php elseif ($row['level'] === \Monolog\Logger::ALERT): ?>
<span class="badge text-bg-danger">Alert</span>
<?php elseif ($row['level'] === \Monolog\Logger::EMERGENCY): ?>
<span class="badge text-bg-danger">Emergency</span>
<?php endif; ?>
<?=$this->e($row['message'])?>
</h4>
<?php if (!empty($row['context']) || !empty($row['extra'])): ?>
<div class="buttons mt-3">
<button class="btn btn-sm btn-bg" type="button" data-bs-toggle="collapse"
data-bs-target="#detail-row-<?= $id ?>" aria-controls="detail-row-<?= $id ?>">
<?= __('Details') ?>
</button>
</div>
<?php endif; ?>
</div>
<?php if (!empty($row['context']) || !empty($row['extra'])): ?>
<div id="detail-row-<?=$id?>" class="collapse" aria-labelledby="log-row-<?=$id?>" data-parent="#log-view">
<div class="card-body pb-0">
<dl>
<?php foreach ($row['context'] as $context_header => $context_value): ?>
<dt><?=$context_header?></dt>
<dd><?=$this->dump($context_value)?></dd>
<?php endforeach; ?>
<?php foreach ($row['extra'] as $context_header => $context_value): ?>
<dt><?=$context_header?></dt>
<dd><?=$this->dump($context_value)?></dd>
<?php endforeach; ?>
</dl>
</div>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>

Some files were not shown because too many files have changed in this diff Show More