diff --git a/packages/pillarbox-playlist/README.md b/packages/pillarbox-playlist/README.md index 2647ebd..2b1521d 100644 --- a/packages/pillarbox-playlist/README.md +++ b/packages/pillarbox-playlist/README.md @@ -38,12 +38,12 @@ Once the player is installed you can activate the plugin as follows: ```javascript import Pillarbox from '@srgssr/pillarbox-web'; -import '@srgssr/pillarbox-playlist'; +import { RepeatMode } from '@srgssr/pillarbox-playlist'; import '@srgssr/pillarbox-playlist/ui'; const player = new Pillarbox('my-player', { plugins: { - pillarboxPlaylist: { autoadvance: true, repeat: true }, + pillarboxPlaylist: { autoadvance: true, repeat: RepeatMode.REPEAT_ALL }, pillarboxPlaylistUI: { insertChildBefore: 'fullscreenToggle' } } }); @@ -76,7 +76,7 @@ The following table outlines the key methods available in the this plugin: | `previous()` | Moves to the previous item in the playlist. | | `shuffle()` | Randomizes the order of the playlist items using the Fisher-Yates shuffle algorithm. | | `select(index)` | Selects and plays the item at the specified index in the playlist. | -| `toggleRepeat(force)` | Toggles the repeat mode of the player to the opposite of its current state, or sets it to the specified boolean value if provided. | +| `toggleRepeat(force)` | Cycles through the repeat mode of the player, or sets it to the specified value if provided. | | `toggleAutoadvance(force)` | Toggles the auto-advance mode of the player to the opposite of its current state, or sets it to the specified boolean value if provided. | #### Options @@ -87,7 +87,7 @@ behavior of the plugin. Here are the available options: | Option | Type | Default | Description | |---------------|---------|---------|---------------------------------------------------------------------------------------------| | `playlist` | Array | `[]` | An array of playlist items to be initially loaded into the player. | -| `repeat` | Boolean | `false` | If true, the playlist will start over automatically after the last item ends. | +| `repeat` | Number | 0 | Set the repeat mode of the playlist: 0 - No Repeat, 1 - Repeat All, 2 - Repeat one. | | `autoadvance` | Boolean | `false` | If enabled, the player will automatically move to the next item after the current one ends. | #### Properties @@ -95,10 +95,10 @@ behavior of the plugin. Here are the available options: After initializing the plugin, you can modify or read these properties to control playlist behavior dynamically: -| Property | Type | Description | -|---------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| `repeat` | Boolean | Enables or disables repeating the playlist once the last item has played. Changes take effect immediately and apply to subsequent operations. | -| `autoadvance` | Boolean | Toggles automatic advancement to the next item when the current item ends. | +| Property | Type | Description | +|---------------|---------|----------------------------------------------------------------------------------------------| +| `repeat` | Number | Changes the repeat mode of the playlist: 0 - No Repeat, 1 - Repeat All, 2 - Repeat one. . | +| `autoadvance` | Boolean | Toggles automatic advancement to the next item when the current item ends. | The following properties are read-only: @@ -108,6 +108,18 @@ The following properties are read-only: | `currentItem` | Object | Retrieves the currently playing item. | | `items` | Array | Retrieves all items in the playlist. Modifications to the returned array will not affect the internal state of the playlist. | +Sure, here is an updated version of your API documentation with a section explaining the `RepeatMode` constant: + +#### Constants + +The following table outlines the key constants available in this plugin: + +| Constant | Description | +|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `RepeatMode.NO_REPEAT` | Disables repeat mode. | +| `RepeatMode.REPEAT_ALL` | Loops the entire playlist. Once the last element of the playlist ends, the next element will be the first one. This mode only works forwards, i.e., when advancing to the next element. | +| `RepeatMode.REPEAT_ONE` | Loops the currently playing item in the playlist. | + ## Contributing For detailed contribution guidelines, refer to the main project’s [README file][main-readme]. Please diff --git a/packages/pillarbox-playlist/index.html b/packages/pillarbox-playlist/index.html index d456cc6..a3ed16b 100644 --- a/packages/pillarbox-playlist/index.html +++ b/packages/pillarbox-playlist/index.html @@ -26,6 +26,7 @@ import { default as pillarbox, SrgSsr} from '@srgssr/pillarbox-web'; import './src/pillarbox-playlist.js'; import './src/pillarbox-playlist-ui.js'; + import { RepeatMode } from './src/pillarbox-playlist.js'; // Handle URL parameters const searchParams = new URLSearchParams(location.search); @@ -40,7 +41,7 @@ autoplay: true, srgOptions: { dataProviderHost: ilHost }, plugins: { - pillarboxPlaylist: { autoadvance: true, repeat: true }, + pillarboxPlaylist: { autoadvance: true, repeat: RepeatMode.REPEAT_ALL }, pillarboxPlaylistUI: { insertChildBefore: 'fullscreenToggle' } } }); diff --git a/packages/pillarbox-playlist/scss/pillarbox-playlist.scss b/packages/pillarbox-playlist/scss/pillarbox-playlist.scss index b109e6e..2d53c71 100644 --- a/packages/pillarbox-playlist/scss/pillarbox-playlist.scss +++ b/packages/pillarbox-playlist/scss/pillarbox-playlist.scss @@ -51,4 +51,22 @@ border-radius: 0.5em; } } + + .pbw-repeat-one { + position: relative; + } + + .pbw-repeat-one::after { + position: absolute; + right: 0.5em; + bottom: 0.5em; + display: flex; + align-items: center; + justify-content: center; + width: 1em; + height: 1em; + font-size: 1em; + border-radius: 50%; + content: "1"; + } } diff --git a/packages/pillarbox-playlist/src/lang/de.json b/packages/pillarbox-playlist/src/lang/de.json index eafdbf7..a649d1d 100644 --- a/packages/pillarbox-playlist/src/lang/de.json +++ b/packages/pillarbox-playlist/src/lang/de.json @@ -1,7 +1,9 @@ { - "Next Item": "Nächstes Element", + "NextItem": "Nächstes element", + "NoRepeat": "Keine Wiederholung", "Playlist": "Wiedergabeliste", - "Previous Item": "Vorheriges Element", - "Repeat": "Wiederholen", + "PreviousItem": "Vorheriges Element", + "RepeatAll": "Alle Wiederholen", + "RepeatOne": "Einzelnes Wiederholen", "Shuffle": "Mischen" } diff --git a/packages/pillarbox-playlist/src/lang/en.json b/packages/pillarbox-playlist/src/lang/en.json index 5d72728..ad1ca88 100644 --- a/packages/pillarbox-playlist/src/lang/en.json +++ b/packages/pillarbox-playlist/src/lang/en.json @@ -1,7 +1,9 @@ { - "Next Item": "Next Item", + "NextItem": "Next Item", + "NoRepeat": "No Repeat", "Playlist": "Playlist", - "Previous Item": "Previous Item", - "Repeat": "Repeat", + "PreviousItem": "Previous Item", + "RepeatAll": "Repeat All", + "RepeatOne": "Repeat One", "Shuffle": "Shuffle" } diff --git a/packages/pillarbox-playlist/src/lang/fr.json b/packages/pillarbox-playlist/src/lang/fr.json index c1b8861..994da0f 100644 --- a/packages/pillarbox-playlist/src/lang/fr.json +++ b/packages/pillarbox-playlist/src/lang/fr.json @@ -1,7 +1,9 @@ { - "Next Item": "Élément suivant", + "NextItem": "Élément suivant", + "NoRepeat": "Pas de Répétition", "Playlist": "Liste de lecture", - "Previous Item": "Élément précédent", - "Repeat": "Répéter", + "PreviousItem": "Élément précédent", + "RepeatAll": "Répéter Tout", + "RepeatOne": "Répéter Un", "Shuffle": "Mélanger" } diff --git a/packages/pillarbox-playlist/src/lang/it.json b/packages/pillarbox-playlist/src/lang/it.json index f903a1a..ff42cdb 100644 --- a/packages/pillarbox-playlist/src/lang/it.json +++ b/packages/pillarbox-playlist/src/lang/it.json @@ -1,7 +1,9 @@ { - "Next Item": "Elemento successivo", + "NextItem": "Elemento successivo", + "NoRepeat": "Nessuna Ripetizione", "Playlist": "Playlist", - "Previous Item": "Elemento precedente", - "Repeat": "Ripeti", + "PreviousItem": "Elemento precedente", + "RepeatAll": "Ripeti Tutto", + "RepeatOne": "Ripeti Uno", "Shuffle": "Mescola" } diff --git a/packages/pillarbox-playlist/src/lang/rm.json b/packages/pillarbox-playlist/src/lang/rm.json index 1c34ac1..ba3c963 100644 --- a/packages/pillarbox-playlist/src/lang/rm.json +++ b/packages/pillarbox-playlist/src/lang/rm.json @@ -1,7 +1,9 @@ { - "Next Item": "Element proxim", + "NextItem": "Element proxim", + "NoRepeat": "Nagins Repeter", "Playlist": "Glista da reprodukziun", - "Previous Item": "Element precedent", - "Repeat": "Repeter", + "PreviousItem": "Element precedent", + "RepeatAll": "Repeter Tut", + "RepeatOne": "Repeter In", "Shuffle": "Maschadar" } diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist-button.js b/packages/pillarbox-playlist/src/pillarbox-playlist-button.js index 7af9008..44f50b8 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist-button.js +++ b/packages/pillarbox-playlist/src/pillarbox-playlist-button.js @@ -1,6 +1,5 @@ import videojs from 'video.js'; import './pillarbox-playlist-modal.js'; -import './lang'; /** * @ignore diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist-modal.js b/packages/pillarbox-playlist/src/pillarbox-playlist-modal.js index 5f946a6..3604cf4 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist-modal.js +++ b/packages/pillarbox-playlist/src/pillarbox-playlist-modal.js @@ -1,5 +1,6 @@ import videojs from 'video.js'; import './pillarbox-playlist-menu-item.js'; +import { RepeatMode } from './pillarbox-playlist.js'; /** * @ignore @@ -128,7 +129,6 @@ class PlaylistMenuDialog extends ModalDialog { itemListEl.addChild(itemEl); }); - this.addChild(itemListEl); } @@ -159,11 +159,22 @@ class PlaylistMenuDialog extends ModalDialog { createPreviousItemButton() { return this.setButtonIcon(new Button(this.player(), { name: 'PreviousItemButton', - controlText: this.localize('Previous Item'), + controlText: this.localize('PreviousItem'), clickHandler: () => this.playlist().previous() }), 'previous-item'); } + repeatModeAsString() { + switch (this.playlist().repeat) { + case RepeatMode.NO_REPEAT: + return this.localize('NoRepeat'); + case RepeatMode.REPEAT_ALL: + return this.localize('RepeatAll'); + case RepeatMode.REPEAT_ONE: + return this.localize('RepeatOne'); + } + } + /** * Create the "Repeat" button. * @@ -172,11 +183,14 @@ class PlaylistMenuDialog extends ModalDialog { createRepeatButton() { const repeatButton = this.setButtonIcon(new Button(this.player(), { name: 'RepeatButton', - controlText: this.localize('Repeat'), + controlText: this.repeatModeAsString(), className: this.playlist().repeat ? 'vjs-selected' : '', clickHandler: () => { this.playlist().toggleRepeat(); - repeatButton.toggleClass('vjs-selected', this.playlist().repeat); + repeatButton.toggleClass('vjs-selected', !this.playlist().isNoRepeatMode()); + repeatButton.toggleClass('pbw-repeat-one', this.playlist().isRepeatOneMode()); + repeatButton.controlText(this.repeatModeAsString()); + repeatButton.setAttribute('aria-pressed', !this.playlist().isNoRepeatMode()); } }), 'repeat'); @@ -204,7 +218,7 @@ class PlaylistMenuDialog extends ModalDialog { createNextItemButton() { return this.setButtonIcon(new Button(this.player(), { name: 'NextItemButton', - controlText: this.localize('Next Item'), + controlText: this.localize('NextItem'), clickHandler: () => this.playlist().next() }), 'next-item'); } @@ -240,10 +254,10 @@ class PlaylistMenuDialog extends ModalDialog { handleLanguagechange() { const controls = this.getChild('PlaylistControls'); - controls.getChild('PreviousItemButton').controlText(this.localize('Previous Item')); - controls.getChild('RepeatButton').controlText(this.localize('Repeat')); + controls.getChild('PreviousItemButton').controlText(this.localize('PreviousItem')); + controls.getChild('RepeatButton').controlText(this.repeatModeAsString()); controls.getChild('ShuffleButton').controlText(this.localize('Shuffle')); - controls.getChild('NextItemButton').controlText(this.localize('Next Item')); + controls.getChild('NextItemButton').controlText(this.localize('NextItem')); } } diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js b/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js index 5951f52..768c39b 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js +++ b/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js @@ -1,5 +1,6 @@ import videojs from 'video.js'; import './pillarbox-playlist-button.js'; +import './lang'; /** * @ignore diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist.js b/packages/pillarbox-playlist/src/pillarbox-playlist.js index 405a671..a793002 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist.js +++ b/packages/pillarbox-playlist/src/pillarbox-playlist.js @@ -7,10 +7,32 @@ import videojs from 'video.js'; const Plugin = videojs.getPlugin('plugin'); const log = videojs.log.createLogger('pillarbox-playlist'); +/** + * Defines the available repeat modes for the playlist. + * + * @enum {number} + */ +export const RepeatMode = { + /** + * Disables repeat mode. + */ + NO_REPEAT: 0, + /** + * Loops the entire playlist. Once the last element of the playlist ends the n + * ext element will be the first one. This mode only works forwards, + * i.e. when advancing to the next element. + */ + REPEAT_ALL: 1, + /** + * Loops the currently playing item in the playlist. + */ + REPEAT_ONE: 2, +}; + /** * Represents a Plugin that allows control over a playlist. */ -class PillarboxPlaylist extends Plugin { +export class PillarboxPlaylist extends Plugin { /** * The items in the playlist. * @@ -26,22 +48,48 @@ class PillarboxPlaylist extends Plugin { */ currentIndex_ = -1; /** - * Whether the repeat is enabled or not. If repeat is enabled once the last - * element of the playlist ends the next element will be the first one. This - * mode only works forwards, i.e. when advancing to the next element. + * The current repeat mode of the player. By default, repeat is disabled. * - * @type boolean + * @type {RepeatMode} */ - repeat = false; + repeat = RepeatMode.NO_REPEAT; /** * Toggles the repeat mode of the player to the opposite of its current state. * - * @param {boolean} [force] Optional. If provided, sets the repeat mode to the specified boolean value (true or false). - * If omitted, the repeat mode will toggle to the opposite of its current state. + * @param {RepeatMode} [force] Optional. + * If provided, sets the repeat mode to the specified state. + * If omitted, the repeat mode will cycle in order through: no repeat, repeat all and repeat one. */ toggleRepeat(force = undefined) { - this.repeat = force ?? !this.repeat; + this.repeat = force ?? (this.repeat + 1) % 3; + } + + /** + * Checks if the repeat mode is set to {@link RepeatMode.REPEAT_ONE}. + * + * @returns {boolean} True if the repeat mode is {@link RepeatMode.REPEAT_ONE}, false otherwise. + */ + isRepeatOneMode() { + return this.repeat === RepeatMode.REPEAT_ONE; + } + + /** + * Checks if the repeat mode is set to {@link RepeatMode.REPEAT_ALL}. + * + * @returns {boolean} True if the repeat mode is {@link RepeatMode.REPEAT_ALL}, false otherwise. + */ + isRepeatAllMode() { + return this.repeat === RepeatMode.REPEAT_ALL; + } + + /** + * Checks if the repeat mode is set to {@link RepeatMode.NO_REPEAT}. + * + * @returns {boolean} True if the repeat mode is {@link RepeatMode.NO_REPEAT}, false otherwise. + */ + isNoRepeatMode() { + return this.repeat === RepeatMode.NO_REPEAT; } /** @@ -84,7 +132,7 @@ class PillarboxPlaylist extends Plugin { }); } this.autoadvance = !!options.autoadvance; - this.repeat = !!options.repeat; + this.repeat = options.repeat ?? RepeatMode.NO_REPEAT; this.player.on('ended', this.onEnded_); } @@ -238,7 +286,7 @@ class PillarboxPlaylist extends Plugin { return; } - if (this.repeat) this.select(0); + if (this.repeat === RepeatMode.REPEAT_ALL) this.select(0); } /** @@ -275,6 +323,12 @@ class PillarboxPlaylist extends Plugin { * will be played, otherwise nothing happens. */ handleEnded() { + if (this.repeat === RepeatMode.REPEAT_ONE) { + this.player.play().then(() => {}); + + return; + } + if (!this.autoadvance) { return; } @@ -323,8 +377,6 @@ class PillarboxPlaylist extends Plugin { videojs.registerPlugin('pillarboxPlaylist', PillarboxPlaylist); -export default PillarboxPlaylist; - /** * Represents a single item in the playlist. * diff --git a/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js index 99a5820..dbfe90d 100644 --- a/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js +++ b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js @@ -1,6 +1,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import pillarbox from '@srgssr/pillarbox-web'; -import PillarboxPlaylist from '../src/pillarbox-playlist.js'; +import { PillarboxPlaylist, RepeatMode } from '../src/pillarbox-playlist.js'; import '../src/pillarbox-playlist-button.js'; const playlist = [ @@ -173,27 +173,6 @@ describe('PillarboxPlaylist', () => { expect(srcSpy).toHaveBeenLastCalledWith(playlist[3].sources); expect(posterSpy).toHaveBeenLastCalledWith(playlist[3].poster); }); - - it('should play the first element if repeat is true when next is called and the current index is the last of the playlist', () => { - // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); - - // When - pillarboxPlaylist.toggleRepeat(true); - pillarboxPlaylist.load(playlist); - pillarboxPlaylist.select(3); - pillarboxPlaylist.next(); - - // Then - expect(pillarboxPlaylist.hasPrevious()).toBeFalsy(); - expect(pillarboxPlaylist.hasNext()).toBeTruthy(); - expect(pillarboxPlaylist.items.length).toBe(4); - expect(pillarboxPlaylist.currentIndex).toBe(0); - expect(pillarboxPlaylist.currentItem).toBe(playlist[0]); - expect(srcSpy).toHaveBeenLastCalledWith(playlist[0].sources); - expect(posterSpy).toHaveBeenLastCalledWith(playlist[0].poster); - }); }); describe('previous', () => { @@ -260,6 +239,48 @@ describe('PillarboxPlaylist', () => { }); }); + describe('repeat', () => { + it('should play the same element if repeat mode is "repeat one"', () => { + // Given + const playSpy = vi.spyOn(player, 'play') + .mockImplementation(() => Promise.resolve()); + + // When + pillarboxPlaylist.toggleRepeat(RepeatMode.REPEAT_ONE); + pillarboxPlaylist.load(playlist); + pillarboxPlaylist.handleEnded(); + + // Then + expect(pillarboxPlaylist.hasPrevious()).toBeFalsy(); + expect(pillarboxPlaylist.hasNext()).toBeTruthy(); + expect(pillarboxPlaylist.items.length).toBe(4); + expect(pillarboxPlaylist.currentIndex).toBe(0); + expect(pillarboxPlaylist.currentItem).toBe(playlist[0]); + expect(playSpy).toHaveBeenCalled(); + }); + + it('should play the first element if repeat is true when next is called and the current index is the last of the playlist', () => { + // Given + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + + // When + pillarboxPlaylist.toggleRepeat(RepeatMode.REPEAT_ALL); + pillarboxPlaylist.load(playlist); + pillarboxPlaylist.select(3); + pillarboxPlaylist.next(); + + // Then + expect(pillarboxPlaylist.hasPrevious()).toBeFalsy(); + expect(pillarboxPlaylist.hasNext()).toBeTruthy(); + expect(pillarboxPlaylist.items.length).toBe(4); + expect(pillarboxPlaylist.currentIndex).toBe(0); + expect(pillarboxPlaylist.currentItem).toBe(playlist[0]); + expect(srcSpy).toHaveBeenLastCalledWith(playlist[0].sources); + expect(posterSpy).toHaveBeenLastCalledWith(playlist[0].poster); + }); + }); + describe('shuffle', () => { it('should randomize the order of playlist items', () => { // Given @@ -466,11 +487,25 @@ describe('PillarboxPlaylist', () => { }); it('should toggle repeat mode through the dialog controls', ()=> { - // When + pillarboxPlaylist.toggleRepeat(RepeatMode.NO_REPEAT); + controls.getChild('RepeatButton').handleClick(); + expect(pillarboxPlaylist.repeat).toBe(RepeatMode.REPEAT_ALL); + expect(pillarboxPlaylist.isNoRepeatMode()).toBeFalsy(); + expect(pillarboxPlaylist.isRepeatAllMode()).toBeTruthy(); + expect(pillarboxPlaylist.isRepeatOneMode()).toBeFalsy(); - // Then - expect(pillarboxPlaylist.repeat).toBeTruthy(); + controls.getChild('RepeatButton').handleClick(); + expect(pillarboxPlaylist.repeat).toBe(RepeatMode.REPEAT_ONE); + expect(pillarboxPlaylist.isNoRepeatMode()).toBeFalsy(); + expect(pillarboxPlaylist.isRepeatAllMode()).toBeFalsy(); + expect(pillarboxPlaylist.isRepeatOneMode()).toBeTruthy(); + + controls.getChild('RepeatButton').handleClick(); + expect(pillarboxPlaylist.repeat).toBe(RepeatMode.NO_REPEAT); + expect(pillarboxPlaylist.isNoRepeatMode()).toBeTruthy(); + expect(pillarboxPlaylist.isRepeatAllMode()).toBeFalsy(); + expect(pillarboxPlaylist.isRepeatOneMode()).toBeFalsy(); }); it('should go the next item through the dialog controls', ()=> {