bifurqué depuis cliss21/pfm-site
1
0
Bifurcation 0

feat: passe à wagtail-webradio 0.2.1

develop 0.5.0
Jérôme Lebleu 2022-02-28 18:28:46 +01:00
Parent 4f90961263
révision 13b22c5b01
14 fichiers modifiés avec 212 ajouts et 761 suppressions

Voir le fichier

@ -1,41 +0,0 @@
import AudioPlayerController from '../vendor/audioplayer/controller';
const LABEL_LOADING = 'Chargement…';
const LABEL_PLAY = 'Lire le titre';
const LABEL_PAUSE = 'Mettre en pause le titre';
const CLASS_NAME_ICON = 'fa fa-fw';
const CLASS_NAME_ICON_LOADING = `${CLASS_NAME_ICON} fa-circle-o-notch fa-spin`;
const CLASS_NAME_ICON_PLAY = `${CLASS_NAME_ICON} fa-play-circle`;
const CLASS_NAME_ICON_PAUSE = `${CLASS_NAME_ICON} fa-pause-circle`;
export default class extends AudioPlayerController {
static targets = ['toggler'];
loadingValueChanged(value, previousValue) {
super.loadingValueChanged(value, previousValue);
this._updateToggleBtn();
}
playingValueChanged(value, previousValue) {
super.playingValueChanged(value, previousValue);
this._updateToggleBtn();
}
_updateToggleBtn() {
const icon = this.togglerTarget.querySelector('.fa');
if (this.loadingValue) {
this.togglerTarget.setAttribute('aria-label', LABEL_LOADING);
icon.className = CLASS_NAME_ICON_LOADING;
} else if (this.playingValue) {
this.togglerTarget.setAttribute('aria-label', LABEL_PAUSE);
icon.className = CLASS_NAME_ICON_PAUSE;
} else {
this.togglerTarget.setAttribute('aria-label', LABEL_PLAY);
icon.className = CLASS_NAME_ICON_PLAY;
}
}
}

Voir le fichier

@ -1,21 +0,0 @@
import { Controller } from '@hotwired/stimulus';
import { formatTime } from '../vendor/player';
export default class extends Controller {
static values = { url: String };
connect() {
this.audio = new Audio();
this.audio.preload = 'metadata';
this.audio.addEventListener('loadedmetadata', () => {
this._setDuration(this.audio.duration);
});
this.audio.src = this.urlValue;
}
_setDuration(duration) {
this.element.innerHTML = formatTime(duration);
}
}

Voir le fichier

@ -3,68 +3,16 @@ __webpack_public_path__ = window.STATIC_URL || '/';
import '@hotwired/turbo';
import { Application } from '@hotwired/stimulus';
import PlayerController from './vendor/player';
import Duration from './controllers/duration';
import './vendor/bootstrap';
import Offcanvas from 'bootstrap/offcanvas';
import EventHandler from 'bootstrap/dom/event-handler';
import SelectorEngine from 'bootstrap/dom/selector-engine';
const ATTR_PLAYER_ADD = 'data-player-add';
const ATTR_PLAYER_ADD_PODCAST = 'data-player-add-podcast';
const ATTR_PLAYER_AUTOPLAY = 'data-player-autoplay';
/**
* ------------------------------------------------------------------------
* Main application
* ------------------------------------------------------------------------
*/
const application = Application.start();
application.register('player', PlayerController);
application.register('duration', Duration);
EventHandler.on(
document,
'click',
`[${ATTR_PLAYER_ADD}]`,
function ({ delegateTarget }) {
const component = window.Unicorn.getComponent('player');
const song = delegateTarget.getAttribute(ATTR_PLAYER_ADD);
const autoplay = delegateTarget.hasAttribute(ATTR_PLAYER_AUTOPLAY)
? 'True'
: 'False';
component.callMethod(`add(${song}, ${autoplay})`, 0, null, (err) => {
console.error(err);
});
}
);
EventHandler.on(
document,
'click',
`[${ATTR_PLAYER_ADD_PODCAST}]`,
function ({ delegateTarget }) {
const component = window.Unicorn.getComponent('player');
const id = Number.parseInt(
delegateTarget.getAttribute(ATTR_PLAYER_ADD_PODCAST),
10
);
const autoplay = delegateTarget.hasAttribute(ATTR_PLAYER_AUTOPLAY)
? 'True'
: 'False';
component.callMethod(`add_podcast(${id}, ${autoplay})`, 0, null, (err) => {
console.error(err);
});
}
);
window.addEventListener('load', () => {
document.documentElement.classList.remove('no-js');

Voir le fichier

@ -1,337 +0,0 @@
import { Controller } from '@hotwired/stimulus';
const STATE_PAUSED = 0;
const STATE_LOADING = 1;
const STATE_PLAYING = 2;
/**
* Format the time from seconds to MM:SS.
* @param {Number} time
* @return {String}
*/
export function formatTime(time) {
if (time === Infinity) {
return '--:--';
}
const min = Math.floor(time / 60);
const sec = Math.round(time - min * 60);
return `${min.toString().padStart(2, 0)}:${sec.toString().padStart(2, 0)}`;
}
export default class extends Controller {
/**
* The map of HTMLMediaElement events and related class method listeners.
* They will be automatically added to the Audio object once the controller
* is connected and removed when it is disconnected.
*/
static _audioEventsMap = new Map([
['error', '_onError'],
['ended', '_onEnd'],
['play', '_onPlay'],
['pause', '_onPause'],
['playing', '_onPlaying'],
['suspend', '_onSuspend'],
['waiting', '_onWaiting'],
['seeked', '_onSeeked'],
['seeking', '_onSeeking'],
['timeupdate', '_onTimeUpdate'],
['durationchange', '_onDurationChange'],
]);
initialize() {
// Initialize a new AudioPlayer instance and store it in Window to make it
// persistent across navigation when using Turbo
if (!window.currentAudio) {
const audio = new Audio();
audio.autoplay = false;
audio.preload = 'metadata';
window.currentAudio = audio;
}
this.audio = window.currentAudio;
// Bind event handlers to use them in connect() and disconnect()
for (const methodName of this.constructor._audioEventsMap.values()) {
this[methodName] = this[methodName].bind(this);
}
}
connect() {
for (const [event, methodName] of this.constructor._audioEventsMap) {
this.audio.addEventListener(event, this[methodName]);
}
}
disconnect() {
for (const [event, methodName] of this.constructor._audioEventsMap) {
this.audio.removeEventListener(event, this[methodName]);
}
}
// Getters
/**
* Whether a song is currently being played.
* @return {Boolean}
*/
get isPlaying() {
return this.audio.paused === false;
}
/**
* Whether a song is set and can be played.
* @return {Boolean}
*/
get canPlay() {
return this.audio.currentSrc !== '' && this.audio.readyState > 0;
}
// Actions
/**
* Play the current song or resume playback.
*/
play() {
if (!this.canPlay) {
throw new Error('No song has been loaded or is ready yet');
}
if (this.isPlaying) {
throw new Error('The song is already playing');
}
this.audio.play();
}
/**
* Pause the currently played song.
*/
pause() {
if (!this.canPlay) {
throw new Error('No song has been loaded or is ready yet');
}
if (!this.isPlaying) {
throw new Error('The song is already paused');
}
this.audio.pause();
}
/**
* Toggle the playing state.
*/
toggle() {
if (this.isPlaying) {
this.pause();
} else {
this.play();
}
}
// CSS Classes
static classes = ['loading', 'playing'];
// Targets
static targets = ['currentTime', 'duration', 'progressSlider', 'toggler'];
currentTimeTargetConnected(element) {
this._updateCurrentTimeElement(element);
}
durationTargetConnected(element) {
this._updateDurationElement(element);
}
progressSliderTargetConnected(element) {
element._inputListener = ({ target }) => this._seek(target.value);
element.addEventListener('input', element._inputListener);
this._updateProgressSliderElement(element);
}
progressSliderTargetDisconnected(element) {
element.removeEventListener('input', element._inputListener);
}
// Values
static values = {
autoplay: Boolean,
url: String,
};
urlValueChanged() {
const { currentSrc } = this.audio;
if (currentSrc !== this.urlValue) {
if (this.isPlaying) {
this.audio.pause();
}
this.audio.src = this.urlValue;
if (this.urlValue) {
// Force the loading state to don't wait for the audio events which could
// be delayed depending on the browser or the network
this._setState(STATE_LOADING);
if (currentSrc) {
this.audio.load();
}
if (this.autoplayValue) {
this.audio.play();
}
}
}
}
// Private methods
/**
* Set the current time of the loaded song.
* @param {Number} percentage - The percentage to the song duration.
*/
_seek(percentage) {
if (!this.canPlay) {
throw new Error('No song has been loaded or is ready yet');
}
if (percentage > 100) {
percentage = 100;
} else if (percentage < 0) {
percentage = 0;
}
this.audio.currentTime = percentage
? this.audio.duration * (percentage / 100)
: 0;
}
_setState(value) {
if (value === STATE_LOADING) {
this.togglerTarget.setAttribute('disabled', true);
this.togglerTarget.classList.remove(...this.playingClasses);
this.togglerTarget.classList.add(...this.loadingClasses);
} else {
this.togglerTarget.removeAttribute('disabled');
if (value === STATE_PLAYING) {
this.togglerTarget.classList.remove(...this.loadingClasses);
this.togglerTarget.classList.add(...this.playingClasses);
} else {
this.togglerTarget.classList.remove(
...this.loadingClasses,
...this.playingClasses
);
}
}
}
_updateCurrentTimeElement(element) {
element.innerText = formatTime(this.audio.currentTime);
}
_updateDurationElement(element) {
element.innerText = this.audio.duration
? formatTime(this.audio.duration)
: '--:--';
}
_updateProgressSliderElement(element) {
const { currentTime, duration } = this.audio;
const progress = duration ? (currentTime / duration) * 100 : 0;
element.value = progress;
element.style.setProperty('--value', `${progress}%`);
element.setAttribute('aria-valuenow', currentTime);
if (duration) {
element.removeAttribute('disabled');
} else {
element.setAttribute('disabled', true);
}
}
// Events handlers
_onError() {
this._setState(STATE_PAUSED);
this.audio.currentTime = 0;
this._onTimeUpdate();
console.error(
`Unable to load audio from ${this.urlValue} (${this.audio.error.message})`
);
}
_onPlay() {
this._setState(STATE_PLAYING);
}
_onPause() {
this._setState(STATE_PAUSED);
}
_onEnd() {
this._setState(STATE_PAUSED);
if (window.Unicorn) {
window.Unicorn.call('player', 'next');
}
}
_onPlaying() {
this._setState(this.isPlaying ? STATE_PLAYING : STATE_PAUSED);
}
_onSuspend() {
this._setState(this.isPlaying ? STATE_PLAYING : STATE_PAUSED);
}
_onWaiting() {
this._setState(STATE_LOADING);
}
_onSeeked() {
if (this.audio.paused === true) {
this._setState(STATE_PAUSED);
}
}
_onSeeking() {
// Consider seeking for the loading state too only when the sound is
// paused since it will generally not be handled by 'waiting' event.
if (this.audio.paused === true) {
this._setState(STATE_LOADING);
}
}
_onTimeUpdate() {
this.currentTimeTargets.forEach((element) => {
this._updateCurrentTimeElement(element);
});
this.progressSliderTargets.forEach((element) => {
this._updateProgressSliderElement(element);
});
}
_onDurationChange() {
this.durationTargets.forEach((element) => {
this._updateDurationElement(element);
});
this.progressSliderTargets.forEach((element) => {
this._updateProgressSliderElement(element);
});
}
}

Voir le fichier

@ -1,3 +1,9 @@
$player-color: $black !default;
$player-bg: $lighter !default;
$player-height: 45px !default;
$player-zindex: $zindex-fixed !default;
$player-breakpoint: 768px !default;
$player-range-thumb-height: 13px !default;
$player-range-thumb-bg: $component-active-bg !default;
$player-range-thumb-box-shadow: $box-shadow-inset !default;
@ -9,6 +15,19 @@ $player-range-track-bg: $gray-400 !default;
$player-range-fill-bg: $component-active-bg !default;
$player-time-current-color: $orange !default;
$player-playlist-min-width: 200px !default;
$player-playlist-bg: scale-color($player-bg, $lightness: -10%) !default;
$player-playlist-border-color: rgba($black, 0.25) !default;
$player-song-hover-color: $primary !default;
$player-song-current-color: $primary !default;
//
// Mixins
//
@mixin player-range-track() {
height: $player-range-track-height;
user-select: none;
@ -40,12 +59,57 @@ $player-range-fill-bg: $component-active-bg !default;
$player-range-thumb-active-box-shadow-color;
}
//
// Styles
//
.player {
position: fixed;
bottom: 0;
z-index: $zindex-fixed;
z-index: $player-zindex;
width: 100%;
background-color: $lighter;
color: $player-color;
background-color: $player-bg;
&.is-empty {
display: none;
}
}
.player__container {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
height: 100%;
padding: 0;
&--controls,
&--current {
flex-shrink: 0;
flex-wrap: nowrap;
width: 100%;
max-width: 100%;
}
&--current {
height: $player-height;
}
@media (min-width: $player-breakpoint) {
&--controls {
flex: 0 0 auto;
width: 65%;
}
&--current {
flex: 1 0 0%;
}
&--controls + &--current {
margin-left: 1rem;
}
}
}
.player-toggler {
@ -78,92 +142,174 @@ $player-range-fill-bg: $component-active-bg !default;
.player-progress {
position: relative;
width: 100%;
}
.player-progress__range {
appearance: none;
display: block;
width: 100%;
min-width: 0;
height: calc(
(#{$player-range-thumb-active-box-shadow-width} * 2) + #{$player-range-thumb-height}
);
padding: 0;
margin: 0;
color: $player-range-fill-bg;
background: transparent;
border: 0;
border-radius: calc(#{$player-range-thumb-height} * 2);
transition: box-shadow 0.3s ease;
// Webkit
&::-webkit-slider-runnable-track {
@include player-range-track;
}
&::-webkit-slider-thumb {
@include player-range-thumb;
&__range {
appearance: none;
margin-top: calc(
((#{$player-range-thumb-height} - #{$player-range-track-height}) / 2) * -1
display: block;
width: 100%;
min-width: 0;
height: calc(
(#{$player-range-thumb-active-box-shadow-width} * 2) + #{$player-range-thumb-height}
);
}
// Mozilla
&::-moz-range-track {
@include player-range-track;
}
&::-moz-range-thumb {
@include player-range-thumb;
}
// Focus and active styles
&::-moz-focus-outer {
padding: 0;
margin: 0;
color: $player-range-fill-bg;
background: transparent;
border: 0;
}
border-radius: calc(#{$player-range-thumb-height} * 2);
transition: box-shadow 0.3s ease;
&:focus {
outline: 0;
}
// Webkit
&::-webkit-slider-runnable-track {
@include player-range-track;
}
&:active {
&::-webkit-slider-thumb {
@include player-range-thumb-active;
@include player-range-thumb;
appearance: none;
margin-top: calc(
((#{$player-range-thumb-height} - #{$player-range-track-height}) / 2) * -1
);
}
// Mozilla
&::-moz-range-track {
@include player-range-track;
}
&::-moz-range-thumb {
@include player-range-thumb-active;
@include player-range-thumb;
}
// Focus and active styles
&::-moz-focus-outer {
border: 0;
}
&:focus {
outline: 0;
}
&:active {
&::-webkit-slider-thumb {
@include player-range-thumb-active;
}
&::-moz-range-thumb {
@include player-range-thumb-active;
}
}
}
}
.player-time {
margin: 0 1rem;
font-size: 0.8rem;
white-space: nowrap;
&--current {
color: $orange;
color: $player-time-current-color;
}
}
.player-current {
height: 45px;
// Song elements
.song {
padding: 0;
clear: both;
color: inherit;
text-align: inherit;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border: 0;
&[type='button']:not(:disabled) {
&:hover {
color: $player-song-hover-color;
}
}
&__thumbnail {
max-width: 100%;
height: auto;
margin-right: 0.5rem;
}
&__title,
&__subtitle {
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 0;
line-height: 1;
text-transform: uppercase;
}
&__title {
font-size: 1rem;
font-weight: $font-weight-bold;
}
&__subtitle {
font-size: 0.8rem;
}
&__title + &__subtitle {
margin-top: 0.125rem;
}
&__time {
margin: 0 0.5rem 0 1rem;
font-size: 0.8rem;
white-space: nowrap;
}
}
.player-current__title,
.player-current__subtitle {
margin-bottom: 0;
line-height: 1;
text-transform: uppercase;
.song-title {
flex-grow: 1;
overflow: hidden;
}
.player-current__title {
font-size: 1rem;
font-weight: $font-weight-bold;
// Playlist
.playlist {
position: absolute;
right: 0;
bottom: 100%;
display: none;
min-width: $player-playlist-min-width;
max-width: 100%;
padding: 1rem;
background-color: $player-playlist-bg;
&__song {
display: flex;
flex-wrap: nowrap;
align-items: center;
&--current {
color: $player-song-current-color;
}
& + & {
padding-top: 0.75rem;
margin-top: 0.75rem;
border-top: 1px solid $player-playlist-border-color;
}
}
}
.player-current__subtitle {
font-size: 0.8rem;
.playlist-toggler {
height: 100%;
border-radius: 0;
transition: none;
&[aria-expanded='true'] {
background-color: $player-playlist-bg;
~ .playlist {
display: block;
}
}
}

11
package-lock.json générée
Voir le fichier

@ -10,7 +10,6 @@
"license": "AGPL-3.0+",
"dependencies": {
"@fontsource/open-sans": "^4.5.5",
"@hotwired/stimulus": "^3.0.1",
"@hotwired/turbo": "^7.1.0",
"@popperjs/core": "^2.11.2",
"bootstrap": "~5.1.3",
@ -1695,11 +1694,6 @@
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.5.5.tgz",
"integrity": "sha512-h1oUPSQpoMnDrnzIZTVS9PPBFhWXS87v6/cd9FY2Xc+GKbOVcjPZxcvUDU1TnCie2QSoYY9aifERRV/d8JHtWQ=="
},
"node_modules/@hotwired/stimulus": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.0.1.tgz",
"integrity": "sha512-oHsJhgY2cip+K2ED7vKUNd2P+BEswVhrCYcJ802DSsblJFv7mPFVk3cQKvm2vHgHeDVdnj7oOKrBbzp1u8D+KA=="
},
"node_modules/@hotwired/turbo": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-7.1.0.tgz",
@ -11182,11 +11176,6 @@
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.5.5.tgz",
"integrity": "sha512-h1oUPSQpoMnDrnzIZTVS9PPBFhWXS87v6/cd9FY2Xc+GKbOVcjPZxcvUDU1TnCie2QSoYY9aifERRV/d8JHtWQ=="
},
"@hotwired/stimulus": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.0.1.tgz",
"integrity": "sha512-oHsJhgY2cip+K2ED7vKUNd2P+BEswVhrCYcJ802DSsblJFv7mPFVk3cQKvm2vHgHeDVdnj7oOKrBbzp1u8D+KA=="
},
"@hotwired/turbo": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-7.1.0.tgz",

Voir le fichier

@ -14,7 +14,6 @@
},
"dependencies": {
"@fontsource/open-sans": "^4.5.5",
"@hotwired/stimulus": "^3.0.1",
"@hotwired/turbo": "^7.1.0",
"@popperjs/core": "^2.11.2",
"bootstrap": "~5.1.3",

Voir le fichier

Voir le fichier

@ -1,178 +0,0 @@
from collections import OrderedDict
from collections.abc import Sequence
from dataclasses import dataclass
from django_unicorn.components import UnicornView
from wagtail_webradio.models import Podcast
@dataclass
class Song:
url: str
title: str
subtitle: str
download_url: str = ''
thumbnail_url: str = ''
class Playlist(Sequence):
def __init__(self):
self.data = OrderedDict()
self._current_id = None
self._previous_id = None
self._next_id = None
self._last_id = 0
def __len__(self):
return len(self.data)
def __getitem__(self, key):
return self.data[key]
def __iter__(self):
return iter(self.data)
def __contains__(self, key):
return key in self.data
def __repr__(self):
return repr(self.data)
def items(self):
return self.data.items()
@property
def current_id(self):
return self._current_id
@current_id.setter
def current_id(self, value):
if value == self._current_id:
return
if value not in self:
raise ValueError("'%s' is not in the playlist" % value)
self._current_id = value
self._update_indexes()
@property
def current(self):
return None if self._current_id is None else self[self._current_id]
@property
def has_previous(self):
return self._previous_id is not None
@property
def has_next(self):
return self._next_id is not None
def add(self, value):
self._last_id += 1
self.data[self._last_id] = value
self._update_indexes()
return self._last_id
def previous(self):
if not self.has_previous:
raise KeyError("There is no previous song")
self.current_id = self._previous_id
return self.current_id
def next(self):
if not self.has_next:
raise KeyError("There is no next song")
self.current_id = self._next_id
return self.current_id
def _update_indexes(self):
self._previous_id = None
self._next_id = None
keys = list(self.data.keys())
try:
index = keys.index(self._current_id)
except ValueError:
return
if index > 0:
self._previous_id = keys[index - 1]
if index + 1 < len(keys):
self._next_id = keys[index + 1]
class PlayerView(UnicornView):
autoplay: bool = False
current_id: int = None
playlist = Playlist()
class Meta:
javascript_exclude = ('current_id', 'playlist')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['current'] = self.playlist.current
return context
# Hooks
def mount(self):
self.current_id = self.playlist.current_id
def updated_current_id(self, value: int):
if value not in self.playlist:
raise AttributeError("This id is not in the playlist")
self.playlist.current_id = value
# Actions
def play(self, song_id: int):
self._set_property('current_id', song_id)
def previous(self):
try:
self.current_id = self.playlist.previous()
except KeyError:
return
self.autoplay = True
def next(self):
try:
self.current_id = self.playlist.next()
except KeyError:
return
self.autoplay = True
def add(self, song, autoplay: bool = True):
if isinstance(song, dict):
song = Song(**song)
song_id = self.playlist.add(song)
if self.current_id is None or autoplay:
self.autoplay = autoplay
self.play(song_id)
def add_podcast(self, podcast_id: int, autoplay: bool = True):
podcast = self._get_podcast(podcast_id)
self.add(self._get_song_from_podcast(podcast), autoplay)
# Private
def _get_podcast(self, podcast_id):
return Podcast.objects.get(pk=podcast_id)
def _get_song_from_podcast(self, podcast):
song = Song(
url=podcast.sound_url,
title=podcast.title,
subtitle=podcast.radio_show.title,
download_url=podcast.sound_url,
)
if podcast.picture:
song.thumbnail_url = podcast.picture.get_rendition('fill-45x45').url
return song

Voir le fichier

@ -1,54 +0,0 @@
<div class="player{% if not playlist %} d-none{% endif %}" data-controller="player" data-player-loading-class="is-loading" data-player-playing-class="is-playing" data-player-autoplay-value="{{ autoplay|lower }}" data-player-url-value="{{ current.url }}">
<div class="container h-100">
<div class="row h-100">
<div class="col-md-8 d-flex align-items-center">
<button class="btn" aria-label="Passer au titre précédent"{% if not playlist.has_previous %} disabled{% endif %} unicorn:click="previous">
<i class="fa fa-step-backward" aria-hidden="true"></i>
</button>
<button class="btn player-toggler" aria-label="Basculer la lecture" disabled data-action="player#toggle" data-player-target="toggler" unicorn:ignore>
<span class="player-toggler__play">
<i class="fa fa-fw fa-play-circle" aria-hidden="true"></i>
<span class="visually-hidden">Lire</span>
</span>
<span class="player-toggler__pause">
<i class="fa fa-fw fa-pause-circle" aria-hidden="true"></i>
<span class="visually-hidden">Mettre en pause</span>
</span>
<span class="player-toggler__loading">
<i class="fa fa-fw fa-circle-o-notch fa-spin" aria-hidden="true"></i>
<span class="visually-hidden">Chargement…</span>
</span>
</button>
<button class="btn" aria-label="Passer au titre suivant"{% if not playlist.has_next %} disabled{% endif %} unicorn:click="next">
<i class="fa fa-step-forward" aria-hidden="true"></i>
</button>
<time class="player-time player-time--current px-3" data-player-target="currentTime" unicorn:ignore></time>
<div class="player-progress" unicorn:ignore>
<input type="range" class="player-progress__range" min="0" max="100" step="0.01" value="0" autocomplete="off" role="slider" aria-label="Naviguer dans le son" aria-valuenow="0" aria-valuemin="0" aria-valuemax="0" data-player-target="progressSlider">
</div>
<time class="player-time ps-3" data-player-target="duration" unicorn:ignore>--:--</time>
</div>
<div class="col-md-4 d-flex align-items-center player-current">
{% if current.thumbnail_url %}
<img src="{{ current.thumbnail_url }}" class="me-2 player-current__thumbnail">
{% endif %}
<div class="flex-grow-1">
<h5 class="player-current__title">{{ current.title }}</h5>
<h6 class="player-current__subtitle">{{ current.subtitle }}</h6>
</div>
{% if current.download_url %}
<a href="{{ current.download_url }}" class="btn player-current__download" aria-label="Télécharger ce podcast">
<i class="fa fa-download" aria-hidden="true"></i>
</a>
{% endif %}
</div>
</div>
</div>
</div>

Voir le fichier

@ -297,5 +297,5 @@ WAGTAILSEARCH_BACKENDS = {
# ------------------------------------------------------------------------------
# https://www.django-unicorn.com/docs/settings/
UNICORN = {
'APPS': ['pfmsite.radio'],
'APPS': ['wagtail_webradio'],
}

Voir le fichier

@ -37,6 +37,7 @@
window.STATIC_URL = '{% get_static_prefix %}';
</script>
<script src="{% static "main.js" %}" defer></script>
<script src="{% static "wagtail_webradio/player/js/main.js" %}" defer></script>
{% endblock %}
{% block extra_head %}{% endblock %}

Voir le fichier

@ -21,7 +21,7 @@
{% image settings.core.displaysettings.podcast_default_picture fill-480x480 class="img-fluid img-border podcast-img" %}
{% endif %}
<button class="btn p-0 podcast-img-btn" data-player-add-podcast="{{ podcast.pk }}" data-player-autoplay>{% include "../play_button.svg" with size=120 %}</button>
<time class="podcast-img-time" data-controller="duration" data-duration-url-value="{{ podcast.sound_url }}">--:--</time>
<time class="podcast-img-time">{{ podcast.get_duration_display }}</time>
</div>
</div>
</div >

Voir le fichier

@ -4,13 +4,12 @@ django >=3.2,<4
django-environ ==0.4.5
django-filter >= 21.1,<22
django-tapeforms >=1.1,<1.2 # https://github.com/stephrdev/django-tapeforms/
django-unicorn ~=0.42.1
# Wagtail
# ------------------------------------------------------------------------------
wagtail >=2.15,<2.16
wagtail-cblocks >=0.3,<0.4 # https://forge.cliss21.org/cliss21/wagtail-cblocks
wagtail-webradio ==0.1.0
wagtail-webradio[components] ~=0.2.1
# Autre
# ------------------------------------------------------------------------------