diff --git a/config/assets.php b/config/assets.php deleted file mode 100644 index b2b8f82e6..000000000 --- a/config/assets.php +++ /dev/null @@ -1,215 +0,0 @@ - [ - 'order' => 0, - 'files' => [ - 'js' => [ - [ - 'src' => 'dist/lib/jquery/jquery.min.js', - ], - ], - ], - ], - - 'minimal' => [ - 'order' => 2, - 'require' => ['jquery'], - 'files' => [ - 'js' => [ - [ - 'src' => 'dist/lib/bootstrap/bootstrap.bundle.min.js', - ], - [ - 'src' => 'dist/lib/bootstrap-notify/bootstrap-notify.min.js', - 'defer' => true, - ], - [ - 'src' => 'dist/app.js', - 'defer' => true, - ], - [ - 'src' => 'dist/material.js', - ], - ], - 'css' => [ - [ - 'href' => 'dist/lib/roboto-fontface/css/roboto/roboto-fontface.css', - ], - [ - 'href' => 'dist/style.css', - ], - ], - ], - 'inline' => [ - 'js' => [ - function (Request $request) { - /** @var App\Session\Flash|null $flashObj */ - $flashObj = $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH); - - if (null === $flashObj || !$flashObj->hasMessages()) { - return null; - } - - $notifies = []; - foreach ($flashObj->getMessages() as $message) { - $notifyMessage = str_replace(['"', "\n"], ['\'', '
'], $message['text']); - $notifies[] = 'notify("' . $notifyMessage . '", "' . $message['color'] . '");'; - } - - return '$(function () { ' . implode('', $notifies) . ' });'; - }, - function (Request $request) { - $localeObj = $request->getAttribute(ServerRequest::ATTR_LOCALE); - - $locale = ($localeObj instanceof App\Enums\SupportedLocales) - ? $localeObj->value - : App\Enums\SupportedLocales::default()->value; - - $locale = explode('.', $locale, 2)[0]; - $localeShort = substr($locale, 0, 2); - $localeWithDashes = str_replace('_', '-', $locale); - - // User profile-specific 24-hour display setting. - $userObj = $request->getAttribute(ServerRequest::ATTR_USER); - $show24Hours = ($userObj instanceof App\Entity\User) - ? $userObj->getShow24HourTime() - : null; - - $timeConfig = new \stdClass(); - if (null !== $show24Hours) { - $timeConfig->hour12 = !$show24Hours; - } - - $app = [ - 'lang' => [ - 'confirm' => __('Are you sure?'), - 'advanced' => __('Advanced'), - ], - 'locale' => $locale, - 'locale_short' => $localeShort, - 'locale_with_dashes' => $localeWithDashes, - 'time_config' => $timeConfig, - 'api_csrf' => null, - ]; - - return 'document.body.App = ' . json_encode($app, JSON_THROW_ON_ERROR) . ';'; - }, - <<<'JS' - document.body.currentTheme = document.documentElement.getAttribute('data-theme'); - if (document.body.currentTheme === 'browser') { - document.body.currentTheme = (window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'; - } - document.body.App.theme = document.body.currentTheme; - JS, - ], - ], - ], - - 'main' => [ - 'order' => 3, - 'require' => ['minimal'], - 'files' => [ - 'js' => [ - [ - 'src' => 'dist/lib/sweetalert2/sweetalert2.min.js', - 'defer' => true, - ], - [ - 'src' => 'dist/lib/turbo/turbo.es2017-umd.js', - ], - ], - ], - 'inline' => [ - 'js' => [ - function (Request $request) { - $csrfJson = 'null'; - $csrf = $request->getAttribute(ServerRequest::ATTR_SESSION_CSRF); - if ($csrf instanceof Csrf) { - $csrfToken = $csrf->generate(ApiAuth::API_CSRF_NAMESPACE); - $csrfJson = json_encode($csrfToken, JSON_THROW_ON_ERROR); - } - return "document.body.App.api_csrf = ${csrfJson};"; - }, - ], - ], - ], - - 'luxon' => [ - 'order' => 8, - 'files' => [ - 'js' => [ - [ - 'src' => 'dist/lib/luxon/luxon.min.js', - ], - ], - ], - 'inline' => [ - 'js' => [ - function (Request $request) { - return <<<'JS' - luxon.Settings.defaultLocale = App.locale_with_dashes; - luxon.Settings.defaultZoneName = 'UTC'; - JS; - }, - ], - ], - ], - - 'humanize-duration' => [ - 'order' => 8, - 'files' => [ - 'js' => [ - [ - 'src' => 'dist/lib/humanize-duration/humanize-duration.js', - ], - ], - ], - ], - - 'clipboard' => [ - 'order' => 10, - 'files' => [ - 'js' => [ - [ - 'src' => 'dist/lib/clipboard/clipboard.min.js', - ], - ], - ], - 'inline' => [ - 'js' => [ - "new ClipboardJS('.btn-copy');", - ], - ], - ], - - 'Vue_PublicWebDJ' => [ - 'order' => 10, - 'files' => [ - 'js' => [ - [ - 'src' => 'dist/lib/webcaster/libshine.js', - ], - [ - 'src' => 'dist/lib/webcaster/libsamplerate.js', - ], - [ - 'src' => 'dist/lib/webcaster/taglib.js', - ], - [ - 'src' => 'dist/lib/webcaster/webcast.js', - ], - ], - ], - ], -]; diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index d572f3f59..845486914 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -14,125 +14,109 @@ const mode = require('gulp-mode')(); const run = require('gulp-run-command').default; var jsFiles = { - 'jquery': { - base: 'node_modules/jquery/dist', - files: [ - 'node_modules/jquery/dist/jquery.min.js' - ] - }, - 'bootstrap': { - base: null, - files: [ - 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js' - ] - }, - 'bootstrap-notify': { - base: 'node_modules/bootstrap-notify', - files: [ - 'node_modules/bootstrap-notify/bootstrap-notify.min.js' - ] - }, - 'sweetalert2': { - base: 'node_modules/sweetalert2/dist', - files: [ - 'node_modules/sweetalert2/dist/sweetalert2.min.js' - ] - }, - 'material-icons': { - files: [ - 'font/*' - ] - }, - 'roboto-fontface': { - base: 'node_modules/roboto-fontface', - files: [ - 'node_modules/roboto-fontface/css/roboto/roboto-fontface.css', - 'node_modules/roboto-fontface/fonts/roboto/*' - ] - }, + 'jquery': { + base: 'node_modules/jquery/dist', + files: [ + 'node_modules/jquery/dist/jquery.min.js' + ] + }, + 'bootstrap': { + base: null, + files: [ + 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js' + ] + }, + 'bootstrap-notify': { + base: 'node_modules/bootstrap-notify', + files: [ + 'node_modules/bootstrap-notify/bootstrap-notify.min.js' + ] + }, + 'sweetalert2': { + base: 'node_modules/sweetalert2/dist', + files: [ + 'node_modules/sweetalert2/dist/sweetalert2.min.js' + ] + }, + 'material-icons': { + files: [ + 'font/*' + ] + }, + 'roboto-fontface': { + base: 'node_modules/roboto-fontface', + files: [ + 'node_modules/roboto-fontface/css/roboto/roboto-fontface.css', + 'node_modules/roboto-fontface/fonts/roboto/*' + ] + }, 'luxon': { files: [ 'node_modules/luxon/build/global/luxon.min.js' ] }, - 'humanize-duration': { - files: [ - 'node_modules/humanize-duration/humanize-duration.js' - ] - }, - 'clipboard': { - base: 'node_modules/clipboard/dist', - files: [ - 'node_modules/clipboard/dist/clipboard.min.js' - ] - }, 'webcaster': { base: null, files: [ 'js/webcaster/*.js' ] }, - 'turbo': { - files: [ - 'node_modules/@hotwired/turbo/dist/turbo.es2017-umd.js' - ] - } }; var defaultTasks = Object.keys(jsFiles); defaultTasks.forEach(function (libName) { - gulp.task('scripts:' + libName, function () { - return gulp.src(jsFiles[libName].files, { - base: jsFiles[libName].base - }).pipe(gulp.dest('../web/static/dist/lib/' + libName)); - }); + gulp.task('scripts:' + libName, function () { + return gulp.src(jsFiles[libName].files, { + base: jsFiles[libName].base + }).pipe(gulp.dest('../web/static/dist/lib/' + libName)); + }); }); gulp.task('bundle-deps', gulp.parallel( - defaultTasks.map(function (name) { - return 'scripts:' + name; - }) + defaultTasks.map(function (name) { + return 'scripts:' + name; + }) )); gulp.task('clean', function () { - return del([ - '../web/static/dist/**/*', - '../web/static/webpack_dist/**/*', - '../web/static/assets.json', - '../web/static/webpack.json' - ], { force: true }); + return del([ + '../web/static/dist/**/*', + '../web/static/webpack_dist/**/*', + '../web/static/assets.json', + '../web/static/webpack.json' + ], {force: true}); }); gulp.task('concat-js', function () { - return gulp.src('./js/inc/*.js') - .pipe(sourcemaps.init()) - .pipe(babel({ - presets: ['@babel/env'] - })) - .pipe(concat('app.js')) - .pipe(uglify()) - .pipe(sourcemaps.write()) - .pipe(gulp.dest('../web/static/dist')); + return gulp.src('./js/inc/*.js') + .pipe(sourcemaps.init()) + .pipe(babel({ + presets: ['@babel/env'] + })) + .pipe(concat('app.js')) + .pipe(uglify()) + .pipe(sourcemaps.write()) + .pipe(gulp.dest('../web/static/dist')); }); gulp.task('build-vue', run('webpack -c webpack.config.js')); gulp.task('build-js', function () { - return gulp.src(['./js/*.js']) - .pipe(sourcemaps.init()) - .pipe(uglify()) - .pipe(sourcemaps.write()) - .pipe(gulp.dest('../web/static/dist')); + return gulp.src(['./js/*.js']) + .pipe(sourcemaps.init()) + .pipe(uglify()) + .pipe(sourcemaps.write()) + .pipe(gulp.dest('../web/static/dist')); }); gulp.task('build-css', function () { - return gulp.src(['./scss/style.scss']) - .pipe(mode.development(sourcemaps.init())) - .pipe(sass()) - .pipe(clean_css()) - .pipe(mode.development(sourcemaps.write())) - .pipe(gulp.dest('../web/static/dist')); + return gulp.src(['./scss/style.scss']) + .pipe(mode.development(sourcemaps.init())) + .pipe(sass()) + .pipe(clean_css()) + .pipe(mode.development(sourcemaps.write())) + .pipe(gulp.dest('../web/static/dist')); }); gulp.task('watch', function () { @@ -144,12 +128,12 @@ gulp.task('watch', function () { }); const buildAll = gulp.series('clean', gulp.parallel('concat-js', 'build-vue', 'build-js', 'build-css', 'bundle-deps'), function () { - return gulp.src(['../web/static/dist/**/*.{js,css}'], { base: '../web/static/' }) - .pipe(rev()) - .pipe(revdel()) - .pipe(gulp.dest('../web/static/')) - .pipe(rev.manifest('assets.json')) - .pipe(gulp.dest('../web/static/')); + return gulp.src(['../web/static/dist/**/*.{js,css}'], {base: '../web/static/'}) + .pipe(rev()) + .pipe(revdel()) + .pipe(gulp.dest('../web/static/')) + .pipe(rev.manifest('assets.json')) + .pipe(gulp.dest('../web/static/')); }); exports.default = buildAll diff --git a/frontend/js/inc/confirm-danger.js b/frontend/js/inc/confirm-danger.js index 5ddbf6bc5..b2ffe08be 100644 --- a/frontend/js/inc/confirm-danger.js +++ b/frontend/js/inc/confirm-danger.js @@ -5,7 +5,7 @@ function confirmDangerousAction (el) { $el = $el.closest('a'); } - let confirmTitle = document.body.App.lang.confirm; + let confirmTitle = App.lang.confirm; if ($el.data('confirm-title')) { confirmTitle = $el.data('confirm-title'); } diff --git a/frontend/npm-shrinkwrap.json b/frontend/npm-shrinkwrap.json index c662c6b31..ce396e6b8 100644 --- a/frontend/npm-shrinkwrap.json +++ b/frontend/npm-shrinkwrap.json @@ -16,7 +16,6 @@ "@fullcalendar/luxon2": "^5.10.2", "@fullcalendar/timegrid": "^5.9.0", "@fullcalendar/vue": "^5.9.0", - "@hotwired/turbo": "^7.2.4", "axios": "^1", "bootstrap": "^4.6.0 <5", "bootstrap-notify": "^3.1.3", @@ -1736,14 +1735,6 @@ "node": ">=0.10.0" } }, - "node_modules/@hotwired/turbo": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-7.2.4.tgz", - "integrity": "sha512-c3xlOroHp/cCZHDOuLp6uzQYEbvXBUVaal0puXoGJ9M8L/KHwZ3hQozD4dVeSN9msHWLxxtmPT1TlCN7gFhj4w==", - "engines": { - "node": ">= 14" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -11332,11 +11323,6 @@ } } }, - "@hotwired/turbo": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-7.2.4.tgz", - "integrity": "sha512-c3xlOroHp/cCZHDOuLp6uzQYEbvXBUVaal0puXoGJ9M8L/KHwZ3hQozD4dVeSN9msHWLxxtmPT1TlCN7gFhj4w==" - }, "@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index bcee7229f..c010e0687 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,6 @@ "@fullcalendar/luxon2": "^5.10.2", "@fullcalendar/timegrid": "^5.9.0", "@fullcalendar/vue": "^5.9.0", - "@hotwired/turbo": "^7.2.4", "axios": "^1", "bootstrap": "^4.6.0 <5", "bootstrap-notify": "^3.1.3", diff --git a/frontend/vue/base.js b/frontend/vue/base.js index 78c61140c..dad3e58eb 100644 --- a/frontend/vue/base.js +++ b/frontend/vue/base.js @@ -12,13 +12,13 @@ document.addEventListener('DOMContentLoaded', function () { silent: true }); - if (typeof document.body.App.locale !== 'undefined') { - Vue.config.language = document.body.App.locale; + if (typeof App.locale !== 'undefined') { + Vue.config.language = App.locale; } // Configure auto-CSRF on requests - if (typeof document.body.App.api_csrf !== 'undefined') { - axios.defaults.headers.common['X-API-CSRF'] = document.body.App.api_csrf; + if (typeof App.api_csrf !== 'undefined') { + axios.defaults.headers.common['X-API-CSRF'] = App.api_csrf; } Vue.use(VueAxios, axios); diff --git a/frontend/vue/components/Admin/AuditLog.vue b/frontend/vue/components/Admin/AuditLog.vue index c5cd6eb03..1338554c8 100644 --- a/frontend/vue/components/Admin/AuditLog.vue +++ b/frontend/vue/components/Admin/AuditLog.vue @@ -144,7 +144,7 @@ export default { }, formatTimestamp(unix_timestamp) { return DateTime.fromSeconds(unix_timestamp).toLocaleString( - {...DateTime.DATETIME_SHORT, ...document.body.App.time_config} + {...DateTime.DATETIME_SHORT, ...App.time_config} ); } } diff --git a/frontend/vue/components/Admin/Backups.vue b/frontend/vue/components/Admin/Backups.vue index 97666b806..47464bc28 100644 --- a/frontend/vue/components/Admin/Backups.vue +++ b/frontend/vue/components/Admin/Backups.vue @@ -184,7 +184,7 @@ export default { }, toLocaleTime(timestamp) { return DateTime.fromSeconds(timestamp).toLocaleString( - {...DateTime.DATETIME_SHORT, ...document.body.App.time_config} + {...DateTime.DATETIME_SHORT, ...App.time_config} ); }, formatFileSize(size) { diff --git a/frontend/vue/components/Common/FlowUpload.vue b/frontend/vue/components/Common/FlowUpload.vue index 886b1f146..f59d178a7 100644 --- a/frontend/vue/components/Common/FlowUpload.vue +++ b/frontend/vue/components/Common/FlowUpload.vue @@ -105,7 +105,7 @@ export default { singleFile: !this.allowMultiple, headers: { 'Accept': 'application/json', - 'X-API-CSRF': document.body.App.api_csrf + 'X-API-CSRF': App.api_csrf }, withCredentials: true, allowDuplicateUploads: true, diff --git a/frontend/vue/components/Common/ScheduleView.vue b/frontend/vue/components/Common/ScheduleView.vue index e8646afb7..eeacf334f 100644 --- a/frontend/vue/components/Common/ScheduleView.vue +++ b/frontend/vue/components/Common/ScheduleView.vue @@ -19,7 +19,7 @@ export default { data() { return { calendarOptions: { - locale: document.body.App.locale_short, + locale: App.locale_short, locales: allLocales, plugins: [luxon2Plugin, timeGridPlugin], initialView: 'timeGridWeek', @@ -36,7 +36,7 @@ export default { views: { timeGridWeek: { slotLabelFormat: { - ...document.body.App.time_config, + ...App.time_config, hour: 'numeric', minute: '2-digit', omitZeroMinute: true, diff --git a/frontend/vue/components/Stations/Media.vue b/frontend/vue/components/Stations/Media.vue index ef4f2ed51..aa461b29e 100644 --- a/frontend/vue/components/Stations/Media.vue +++ b/frontend/vue/components/Stations/Media.vue @@ -268,7 +268,7 @@ export default { return ''; } return DateTime.fromSeconds(value).setZone(this.stationTimeZone).toLocaleString( - {...DateTime.DATETIME_MED, ...document.body.App.time_config} + {...DateTime.DATETIME_MED, ...App.time_config} ); }, selectable: true, diff --git a/frontend/vue/components/Stations/Playlists.vue b/frontend/vue/components/Stations/Playlists.vue index bca578343..65e737e5e 100644 --- a/frontend/vue/components/Stations/Playlists.vue +++ b/frontend/vue/components/Stations/Playlists.vue @@ -193,13 +193,13 @@ export default { }, formatTime (time) { return DateTime.fromSeconds(time).setZone(this.stationTimeZone).toLocaleString( - {...DateTime.DATETIME_MED, ...document.body.App.time_config} + {...DateTime.DATETIME_MED, ...App.time_config} ); }, formatLength (length) { return humanizeDuration(length * 1000, { round: true, - language: document.body.App.locale_short, + language: App.locale_short, fallbacks: ['en'] }); }, diff --git a/frontend/vue/components/Stations/Profile/SchedulePanel.vue b/frontend/vue/components/Stations/Profile/SchedulePanel.vue index 9f3612734..37cac6918 100644 --- a/frontend/vue/components/Stations/Profile/SchedulePanel.vue +++ b/frontend/vue/components/Stations/Profile/SchedulePanel.vue @@ -61,21 +61,21 @@ export default { if (start_moment.hasSame(now, 'day')) { row.start_formatted = start_moment.toLocaleString( - {...DateTime.TIME_SIMPLE, ...document.body.App.time_config} + {...DateTime.TIME_SIMPLE, ...App.time_config} ); } else { row.start_formatted = start_moment.toLocaleString( - {...DateTime.DATETIME_MED, ...document.body.App.time_config} + {...DateTime.DATETIME_MED, ...App.time_config} ); } if (end_moment.hasSame(start_moment, 'day')) { row.end_formatted = end_moment.toLocaleString( - {...DateTime.TIME_SIMPLE, ...document.body.App.time_config} + {...DateTime.TIME_SIMPLE, ...App.time_config} ); } else { row.end_formatted = end_moment.toLocaleString( - {...DateTime.DATETIME_MED, ...document.body.App.time_config} + {...DateTime.DATETIME_MED, ...App.time_config} ); } diff --git a/frontend/vue/components/Stations/Queue.vue b/frontend/vue/components/Stations/Queue.vue index 8f62ab888..c49ebe6c5 100644 --- a/frontend/vue/components/Stations/Queue.vue +++ b/frontend/vue/components/Stations/Queue.vue @@ -82,7 +82,7 @@ export default { methods: { formatTime(time) { return this.getDateTime(time).toLocaleString( - {...DateTime.TIME_WITH_SECONDS, ...document.body.App.time_config} + {...DateTime.TIME_WITH_SECONDS, ...App.time_config} ); }, formatRelativeTime(time) { diff --git a/frontend/vue/components/Stations/Reports/Listeners/Map.vue b/frontend/vue/components/Stations/Reports/Listeners/Map.vue index 6b80b02af..9931da1ba 100644 --- a/frontend/vue/components/Stations/Reports/Listeners/Map.vue +++ b/frontend/vue/components/Stations/Reports/Listeners/Map.vue @@ -41,7 +41,7 @@ export default { }, data() { return { - tileUrl: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/' + document.body.App.theme + '_all/{z}/{x}/{y}.png', + tileUrl: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/' + App.theme + '_all/{z}/{x}/{y}.png', tileAttribution: 'Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL.', } }, diff --git a/frontend/vue/components/Stations/Reports/Requests.vue b/frontend/vue/components/Stations/Reports/Requests.vue index 2fc8bc148..12c01c800 100644 --- a/frontend/vue/components/Stations/Reports/Requests.vue +++ b/frontend/vue/components/Stations/Reports/Requests.vue @@ -105,7 +105,7 @@ export default { }, formatTime(time) { return DateTime.fromSeconds(time).setZone(this.stationTimeZone).toLocaleString( - {...DateTime.DATETIME_MED, ...document.body.App.time_config} + {...DateTime.DATETIME_MED, ...App.time_config} ); }, doDelete(url) { diff --git a/frontend/vue/components/Stations/Reports/Timeline.vue b/frontend/vue/components/Stations/Reports/Timeline.vue index 4c224046f..fd7e9b81c 100644 --- a/frontend/vue/components/Stations/Reports/Timeline.vue +++ b/frontend/vue/components/Stations/Reports/Timeline.vue @@ -166,12 +166,12 @@ export default { }, formatTimestamp(unix_timestamp) { return DateTime.fromSeconds(unix_timestamp).toLocaleString( - {...DateTime.DATETIME_SHORT, ...document.body.App.time_config} + {...DateTime.DATETIME_SHORT, ...App.time_config} ); }, formatTimestampStation(unix_timestamp) { return DateTime.fromSeconds(unix_timestamp).setZone(this.stationTimeZone).toLocaleString( - {...DateTime.DATETIME_SHORT, ...document.body.App.time_config} + {...DateTime.DATETIME_SHORT, ...App.time_config} ); } } diff --git a/frontend/vue/components/Stations/Streamers/BroadcastsModal.vue b/frontend/vue/components/Stations/Streamers/BroadcastsModal.vue index 4f7653930..189a99b3f 100644 --- a/frontend/vue/components/Stations/Streamers/BroadcastsModal.vue +++ b/frontend/vue/components/Stations/Streamers/BroadcastsModal.vue @@ -63,7 +63,7 @@ export default { sortable: false, formatter: (value, key, item) => { return DateTime.fromSeconds(value).toLocaleString( - {...DateTime.DATETIME_MED, ...document.body.App.time_config} + {...DateTime.DATETIME_MED, ...App.time_config} ); }, class: 'pl-3' @@ -77,7 +77,7 @@ export default { return this.$gettext('Live'); } return DateTime.fromSeconds(value).toLocaleString( - {...DateTime.DATETIME_MED, ...document.body.App.time_config} + {...DateTime.DATETIME_MED, ...App.time_config} ); } }, diff --git a/frontend/vue/vendor/luxon.js b/frontend/vue/vendor/luxon.js index 674587d7b..4ae23e9e1 100644 --- a/frontend/vue/vendor/luxon.js +++ b/frontend/vue/vendor/luxon.js @@ -1,7 +1,7 @@ import {Settings} from 'luxon'; document.addEventListener('DOMContentLoaded', function () { - Settings.defaultLocale = document.body.App.locale_with_dashes; + Settings.defaultLocale = App.locale_with_dashes; Settings.defaultZoneName = 'UTC'; }); diff --git a/src/Assets.php b/src/Assets.php deleted file mode 100644 index 068b8f77f..000000000 --- a/src/Assets.php +++ /dev/null @@ -1,567 +0,0 @@ - Known libraries loaded in initialization. */ - private array $libraries = []; - - /** @var array An optional array lookup for versioned files. */ - private array $versioned_files = []; - - /** @var array Loaded libraries. */ - private array $loaded = []; - - /** @var bool Whether the current loaded libraries have been sorted by order. */ - private bool $is_sorted = true; - - /** @var string A randomly generated number-used-once (nonce) for inline CSP. */ - private string $csp_nonce; - - /** @var array The loaded domains that should be included in the CSP header. */ - private array $csp_domains; - - public function __construct( - private readonly Environment $environment, - array $libraries, - ) { - foreach ($libraries as $library_name => $library) { - $this->addLibrary($library, $library_name); - } - - $versioned_files = Json::loadFromFile($environment->getBaseDirectory() . '/web/static/assets.json'); - $this->versioned_files = $versioned_files; - - $vueComponents = Json::loadFromFile($environment->getBaseDirectory() . '/web/static/webpack.json'); - $this->addVueComponents($vueComponents); - - $this->csp_nonce = (string)preg_replace('/[^A-Za-z\d+\/=]/', '', base64_encode(random_bytes(18))); - $this->csp_domains = []; - } - - private function addVueComponents(array $vueComponents = []): void - { - if (!empty($vueComponents['entrypoints'])) { - foreach ($vueComponents['entrypoints'] as $componentName => $componentDeps) { - $componentName = 'Vue_' . $componentName; - - $library = $this->libraries[$componentName] ?? [ - 'order' => 10, - 'require' => [], - 'files' => [], - ]; - - foreach ($componentDeps['assets']['js'] as $componentDep) { - $library['files']['js'][] = [ - 'src' => $componentDep, - ]; - } - - $this->addLibrary($library, $componentName); - } - } - } - - /** - * Add a library to the collection. - * - * @param array $data Array with asset data. - * @param string|null $library_name - */ - public function addLibrary(array $data, string $library_name = null): self - { - $library_name = $library_name ?? uniqid('', false); - - $this->libraries[$library_name] = [ - 'name' => $library_name, - 'order' => $data['order'] ?? 0, - 'files' => $data['files'] ?? [], - 'inline' => $data['inline'] ?? [], - 'require' => $data['require'] ?? [], - 'replace' => $data['replace'] ?? [], - ]; - - return $this; - } - - /** - * @param string $library_name - * - * @return mixed[]|null - */ - public function getLibrary(string $library_name): ?array - { - return $this->libraries[$library_name] ?? null; - } - - /** - * Returns the randomly generated nonce for inline CSP for this request. - */ - public function getCspNonce(): string - { - return $this->csp_nonce; - } - - /** - * Returns the list of approved domains for CSP header inclusion. - * - * @return string[] - */ - public function getCspDomains(): array - { - return $this->csp_domains; - } - - /** - * Add a single javascript file. - * - * @param array|string $js_script - */ - public function addJs(array|string $js_script): self - { - $this->load( - [ - 'order' => 100, - 'files' => [ - 'js' => [ - (is_array($js_script)) ? $js_script : ['src' => $js_script], - ], - ], - ] - ); - - return $this; - } - - /** - * Loads assets from given name or array definition. - * - * @param mixed $data Name or array definition of library/asset. - */ - public function load(mixed $data): self - { - if (is_array($data)) { - $item = [ - 'name' => $data['name'] ?? uniqid('', false), - 'order' => $data['order'] ?? 0, - 'files' => $data['files'] ?? [], - 'inline' => $data['inline'] ?? [], - 'require' => $data['require'] ?? [], - 'replace' => $data['replace'] ?? [], - ]; - } elseif (isset($this->libraries[$data])) { - $item = $this->libraries[$data]; - } else { - throw new InvalidArgumentException(sprintf('Library %s not found!', $data)); - } - - /** @var string $name */ - $name = $item['name']; - - // Check if a library is "replaced" by other libraries already loaded. - $is_replaced = false; - foreach ($this->loaded as $loaded_item) { - if (!empty($loaded_item['replace']) && in_array($name, (array)$loaded_item['replace'], true)) { - $is_replaced = true; - break; - } - } - - if (!$is_replaced && !isset($this->loaded[$name])) { - if (!empty($item['replace'])) { - foreach ((array)$item['replace'] as $replace_name) { - $this->unload($replace_name); - } - } - - if (!empty($item['require'])) { - foreach ((array)$item['require'] as $require_name) { - $this->load($require_name); - } - } - - $this->loaded[$name] = $item; - $this->is_sorted = false; - } - - return $this; - } - - /** - * Unload a given library if it's already loaded. - * - * @param string $name - */ - public function unload(string $name): self - { - if (isset($this->loaded[$name])) { - unset($this->loaded[$name]); - $this->is_sorted = false; - } - - return $this; - } - - /** - * Add a single javascript inline script. - * - * @param array|string $js_script - * @param int $order - */ - public function addInlineJs(array|string $js_script, int $order = 100): self - { - $this->load( - [ - 'order' => $order, - 'inline' => [ - 'js' => (is_array($js_script)) ? $js_script : [$js_script], - ], - ] - ); - - return $this; - } - - public function addVueRender(string $name, string $elementId, array $props = []): self - { - $this->load($name); - - $nameWithoutPrefix = str_replace('Vue_', '', $name); - $propsJson = json_encode($props, JSON_THROW_ON_ERROR); - - $this->addInlineJs( - <<load( - [ - 'order' => $order, - 'files' => [ - 'css' => [ - (is_array($css_script)) ? $css_script : ['src' => $css_script], - ], - ], - ] - ); - - return $this; - } - - /** - * Add a single inline CSS file[s]. - * - * @param array|string $css_script - */ - public function addInlineCss(array|string $css_script): self - { - $this->load( - [ - 'order' => 100, - 'inline' => [ - 'css' => (is_array($css_script)) ? $css_script : [$css_script], - ], - ] - ); - - return $this; - } - - /** - * Returns all CSS includes and inline styles. - * - * @return string HTML tags as string. - */ - public function css(): string - { - $this->sort(); - - $result = []; - foreach ($this->loaded as $item) { - if (!empty($item['files']['css'])) { - foreach ($item['files']['css'] as $file) { - $attributes = $this->resolveAttributes( - $file, - [ - 'rel' => 'stylesheet', - 'type' => 'text/css', - ] - ); - - $key = $attributes['href']; - if (isset($result[$key])) { - continue; - } - - $result[$key] = 'compileAttributes($attributes)) . ' />'; - } - } - - if (!empty($item['inline']['css'])) { - foreach ($item['inline']['css'] as $inline) { - if (!empty($inline)) { - $result[] = sprintf( - '', - $this->csp_nonce, - "\n", - $inline - ); - } - } - } - } - - return implode("\n", $result) . "\n"; - } - - /** - * Returns all script include tags. - * - * @return string HTML tags as string. - */ - public function js(): string - { - $this->sort(); - - $result = []; - foreach ($this->loaded as $item) { - if (!empty($item['files']['js'])) { - foreach ($item['files']['js'] as $file) { - $attributes = $this->resolveAttributes( - $file, - [ - 'type' => 'text/javascript', - ] - ); - - $key = $attributes['src']; - if (isset($result[$key])) { - continue; - } - - $result[$key] = ''; - } - } - } - - return implode("\n", $result) . "\n"; - } - - /** - * Return any inline JavaScript. - */ - public function inlineJs(): string - { - $this->sort(); - - $result = []; - foreach ($this->loaded as $item) { - if (!empty($item['inline']['js'])) { - foreach ($item['inline']['js'] as $inline) { - if (is_callable($inline)) { - $inline = $inline($this->request); - } - - if (!empty($inline)) { - $result[] = sprintf( - '', - $this->csp_nonce, - "\n", - $inline - ); - } - } - } - } - - return implode("\n", $result) . "\n"; - } - - /** - * Sort the list of loaded libraries. - */ - private function sort(): void - { - if (!$this->is_sorted) { - uasort( - $this->loaded, - static function ($a, $b): int { - return $a['order'] <=> $b['order']; // SPACESHIP! - } - ); - - $this->is_sorted = true; - } - } - - private function resolveAttributes(array $file, array $defaults): array - { - if (isset($file['src'])) { - $defaults['src'] = $this->getUrl($file['src']); - unset($file['src']); - } - - if (isset($file['href'])) { - $defaults['href'] = $this->getUrl($file['href']); - unset($file['href']); - } - - if (isset($file['integrity'])) { - $defaults['crossorigin'] = 'anonymous'; - } - - return array_merge($defaults, $file); - } - - /** - * Build the proper include tag for a JS/CSS include. - * - * @param array $attributes - * - * @return string[] - */ - private function compileAttributes(array $attributes): array - { - $compiled_attributes = []; - foreach ($attributes as $attr_key => $attr_val) { - // Check for attributes like "defer" - if ($attr_val === true) { - $compiled_attributes[] = $attr_key; - } else { - $compiled_attributes[] = $attr_key . '="' . $attr_val . '"'; - } - } - - return $compiled_attributes; - } - - /** - * @return string[] The paths to all currently loaded files. - */ - public function getLoadedFiles(): array - { - $this->sort(); - - $result = []; - foreach ($this->loaded as $item) { - if (!empty($item['files']['js'])) { - foreach ($item['files']['js'] as $file) { - if (isset($file['src'])) { - $result[] = $this->getUrl($file['src']); - } - } - } - - if (!empty($item['files']['css'])) { - foreach ($item['files']['css'] as $file) { - if (isset($file['href'])) { - $result[] = $this->getUrl($file['href']); - } - } - } - } - - return $result; - } - - /** - * Resolve the URI of the resource, whether local or remote/CDN-based. - * - * @param string $resource_uri - * - * @return string The resolved resource URL. - */ - public function getUrl(string $resource_uri): string - { - if (isset($this->versioned_files[$resource_uri])) { - $resource_uri = $this->versioned_files[$resource_uri]; - } - - if (str_starts_with($resource_uri, 'http')) { - $this->addDomainToCsp($resource_uri); - return $resource_uri; - } - - if (str_starts_with($resource_uri, '/')) { - return $resource_uri; - } - - return $this->environment->getAssetUrl() . '/' . $resource_uri; - } - - /** - * Add the loaded domain to the full list of CSP-approved domains. - * - * @param string $src - */ - private function addDomainToCsp(string $src): void - { - $uri = new Uri($src); - - $domain = $uri->getScheme() . '://' . $uri->getHost(); - if (!isset($this->csp_domains[$domain])) { - $this->csp_domains[$domain] = $domain; - } - } - - public function writeCsp(ResponseInterface $response): ResponseInterface - { - $csp = []; - if (null !== $this->request && 'https' === $this->request->getUri()->getScheme()) { - $csp[] = 'upgrade-insecure-requests'; - } - - // CSP JavaScript policy - // Note: unsafe-eval included for Vue template compiling - $cspScriptSrc = $this->getCspDomains(); - $cspScriptSrc[] = "'self'"; - $cspScriptSrc[] = "'unsafe-eval'"; - $cspScriptSrc[] = "'unsafe-inline'"; - // $cspScriptSrc[] = "'nonce-" . $this->getCspNonce() . "'"; - $csp[] = 'script-src ' . implode(' ', $cspScriptSrc); - - $cspWorkerSrc = []; - $cspWorkerSrc[] = "blob:"; - $cspWorkerSrc[] = "'self'"; - - $csp[] = 'worker-src ' . implode(' ', $cspWorkerSrc); - - return $response->withHeader('Content-Security-Policy', implode('; ', $csp)); - } -} diff --git a/src/Controller/Frontend/PublicPages/WebDjAction.php b/src/Controller/Frontend/PublicPages/WebDjAction.php index 0a650486f..bff4c7285 100644 --- a/src/Controller/Frontend/PublicPages/WebDjAction.php +++ b/src/Controller/Frontend/PublicPages/WebDjAction.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Controller\Frontend\PublicPages; -use App\Assets; use App\Exception\StationNotFoundException; use App\Exception\StationUnsupportedException; use App\Http\Response; @@ -15,7 +14,6 @@ use Psr\Http\Message\ResponseInterface; final class WebDjAction { public function __construct( - private readonly Assets $assets, private readonly Adapters $adapters, ) { } @@ -40,34 +38,16 @@ final class WebDjAction throw new StationUnsupportedException(); } - $router = $request->getRouter(); - $wss_url = (string)$backend->getWebStreamingUrl($station, $request->getRouter()->getBaseUrl()); $wss_url = str_replace('wss://', '', $wss_url); - $libUrls = []; - $lib = $this->assets->getLibrary('Vue_PublicWebDJ'); - if (null !== $lib) { - foreach (array_slice($lib['files']['js'], 0, -1) as $script) { - $libUrls[] = (string)($router->getBaseUrl()->withPath($this->assets->getUrl($script['src']))); - } - } - - return $request->getView()->renderVuePage( + return $request->getView()->renderToResponse( response: $response->withHeader('X-Frame-Options', '*'), - component: 'Vue_PublicWebDJ', - id: 'web_dj', - layout: 'minimal', - title: __('Web DJ') . ' - ' . $station->getName(), - layoutParams: [ - 'page_class' => 'dj station-' . $station->getShortName(), - 'hide_footer' => true, - ], - props: [ - 'stationName' => $station->getName(), - 'libUrls' => $libUrls, - 'baseUri' => $wss_url, - ], + templateName: 'frontend/public/webdj', + templateArgs: [ + 'station' => $station, + 'wss_url' => $wss_url, + ] ); } } diff --git a/src/Middleware/Module/Admin.php b/src/Middleware/Module/Admin.php index 2517328b8..8d11a4902 100644 --- a/src/Middleware/Module/Admin.php +++ b/src/Middleware/Module/Admin.php @@ -48,15 +48,14 @@ final class Admin ); // These two intentionally separated (the sidebar needs admin_panels). - $view->addData( - [ - 'sidebar' => $view->render( - 'admin/sidebar', - [ - 'active_tab' => $active_tab, - ] - ), - ] + $view->getSections()->set( + 'sidebar', + $view->render( + 'admin/sidebar', + [ + 'active_tab' => $active_tab, + ] + ) ); return $handler->handle($request); diff --git a/src/Middleware/Module/Stations.php b/src/Middleware/Module/Stations.php index 7d2188b01..8c5f93aa9 100644 --- a/src/Middleware/Module/Stations.php +++ b/src/Middleware/Module/Stations.php @@ -47,16 +47,15 @@ final class Stations $active_tab = $route_parts[1]; } - $view->addData( - [ - 'sidebar' => $view->render( - 'stations/sidebar', - [ - 'menu' => $event->getFilteredMenu(), - 'active' => $active_tab, - ] - ), - ] + $view->getSections()->set( + 'sidebar', + $view->render( + 'stations/sidebar', + [ + 'menu' => $event->getFilteredMenu(), + 'active' => $active_tab, + ] + ), ); return $handler->handle($request); diff --git a/src/View.php b/src/View.php index 293dbf65d..9c252a4a6 100644 --- a/src/View.php +++ b/src/View.php @@ -7,10 +7,10 @@ namespace App; use App\Http\RouterInterface; use App\Http\ServerRequest; use App\Traits\RequestAwareTrait; -use Doctrine\Inflector\InflectorFactory; +use App\Utilities\Json; +use App\View\GlobalSections; use League\Plates\Engine; use League\Plates\Template\Data; -use League\Plates\Template\Template; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -21,22 +21,25 @@ final class View extends Engine { use RequestAwareTrait; + private readonly GlobalSections $sections; + public function __construct( Environment $environment, EventDispatcherInterface $dispatcher, Version $version, - RouterInterface $router, - private Assets $assets + RouterInterface $router ) { parent::__construct($environment->getViewsDirectory(), 'phtml'); + $this->sections = new GlobalSections(); + // Add non-request-dependent content. $this->addData( [ + 'sections' => $this->sections, 'environment' => $environment, 'version' => $version, 'router' => $router, - 'assets' => $this->assets, ] ); @@ -62,52 +65,29 @@ final class View extends Engine } ); + $vueComponents = Json::loadFromFile($environment->getBaseDirectory() . '/web/static/webpack.json'); $this->registerFunction( - 'mailto', - function ($address, $link_text = null) { - $address = substr(chunk_split(bin2hex(" $address"), 2, ';&#x'), 3, -3); - $link_text = $link_text ?? $address; - return '' . $link_text . ''; - } + 'getVueComponentInfo', + fn(string $component) => $vueComponents['entrypoints'][$component] ?? null ); + $versionedFiles = Json::loadFromFile($environment->getBaseDirectory() . '/web/static/assets.json'); $this->registerFunction( - 'pluralize', - function ($word, $num = 0) { - if ((int)$num === 1) { - return $word; + 'assetUrl', + function (string $url) use ($environment, $versionedFiles): string { + if (isset($versionedFiles[$url])) { + $url = $versionedFiles[$url]; } - return InflectorFactory::create()->build()->pluralize($word); - } - ); - - $this->registerFunction( - 'truncate', - function ($text, $length = 80) { - return Utilities\Strings::truncateText($text, $length); - } - ); - - $this->registerFunction( - 'truncateUrl', - function ($url) { - return Utilities\Strings::truncateUrl($url); - } - ); - - $this->registerFunction( - 'link', - function ($url, $external = true, $truncate = true) { - $url = htmlspecialchars($url, ENT_QUOTES); - - $a = ['href="' . $url . '"']; - if ($external) { - $a[] = 'target="_blank"'; + if (str_starts_with($url, 'http')) { + return $url; } - $a_body = ($truncate) ? Utilities\Strings::truncateUrl($url) : $url; - return '' . $a_body . ''; + if (str_starts_with($url, '/')) { + return $url; + } + + return $environment->getAssetUrl() . '/' . $url; } ); @@ -116,13 +96,11 @@ final class View extends Engine public function setRequest(?ServerRequestInterface $request): void { - $this->assets = $this->assets->withRequest($request); $this->request = $request; if (null !== $request) { $this->addData( [ - 'assets' => $this->assets, 'request' => $request, 'router' => $request->getAttribute(ServerRequest::ATTR_ROUTER), 'auth' => $request->getAttribute(ServerRequest::ATTR_AUTH), @@ -135,6 +113,11 @@ final class View extends Engine } } + public function getSections(): GlobalSections + { + return $this->sections; + } + public function reset(): void { $this->data = new Data(); @@ -161,8 +144,10 @@ final class View extends Engine string $templateName, array $templateArgs = [] ): ResponseInterface { - $template = $this->render($templateName, $templateArgs); - return $this->writeStringToResponse($response, $template); + $response->getBody()->write( + $this->render($templateName, $templateArgs) + ); + return $response->withHeader('Content-type', 'text/html; charset=utf-8'); } public function renderVuePage( @@ -176,46 +161,17 @@ final class View extends Engine ): ResponseInterface { $id ??= $component; - $vueTemplate = new class ($this, 'vue') extends Template { - public function render(array $data = []): string - { - $this->data($data); - - /** @noinspection UselessUnsetInspection */ - unset($data); - - $content = '
'; - - $layout = $this->engine->make($this->layoutName); - $layout->sections = array_merge($this->sections, ['content' => $content]); - return $layout->render($this->layoutData); - } - }; - - $vueTemplate->layout( - $layout, - array_merge( - [ - 'title' => $title, - 'manual' => true, - ], - $layoutParams - ) + return $this->renderToResponse( + $response, + 'system/vue_page', + [ + 'component' => $component, + 'id' => $id, + 'layout' => $layout, + 'title' => $title, + 'layoutParams' => $layoutParams, + 'props' => $props, + ] ); - - $this->assets->addVueRender($component, '#' . $id, $props); - - $body = $vueTemplate->render(['id' => $id]); - return $this->writeStringToResponse($response, $body); - } - - private function writeStringToResponse( - ResponseInterface $response, - string $body - ): ResponseInterface { - $response->getBody()->write($body); - $response = $response->withHeader('Content-type', 'text/html; charset=utf-8'); - - return $this->assets->writeCsp($response); } } diff --git a/src/View/GlobalSections.php b/src/View/GlobalSections.php new file mode 100644 index 000000000..4c1e6c950 --- /dev/null +++ b/src/View/GlobalSections.php @@ -0,0 +1,89 @@ +sections[$section]); + } + + public function get(string $section, ?string $default = null): ?string + { + return $this->sections[$section] ?? $default; + } + + public function set( + string $section, + ?string $value, + int $mode = Template::SECTION_MODE_REWRITE + ): void { + $initialValue = $this->sections[$section] ?? ''; + + $this->sections[$section] = match ($mode) { + Template::SECTION_MODE_REWRITE => $value, + Template::SECTION_MODE_PREPEND => $value . $initialValue, + Template::SECTION_MODE_APPEND => $initialValue . $value + }; + } + + public function prepend(string $section, ?string $value): void + { + $this->set($section, $value, Template::SECTION_MODE_PREPEND); + } + + public function append(string $section, ?string $value): void + { + $this->set($section, $value, Template::SECTION_MODE_APPEND); + } + + public function start(string $name): void + { + if ($this->sectionName) { + throw new LogicException('You cannot nest sections within other sections.'); + } + + $this->sectionName = $name; + + ob_start(); + } + + public function appendStart($name): void + { + $this->sectionMode = Template::SECTION_MODE_APPEND; + $this->start($name); + } + + public function prependStart($name) + { + $this->sectionMode = Template::SECTION_MODE_PREPEND; + $this->start($name); + } + + public function end(): void + { + if (is_null($this->sectionName)) { + throw new LogicException( + 'You must start a section before you can stop it.' + ); + } + + $this->set($this->sectionName, ob_get_clean(), $this->sectionMode); + + $this->sectionName = null; + $this->sectionMode = Template::SECTION_MODE_REWRITE; + } +} diff --git a/templates/admin/debug/index.js.phtml b/templates/admin/debug/index.js.phtml deleted file mode 100644 index aa6610be6..000000000 --- a/templates/admin/debug/index.js.phtml +++ /dev/null @@ -1,5 +0,0 @@ -jQuery(function ($) { - $('time[data-duration]').each(function () { - $(this).text(luxon.DateTime.fromSeconds($(this).data('duration')).toRelative()); - }); -}); diff --git a/templates/admin/debug/index.phtml b/templates/admin/debug/index.phtml index 48f560761..990ce84a6 100644 --- a/templates/admin/debug/index.phtml +++ b/templates/admin/debug/index.phtml @@ -1,6 +1,10 @@ layout('main', [ @@ -8,9 +12,19 @@ $this->layout('main', [ 'manual' => true, ]); -$assets - ->load('luxon') - ->addInlineJs($this->fetch('admin/debug/index.js'), 99); +$sections->appendStart('bodyjs'); +?> + + + +end(); ?>

@@ -183,7 +197,7 @@ $assets value): ?> + if ($station['backend_type'] === App\Radio\Enums\BackendAdapters::Liquidsoap->value): ?>
diff --git a/templates/admin/relays/index.js.phtml b/templates/admin/relays/index.js.phtml deleted file mode 100644 index 7e08de32b..000000000 --- a/templates/admin/relays/index.js.phtml +++ /dev/null @@ -1,6 +0,0 @@ -$(function () { - $('time[data-content]').each(function () { - let tz_display = $(this).data('content'); - $(this).text(luxon.DateTime.fromSeconds(tz_display).toLocaleString(luxon.DateTime.TIME_SIMPLE)); - }); -}); diff --git a/templates/admin/relays/index.phtml b/templates/admin/relays/index.phtml index 627dd5cd6..201532b14 100644 --- a/templates/admin/relays/index.phtml +++ b/templates/admin/relays/index.phtml @@ -1,34 +1,50 @@ layout('main', [ 'title' => __('Connected AzuraRelays'), - 'manual' => true + 'manual' => true, ]); -/** @var \App\Assets $assets */ -$assets - ->load('luxon') - ->addInlineJs($this->fetch('admin/relays/index.js')); +$sections->appendStart('bodyjs'); +?> + + + +end(); ?>
-

+

- - - - - - + + + + + + - +
- e($row['name']) ?> + e($row['name']) ?> diff --git a/templates/admin/sidebar.phtml b/templates/admin/sidebar.phtml index d16ce9506..63b304879 100644 --- a/templates/admin/sidebar.phtml +++ b/templates/admin/sidebar.phtml @@ -1,7 +1,15 @@ + + -fetch('partials/sidebar_menu', ['menu' => $admin_panels]) ?> \ No newline at end of file +fetch('partials/sidebar_menu', ['menu' => $admin_panels]) ?> diff --git a/templates/frontend/account/login.phtml b/templates/frontend/account/login.phtml index a30a14ef6..38fe8da29 100644 --- a/templates/frontend/account/login.phtml +++ b/templates/frontend/account/login.phtml @@ -1,4 +1,9 @@ layout( 'minimal', @@ -70,7 +75,7 @@ $this->layout(

%s', - (string)$router->named('account:forgot'), + $router->named('account:forgot'), __('Forgot your password?') )?>

diff --git a/templates/frontend/public/index.phtml b/templates/frontend/public/index.phtml index 29b7bbb31..9d96c2f40 100644 --- a/templates/frontend/public/index.phtml +++ b/templates/frontend/public/index.phtml @@ -1,7 +1,11 @@ layout( @@ -12,47 +16,52 @@ $this->layout( ] ); -/** @var \App\Assets $assets */ -$assets->addVueRender('Vue_PublicFullPlayer', '#public-radio-player', $props); - // Register PWA service worker -$swJsRoute = (string)$router->named('public:sw'); +$swJsRoute = $router->named('public:sw'); -$assets->addInlineJs( - <<push('head'); +$sections->appendStart('head'); ?> - + - - - + + + - - - + + + - - - - + + + + end(); -?> +$sections->end(); -
- Loading... -
+$sections->appendStart('bodyjs'); +?> + +end(); + +echo $this->fetch( + 'partials/vue_body', + [ + 'component' => 'Vue_PublicFullPlayer', + 'id' => 'public-radio-player', + 'props' => $props, + ] +); diff --git a/templates/frontend/public/podcast-episode.js.phtml b/templates/frontend/public/podcast-episode.js.phtml deleted file mode 100644 index e816a6591..000000000 --- a/templates/frontend/public/podcast-episode.js.phtml +++ /dev/null @@ -1,3 +0,0 @@ -$(function () { - $('[data-toggle="tooltip"]').tooltip(); -}); diff --git a/templates/frontend/public/podcast-episode.phtml b/templates/frontend/public/podcast-episode.phtml index 9721927d7..54185b199 100644 --- a/templates/frontend/public/podcast-episode.phtml +++ b/templates/frontend/public/podcast-episode.phtml @@ -1,4 +1,8 @@ layout('minimal', [ 'hide_footer' => true, ]); -/** @var \App\Assets $assets */ -$assets->addInlineJs( - $this->fetch('frontend/public/podcast-episode.js', []) -); - -$episodeAudioSrc = (string) $router->named( +$episodeAudioSrc = $router->named( 'api:stations:podcast:episode:download', [ 'station_id' => $station->getId(), @@ -30,11 +29,20 @@ if ($episode->getPublishAt() !== null) { $publishedAt = CarbonImmutable::createFromTimestamp($episode->getPublishAt()); } -$this->push('head'); +$sections->append( + 'head', + << + HTML +); + +$sections->appendStart('bodyjs'); ?> - - - + end(); ?> @@ -43,16 +51,17 @@ $this->end();
-

e($podcast->getTitle())?>

+

e($podcast->getTitle()) ?>

@@ -60,31 +69,34 @@ $this->end();
- named( 'api:stations:podcast:episode:art', [ 'station_id' => $station->getId(), 'podcast_id' => $podcast->getId(), 'episode_id' => $episode->getId() . '|' . $episode->getArtUpdatedAt(), ] - );?>" class="card-img img-fluid" alt="e($podcast->getTitle())?>"> + ); ?>" class="card-img img-fluid" alt="e($podcast->getTitle()) ?>">
- format('d. M. Y')?> + format('d. M. Y') ?>
getExplicit()) : ?>
- +
- +
diff --git a/templates/frontend/public/podcast-episodes.js.phtml b/templates/frontend/public/podcast-episodes.js.phtml deleted file mode 100644 index e816a6591..000000000 --- a/templates/frontend/public/podcast-episodes.js.phtml +++ /dev/null @@ -1,3 +0,0 @@ -$(function () { - $('[data-toggle="tooltip"]').tooltip(); -}); diff --git a/templates/frontend/public/podcast-episodes.phtml b/templates/frontend/public/podcast-episodes.phtml index aebced38f..c213b37e2 100644 --- a/templates/frontend/public/podcast-episodes.phtml +++ b/templates/frontend/public/podcast-episodes.phtml @@ -1,4 +1,8 @@ layout( ] ); -/** @var \App\Assets $assets */ -$assets->addInlineJs( - $this->fetch('frontend/public/podcast-episodes.js', []) +$sections->append( + 'head', + << + HTML ); -$this->push('head'); +$sections->appendStart('bodyjs'); ?> - - - + end(); ?> @@ -29,20 +37,22 @@ $this->end();
-

+

-

e($podcast->getTitle())?>

+

e($podcast->getTitle()) ?>

diff --git a/templates/frontend/public/webdj.phtml b/templates/frontend/public/webdj.phtml new file mode 100644 index 000000000..d820da6ad --- /dev/null +++ b/templates/frontend/public/webdj.phtml @@ -0,0 +1,48 @@ +layout( + 'minimal', + [ + 'page_class' => 'dj station-' . $station->getShortName(), + 'hide_footer' => true, + 'title' => __('Web DJ') . ' - ' . $station->getName(), + 'manual' => true, + ] +); + +$jsLibs = [ + $this->assetUrl('dist/lib/webcaster/libshine.js'), + $this->assetUrl('dist/lib/webcaster/libsamplerate.js'), + $this->assetUrl('dist/lib/webcaster/taglib.js'), + $this->assetUrl('dist/lib/webcaster/webcast.js'), +]; + +$libUrls = []; +foreach ($jsLibs as $script) { + $libUrls[] = (string)($router->getBaseUrl()->withPath($script)); +} + +$scriptLines = []; +foreach ($jsLibs as $jsLib) { + $scriptLines[] = << + HTML; +} + +$sections->append('bodyjs', implode("\n", $scriptLines)); + +echo $this->fetch( + 'partials/vue_body', + [ + 'component' => 'Vue_PublicWebDJ', + 'id' => 'web_dj', + 'props' => [ + 'stationName' => $station->getName(), + 'libUrls' => $libUrls, + 'baseUri' => $wss_url, + ], + ] +); diff --git a/templates/mail/forgot.phtml b/templates/mail/forgot.phtml index 0e4d9ff22..a7d4740d1 100644 --- a/templates/mail/forgot.phtml +++ b/templates/mail/forgot.phtml @@ -1,18 +1,18 @@ - getAppName()) ?> - - named( - 'account:recover', - ['token' => $token], - [], - true + routeName: 'account:recover', + routeParams: ['token' => $token], + absolute: true )?> diff --git a/templates/main.phtml b/templates/main.phtml index d8c7b30ec..d24f792cf 100644 --- a/templates/main.phtml +++ b/templates/main.phtml @@ -6,11 +6,11 @@ * @var App\Http\Router $router * @var App\Session\Flash $flash * @var App\Customization $customization - * @var App\Assets $assets * @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; @@ -26,29 +26,44 @@ $header ??= null; <?= $customization->getPageTitle($title) ?> - fetch('partials/icons') ?> + fetch('partials/head') ?> - section('head') ?> + - load('main') - ->addInlineCss($customization->getCustomInternalCss()); + get('head') ?> - echo $assets->css(); - echo $assets->js(); - ?> + - has('sidebar')): ?>has-sidebar"> -inlineJs()?> - +fetch('partials/bodyjs') ?> + +get('bodyjs') ?> + + + + +if ($sections->has('sidebar')): ?>
class="content-alt"has('sidebar')): ?>class="content-alt" role="main">