Merge commit '752d8d679f8cf075ed2d8608071dad654c293f93'
This commit is contained in:
parent
4c64842553
commit
85214e6d2d
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"vue/**/*.ts",
|
||||
"vue/**/*.d.ts",
|
||||
"vue/**/*.tsx",
|
||||
"vue/**/*.vue"
|
||||
"vue/**/*.vue",
|
||||
"vue/**/*.js"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">
|
||||
|
||||
</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">
|
||||
|
||||
</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>
|
||||
•
|
||||
<span v-html="version" />
|
||||
•
|
||||
<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>
|
|
@ -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>
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
. ' • PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
|
||||
]);
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
59
src/View.php
59
src/View.php
|
@ -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 = [],
|
||||
|
|
|
@ -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]) ?>
|
|
@ -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"> </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"> </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> • ' . $version->getVersionText() . ' • ' . ($environment->isDocker(
|
||||
) ? 'Docker' : 'Ansible') . ' • 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>
|
|
@ -38,8 +38,6 @@ $hide_footer ??= false;
|
|||
|
||||
<body class="page-minimal <?= $page_class ?? '' ?>">
|
||||
|
||||
<?= $this->fetch('partials/bodyjs') ?>
|
||||
|
||||
<?= $sections->get('bodyjs') ?>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 = [];
|
||||
|
||||
|
|
|
@ -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]);
|
||||
?>
|
|
@ -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
Loading…
Reference in New Issue