diff --git a/app/Controllers/EpisodeController.php b/app/Controllers/EpisodeController.php
index c484ca14..e51ba7b7 100644
--- a/app/Controllers/EpisodeController.php
+++ b/app/Controllers/EpisodeController.php
@@ -103,7 +103,7 @@ class EpisodeController extends BaseController
public function embeddablePlayer(string $theme = 'light-transparent'): string
{
- header('Content-Security-Policy: frame-ancestors https://* http://*');
+ header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
@@ -122,12 +122,13 @@ class EpisodeController extends BaseController
$cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
if (! ($cachedView = cache($cacheName))) {
- $theme = EpisodeModel::$themes[$theme];
+ $themeData = EpisodeModel::$themes[$theme];
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'theme' => $theme,
+ 'themeData' => $themeData,
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
@@ -159,9 +160,9 @@ class EpisodeController extends BaseController
'html' =>
'',
+ '" width="100%" height="144" frameborder="0" scrolling="no">',
'width' => 600,
- 'height' => 200,
+ 'height' => 144,
'thumbnail_url' => $this->episode->image->large_url,
'thumbnail_width' => config('Images')
->largeSize,
@@ -189,11 +190,11 @@ class EpisodeController extends BaseController
htmlentities(
'',
+ '" width="100%" height="144" frameborder="0" scrolling="no">',
),
);
$oembed->addChild('width', '600');
- $oembed->addChild('height', '200');
+ $oembed->addChild('height', '144');
return $this->response->setXML((string) $oembed);
}
diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php
index 90e33641..58f80a3a 100644
--- a/app/Helpers/components_helper.php
+++ b/app/Helpers/components_helper.php
@@ -185,7 +185,7 @@ if (! function_exists('data_table')) {
* @param mixed[] $data data to loop through and display in rows
* @param mixed ...$rest Any other argument to pass to the `cell` function
*/
- function data_table(array $columns, array $data = [], ...$rest): string
+ function data_table(array $columns, array $data = [], string $class = '', ...$rest): string
{
$table = new Table();
@@ -199,8 +199,8 @@ if (! function_exists('data_table')) {
'cell_start' => '
',
'cell_alt_start' => ' | ',
- 'row_start' => ' | ',
- 'row_alt_start' => '
',
+ 'row_start' => '
',
+ 'row_alt_start' => '
',
];
$table->setTemplate($template);
@@ -225,7 +225,7 @@ if (! function_exists('data_table')) {
return lang('Common.no_data');
}
- return '' .
+ return '
' .
$table->generate() .
'
';
}
@@ -241,28 +241,16 @@ if (! function_exists('publication_pill')) {
*/
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
{
- if ($publicationDate === null) {
- return '';
- }
+ $class = match ($publicationStatus) {
+ 'published' => 'text-pine-600 border-pine-600 bg-pine-50',
+ 'scheduled' => 'text-red-600 border-red-600 bg-red-50',
+ 'not_published' => 'text-gray-600 border-gray-600 bg-gray-50',
+ default => 'text-gray-600 border-gray-600 bg-gray-50',
+ };
- $class =
- $publicationStatus === 'published'
- ? 'text-pine-500 border-pine-500'
- : 'text-red-600 border-red-600';
+ $label = lang('Episode.publication_status.' . $publicationStatus);
- $langOptions = [
- '
',
- ];
-
- $label = lang('Episode.publication_status.' . $publicationStatus, $langOptions);
-
- return '
+ CODE_SAMPLE;
+ }
+}
+
+// ------------------------------------------------------------------------
+
+
+if (! function_exists('audio_player')) {
+ /**
+ * Returns audio player
+ */
+ function audio_player(string $source, string $mediaType, string $class = ''): string
+ {
+ $language = service('request')
+ ->getLocale();
+
+ return <<
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CODE_SAMPLE;
+ }
+}
diff --git a/app/Helpers/misc_helper.php b/app/Helpers/misc_helper.php
index f45726b0..21a7ae5c 100644
--- a/app/Helpers/misc_helper.php
+++ b/app/Helpers/misc_helper.php
@@ -136,20 +136,26 @@ if (! function_exists('slugify')) {
if (! function_exists('format_duration')) {
/**
- * Formats duration in seconds to an hh:mm:ss string
+ * Formats duration in seconds to an hh:mm:ss string. Doesn't show leading zeros if any.
+ *
+ * ⚠️ This uses php's gmdate function so any duration > 86000 seconds (24 hours) will not be formatted properly.
*
* @param int $seconds seconds to format
*/
- function format_duration(int $seconds, string $separator = ':'): string
+ function format_duration(int $seconds): string
{
- return sprintf(
- '%02d%s%02d%s%02d',
- floor($seconds / 3600),
- $separator,
- ($seconds / 60) % 60,
- $separator,
- $seconds % 60,
- );
+ if ($seconds < 60) {
+ return '0:' . $seconds;
+ }
+ if ($seconds < 3600) {
+ // < 1 hour: returns MM:SS
+ return ltrim(gmdate('i:s', $seconds), '0');
+ }
+ if ($seconds < 36000) {
+ // < 10 hours: returns H:MM:SS
+ return ltrim(gmdate('h:i:s', $seconds), '0');
+ }
+ return gmdate('h:i:s', $seconds);
}
}
diff --git a/app/Language/en/Common.php b/app/Language/en/Common.php
index 0bf26a68..28afa799 100644
--- a/app/Language/en/Common.php
+++ b/app/Language/en/Common.php
@@ -41,4 +41,8 @@ return [
'upload_file' => 'Upload a file',
'remote_url' => 'Remote URL',
],
+ 'play_episode_button' => [
+ 'play' => 'Play',
+ 'playing' => 'Playing',
+ ],
];
diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php
index 4cd45688..94f5fc16 100644
--- a/app/Language/en/Episode.php
+++ b/app/Language/en/Episode.php
@@ -44,10 +44,16 @@ return [
'go_to_page' => 'Go to page',
'create' => 'Add an episode',
'publication_status' => [
- 'published' => 'Published on {0}',
- 'scheduled' => 'Scheduled for {0}',
+ 'published' => 'Published',
+ 'scheduled' => 'Scheduled',
'not_published' => 'Not published',
],
+ 'list' => [
+ 'episode' => 'Episode',
+ 'visibility' => 'Visibility',
+ 'comments' => 'Comments',
+ 'actions' => 'Actions',
+ ],
'form' => [
'warning' =>
'In case of fatal error, try increasing the `memory_limit`, `upload_max_filesize` and `post_max_size` values in your php configuration file then restart your web server.
These values must be higher than the audio file you wish to upload.',
diff --git a/app/Language/fr/Common.php b/app/Language/fr/Common.php
index a3415467..64997463 100644
--- a/app/Language/fr/Common.php
+++ b/app/Language/fr/Common.php
@@ -41,4 +41,8 @@ return [
'upload_file' => 'Téléversez un fichier',
'remote_url' => 'URL distante',
],
+ 'play_episode_button' => [
+ 'play' => 'Lire',
+ 'playing' => 'En cours',
+ ],
];
diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php
index 5f75b506..3a655787 100644
--- a/app/Language/fr/Episode.php
+++ b/app/Language/fr/Episode.php
@@ -44,10 +44,16 @@ return [
'go_to_page' => 'Voir',
'create' => 'Ajouter un épisode',
'publication_status' => [
- 'published' => 'Publié le {0}',
- 'scheduled' => 'Planifié pour le {0}',
+ 'published' => 'Publié',
+ 'scheduled' => 'Planifié',
'not_published' => 'Non publié',
],
+ 'list' => [
+ 'episode' => 'Épisode',
+ 'visibility' => 'Visibilité',
+ 'comments' => 'Commentaires',
+ 'actions' => 'Actions',
+ ],
'form' => [
'warning' =>
'En cas d’erreur fatale, essayez d’augmenter les valeurs de `memory_limit`, `upload_max_filesize` et `post_max_size` dans votre fichier de configuration php puis redémarrez votre serveur web.
Les valeurs doivent être plus grandes que le fichier audio que vous souhaitez téléverser.',
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index 78bebf53..d5540624 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -43,7 +43,7 @@ class EpisodeModel extends Model
],
'dark' => [
'style' => 'background-color: #001f1a;',
- 'background' => '#001f1a',
+ 'background' => '#313131',
'text' => '#fff',
'inverted' => '#000',
],
diff --git a/app/Resources/icons/disc.svg b/app/Resources/icons/disc.svg
new file mode 100644
index 00000000..095d2cda
--- /dev/null
+++ b/app/Resources/icons/disc.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/Resources/icons/pause.svg b/app/Resources/icons/pause.svg
new file mode 100644
index 00000000..81cffce1
--- /dev/null
+++ b/app/Resources/icons/pause.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/Resources/icons/volume-high.svg b/app/Resources/icons/volume-high.svg
new file mode 100644
index 00000000..0aa5be36
--- /dev/null
+++ b/app/Resources/icons/volume-high.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/Resources/icons/volume-low.svg b/app/Resources/icons/volume-low.svg
new file mode 100644
index 00000000..6acfade5
--- /dev/null
+++ b/app/Resources/icons/volume-low.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/Resources/icons/volume-mute.svg b/app/Resources/icons/volume-mute.svg
new file mode 100644
index 00000000..79bd55ac
--- /dev/null
+++ b/app/Resources/icons/volume-mute.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/Resources/js/audio-player.ts b/app/Resources/js/audio-player.ts
new file mode 100644
index 00000000..0a120376
--- /dev/null
+++ b/app/Resources/js/audio-player.ts
@@ -0,0 +1,125 @@
+import {
+ VmAudio,
+ VmCaptions,
+ VmClickToPlay,
+ VmControl,
+ VmControls,
+ VmCurrentTime,
+ VmDefaultControls,
+ VmDefaultSettings,
+ VmDefaultUi,
+ VmEndTime,
+ VmFile,
+ VmIcon,
+ VmIconLibrary,
+ VmLoadingScreen,
+ VmMenu,
+ VmMenuItem,
+ VmMenuRadio,
+ VmMenuRadioGroup,
+ VmMuteControl,
+ VmPlaybackControl,
+ VmPlayer,
+ VmScrubberControl,
+ VmSettings,
+ VmSettingsControl,
+ VmSkeleton,
+ VmSlider,
+ VmSubmenu,
+ VmTime,
+ VmTimeProgress,
+ VmTooltip,
+ VmUi,
+ VmVolumeControl,
+} from "@vime/core";
+import "@vime/core/themes/default.css";
+import "@vime/core/themes/light.css";
+import { html, render } from "lit";
+import "./modules/play-episode-button";
+
+const player = html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+
+render(player, document.body);
+
+// Register Castopod's icons library
+const library: HTMLVmIconLibraryElement | null = document.querySelector(
+ 'vm-icon-library[name="castopod-icons"]'
+);
+if (library) {
+ library.resolver = (iconName) => `/assets/icons/${iconName}.svg`;
+}
+
+// Vime elements for audio player
+customElements.define("vm-player", VmPlayer);
+customElements.define("vm-file", VmFile);
+customElements.define("vm-audio", VmAudio);
+customElements.define("vm-ui", VmUi);
+customElements.define("vm-default-ui", VmDefaultUi);
+customElements.define("vm-click-to-play", VmClickToPlay);
+customElements.define("vm-captions", VmCaptions);
+customElements.define("vm-loading-screen", VmLoadingScreen);
+customElements.define("vm-default-controls", VmDefaultControls);
+customElements.define("vm-default-settings", VmDefaultSettings);
+customElements.define("vm-controls", VmControls);
+customElements.define("vm-playback-control", VmPlaybackControl);
+customElements.define("vm-volume-control", VmVolumeControl);
+customElements.define("vm-scrubber-control", VmScrubberControl);
+customElements.define("vm-current-time", VmCurrentTime);
+customElements.define("vm-end-time", VmEndTime);
+customElements.define("vm-settings-control", VmSettingsControl);
+customElements.define("vm-time-progress", VmTimeProgress);
+customElements.define("vm-control", VmControl);
+customElements.define("vm-icon", VmIcon);
+customElements.define("vm-icon-library", VmIconLibrary);
+customElements.define("vm-tooltip", VmTooltip);
+customElements.define("vm-mute-control", VmMuteControl);
+customElements.define("vm-slider", VmSlider);
+customElements.define("vm-time", VmTime);
+customElements.define("vm-menu", VmMenu);
+customElements.define("vm-menu-item", VmMenuItem);
+customElements.define("vm-submenu", VmSubmenu);
+customElements.define("vm-menu-radio-group", VmMenuRadioGroup);
+customElements.define("vm-menu-radio", VmMenuRadio);
+customElements.define("vm-settings", VmSettings);
+customElements.define("vm-skeleton", VmSkeleton);
diff --git a/app/Resources/js/embed.ts b/app/Resources/js/embed.ts
new file mode 100644
index 00000000..4689cf43
--- /dev/null
+++ b/app/Resources/js/embed.ts
@@ -0,0 +1,78 @@
+import {
+ VmAudio,
+ VmCaptions,
+ VmClickToPlay,
+ VmControl,
+ VmControls,
+ VmCurrentTime,
+ VmDefaultControls,
+ VmDefaultSettings,
+ VmDefaultUi,
+ VmEndTime,
+ VmFile,
+ VmIcon,
+ VmIconLibrary,
+ VmLoadingScreen,
+ VmMenu,
+ VmMenuItem,
+ VmMenuRadio,
+ VmMenuRadioGroup,
+ VmMuteControl,
+ VmPlaybackControl,
+ VmPlayer,
+ VmScrubberControl,
+ VmSettings,
+ VmSettingsControl,
+ VmSkeleton,
+ VmSlider,
+ VmSubmenu,
+ VmTime,
+ VmTimeProgress,
+ VmTooltip,
+ VmUi,
+ VmVolumeControl,
+} from "@vime/core";
+import "@vime/core/themes/default.css";
+import "@vime/core/themes/light.css";
+
+// Vime elements for audio player
+customElements.define("vm-player", VmPlayer);
+customElements.define("vm-file", VmFile);
+customElements.define("vm-audio", VmAudio);
+customElements.define("vm-ui", VmUi);
+customElements.define("vm-default-ui", VmDefaultUi);
+customElements.define("vm-click-to-play", VmClickToPlay);
+customElements.define("vm-captions", VmCaptions);
+customElements.define("vm-loading-screen", VmLoadingScreen);
+customElements.define("vm-default-controls", VmDefaultControls);
+customElements.define("vm-default-settings", VmDefaultSettings);
+customElements.define("vm-controls", VmControls);
+customElements.define("vm-playback-control", VmPlaybackControl);
+customElements.define("vm-volume-control", VmVolumeControl);
+customElements.define("vm-scrubber-control", VmScrubberControl);
+customElements.define("vm-current-time", VmCurrentTime);
+customElements.define("vm-end-time", VmEndTime);
+customElements.define("vm-settings-control", VmSettingsControl);
+customElements.define("vm-time-progress", VmTimeProgress);
+customElements.define("vm-control", VmControl);
+customElements.define("vm-icon", VmIcon);
+customElements.define("vm-icon-library", VmIconLibrary);
+customElements.define("vm-tooltip", VmTooltip);
+customElements.define("vm-mute-control", VmMuteControl);
+customElements.define("vm-slider", VmSlider);
+customElements.define("vm-time", VmTime);
+customElements.define("vm-menu", VmMenu);
+customElements.define("vm-menu-item", VmMenuItem);
+customElements.define("vm-submenu", VmSubmenu);
+customElements.define("vm-menu-radio-group", VmMenuRadioGroup);
+customElements.define("vm-menu-radio", VmMenuRadio);
+customElements.define("vm-settings", VmSettings);
+customElements.define("vm-skeleton", VmSkeleton);
+
+// Register Castopod's icons library
+const library: HTMLVmIconLibraryElement | null = document.querySelector(
+ 'vm-icon-library[name="castopod-icons"]'
+);
+if (library) {
+ library.resolver = (iconName) => `/assets/icons/${iconName}.svg`;
+}
diff --git a/app/Resources/js/modules/play-episode-button.ts b/app/Resources/js/modules/play-episode-button.ts
new file mode 100644
index 00000000..7ec0d417
--- /dev/null
+++ b/app/Resources/js/modules/play-episode-button.ts
@@ -0,0 +1,263 @@
+import { css, html, LitElement, TemplateResult } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+
+@customElement("play-episode-button")
+export class PlayEpisodeButton extends LitElement {
+ @property()
+ id = "0";
+
+ @property()
+ src = "";
+
+ @property()
+ mediaType = "";
+
+ @property()
+ title!: string;
+
+ @property()
+ podcast!: string;
+
+ @property()
+ imageSrc!: string;
+
+ @property()
+ playLabel!: string;
+
+ @property()
+ playingLabel!: string;
+
+ @property()
+ isPlaying!: boolean;
+
+ @property()
+ _castopodAudioPlayer!: HTMLDivElement;
+
+ @property()
+ _audio!: HTMLAudioElement;
+
+ @state()
+ _playbackSpeed = 1;
+
+ @state()
+ _events = [
+ {
+ name: "canplay",
+ onEvent: (event: Event): void => {
+ (event.target as HTMLAudioElement)?.play();
+ },
+ },
+ {
+ name: "play",
+ onEvent: (): void => {
+ this.isPlaying = true;
+ },
+ },
+ {
+ name: "pause",
+ onEvent: (): void => {
+ this.isPlaying = false;
+ },
+ },
+ {
+ name: "ratechange",
+ onEvent: (event: Event): void => {
+ this._playbackSpeed = (event.target as HTMLAudioElement)?.playbackRate;
+ console.log(this._playbackSpeed);
+ },
+ },
+ ];
+
+ async connectedCallback(): Promise {
+ super.connectedCallback();
+
+ await this._elementReady("div[id=castopod-audio-player]");
+ await this._elementReady("div[id=castopod-audio-player] audio");
+
+ this._castopodAudioPlayer = document.body.querySelector(
+ "div[id=castopod-audio-player]"
+ ) as HTMLDivElement;
+
+ this._audio = this._castopodAudioPlayer.querySelector(
+ "audio"
+ ) as HTMLAudioElement;
+ }
+
+ private _elementReady(selector: string) {
+ return new Promise((resolve) => {
+ const element = document.querySelector(selector);
+ if (element) {
+ resolve(element);
+ }
+ new MutationObserver((_, observer) => {
+ // Query for elements matching the specified selector
+ Array.from(document.querySelectorAll(selector)).forEach((element) => {
+ resolve(element);
+ //Once we have resolved we don't need the observer anymore.
+ observer.disconnect();
+ });
+ }).observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ });
+ });
+ }
+
+ play(): void {
+ const currentlyPlayingEpisode = this._castopodAudioPlayer.dataset.episode;
+
+ const isCurrentEpisode = currentlyPlayingEpisode === this.id;
+
+ if (currentlyPlayingEpisode === "-1") {
+ this._showPlayer();
+ }
+
+ if (isCurrentEpisode) {
+ this._audio.play();
+ } else {
+ const playingEpisodeButton = document.querySelector(
+ `play-episode-button[id="${currentlyPlayingEpisode}"]`
+ ) as PlayEpisodeButton;
+ if (playingEpisodeButton) {
+ this._flushLastPlayButton(playingEpisodeButton);
+ }
+
+ this._loadEpisode();
+ }
+ }
+
+ pause(): void {
+ this._audio.pause();
+ }
+
+ private _showPlayer(): void {
+ this._castopodAudioPlayer.style.display = "";
+ document.body.style.paddingBottom = "52px";
+ }
+
+ private _flushLastPlayButton(playingEpisodeButton: PlayEpisodeButton): void {
+ playingEpisodeButton.isPlaying = false;
+
+ for (const event of playingEpisodeButton._events) {
+ playingEpisodeButton._audio.removeEventListener(
+ event.name,
+ event.onEvent,
+ false
+ );
+ }
+
+ this._playbackSpeed = playingEpisodeButton._playbackSpeed;
+ }
+
+ private _loadEpisode(): void {
+ this._castopodAudioPlayer.dataset.episode = this.id;
+
+ this._audio.src = this.src;
+ this._audio.load();
+ this._audio.playbackRate = this._playbackSpeed;
+ for (const event of this._events) {
+ this._audio.addEventListener(event.name, event.onEvent, false);
+ }
+
+ const img: HTMLImageElement | null =
+ this._castopodAudioPlayer.querySelector("img");
+
+ if (img) {
+ img.src = this.imageSrc;
+ img.alt = this.title;
+ }
+
+ const episodeTitle: HTMLParagraphElement | null =
+ this._castopodAudioPlayer.querySelector('p[id="castopod-player-title"]');
+
+ if (episodeTitle) {
+ episodeTitle.title = this.title;
+ episodeTitle.innerHTML = this.title;
+ }
+
+ const podcastTitle: HTMLParagraphElement | null =
+ this._castopodAudioPlayer.querySelector(
+ 'p[id="castopod-player-podcast"]'
+ );
+
+ if (podcastTitle) {
+ podcastTitle.title = this.podcast;
+ podcastTitle.innerHTML = this.podcast;
+ }
+ }
+
+ static styles = css`
+ button {
+ background-color: #ffffff;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 600;
+ border-width: 2px;
+ border-style: solid;
+ border-radius: 9999px;
+ border-color: rgba(207, 247, 243, 1);
+
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ }
+
+ button:hover {
+ border-color: #009486;
+ background-color: #ebf8f8;
+ }
+
+ button:focus {
+ background-color: #ebf8f8;
+ }
+
+ svg {
+ font-size: 1.5rem;
+ margin-right: 0.25rem;
+ color: #009486;
+ }
+
+ @keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+ }
+
+ .animate-spin {
+ animation: spin 3s linear infinite;
+ }
+ `;
+
+ render(): TemplateResult<1> {
+ return html``;
+ }
+}
diff --git a/app/Resources/styles/inputRange.css b/app/Resources/styles/inputRange.css
new file mode 100644
index 00000000..053df137
--- /dev/null
+++ b/app/Resources/styles/inputRange.css
@@ -0,0 +1,86 @@
+.wrap {
+ display: flex;
+ align-items: center;
+ position: relative;
+ width: 12.5em;
+ height: 5.25em;
+ font: 1em/1 arial, sans-serif;
+}
+[type="range"] {
+ flex: 1;
+ margin: 0;
+ padding: 0;
+ min-height: 1.5em;
+ background: transparent;
+ font: inherit;
+}
+[type="range"],
+[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+}
+[type="range"]::-webkit-slider-runnable-track {
+ box-sizing: border-box;
+ border: none;
+ width: 12.5em;
+ height: 0.25em;
+ background: #ccc;
+}
+[type="range"]::-moz-range-track {
+ box-sizing: border-box;
+ border: none;
+ width: 12.5em;
+ height: 0.25em;
+ background: #ccc;
+}
+[type="range"]::-ms-track {
+ box-sizing: border-box;
+ border: none;
+ width: 12.5em;
+ height: 0.25em;
+ background: #ccc;
+}
+[type="range"]::-webkit-slider-thumb {
+ margin-top: -0.625em;
+ box-sizing: border-box;
+ border: none;
+ width: 1.5em;
+ height: 1.5em;
+ border-radius: 50%;
+ background: #f90;
+}
+[type="range"]::-moz-range-thumb {
+ box-sizing: border-box;
+ border: none;
+ width: 1.5em;
+ height: 1.5em;
+ border-radius: 50%;
+ background: #f90;
+}
+[type="range"]::-ms-thumb {
+ margin-top: 0;
+ box-sizing: border-box;
+ border: none;
+ width: 1.5em;
+ height: 1.5em;
+ border-radius: 50%;
+ background: #f90;
+}
+[type="range"]::-ms-tooltip {
+ display: none;
+}
+[type="range"] ~ output {
+ display: none;
+}
+.js [type="range"] ~ output {
+ display: block;
+ position: absolute;
+ left: 0.75em;
+ top: 0;
+ padding: 0.25em 0.5em;
+ border-radius: 3px;
+ transform: translate(
+ calc((var(--val) - var(--min)) / (var(--max) - var(--min)) * 11em - 50%)
+ );
+ background: #95a;
+ color: #eee;
+}
diff --git a/app/Views/_layout.php b/app/Views/_layout.php
index bf9bcded..5782c039 100644
--- a/app/Views/_layout.php
+++ b/app/Views/_layout.php
@@ -8,7 +8,9 @@
+
= service('vite')->asset('styles/index.css', 'css') ?>
+ = service('vite')->asset('js/audio-player.ts', 'js') ?>
diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php
index c5375af6..16bcf983 100644
--- a/app/Views/admin/_layout.php
+++ b/app/Views/admin/_layout.php
@@ -7,8 +7,10 @@
+
= service('vite')->asset('styles/index.css', 'css') ?>
= service('vite')->asset('js/admin.ts', 'js') ?>
+ = service('vite')->asset('js/audio-player.ts', 'js') ?>
diff --git a/app/Views/admin/contributor/list.php b/app/Views/admin/contributor/list.php
index f83064d9..d390902f 100644
--- a/app/Views/admin/contributor/list.php
+++ b/app/Views/admin/contributor/list.php
@@ -65,6 +65,7 @@
],
],
$podcast->contributors,
+ '',
$podcast,
) ?>
diff --git a/app/Views/admin/episode/embeddable_player.php b/app/Views/admin/episode/embeddable_player.php
index bb42a4a3..b15060b7 100644
--- a/app/Views/admin/episode/embeddable_player.php
+++ b/app/Views/admin/episode/embeddable_player.php
@@ -24,7 +24,7 @@
-
+
= form_textarea(
diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php
index 5780b47c..fdee95e1 100644
--- a/app/Views/admin/episode/list.php
+++ b/app/Views/admin/episode/list.php
@@ -5,9 +5,7 @@
= $this->endSection() ?>
= $this->section('pageTitle') ?>
-= lang('Episode.all_podcast_episodes') ?> (= $pager->getDetails()[
- 'total'
- ] ?>)
+= lang('Episode.all_podcast_episodes') ?> (= $pager->getDetails()['total'] ?>)
= $this->endSection() ?>
= $this->section('headerRight') ?>
@@ -21,110 +19,108 @@
= $this->section('content') ?>
= lang('Common.pageInfo', [
- 'currentPage' => $pager->getDetails()['currentPage'],
- 'pageCount' => $pager->getDetails()['pageCount'],
-]) ?>
-
-
-
-
-
-
-
-
- = publication_pill(
- $episode->published_at,
- $episode->publication_status,
- ) ?>
-
•
-
+ ) .
+ '' .
+ '
' .
+ '
' .
+ '' .
+ '' .
+ episode_numbering(
+ $episode->number,
+ $episode->season_number,
+ 'text-xs font-semibold text-gray-600',
+ true,
+ ) .
+ '-' .
+ '' . $episode->title . '' .
+ '
' .
+ '' . $episode->description . '
' .
+ '' .
+ '';
+ },
+ ],
+ [
+ 'header' => lang('Episode.list.visibility'),
+ 'cell' => function ($episode) {
+ return publication_pill(
+ $episode->published_at,
+ $episode->publication_status,
+ );
+ },
+ ],
+ [
+ 'header' => lang('Episode.list.comments'),
+ 'cell' => function ($episode) {
+ return count($episode->comments);
+ },
+ ],
+ [
+ 'header' => lang('Episode.list.actions'),
+ 'cell' => function ($episode, $podcast) {
+ return '
' .
+ '
' .
+ '
';
+ },
+ ],
+ ],
+ $episodes,
+ 'mb-6',
+ $podcast
+) ?>
= $pager->links() ?>
-= $this->endSection() ?>
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/admin/episode/publish.php b/app/Views/admin/episode/publish.php
index e1ed34bc..a3c192a2 100644
--- a/app/Views/admin/episode/publish.php
+++ b/app/Views/admin/episode/publish.php
@@ -73,10 +73,7 @@
-
+ = audio_player($episode->audio_file_url, $episode->audio_file_mimetype, 'mt-auto') ?>