@outbook/webcomponents-player 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -24
- package/_lib/get-background-color.js +40 -0
- package/_lib/order-by-luminance.js +30 -6
- package/_lib/playlist/index.js +27 -66
- package/_lib/rgb-to-hex.js +7 -10
- package/_lib/setup-media-session.js +6 -2
- package/_lib/to-boolean.js +8 -2
- package/_lib/track-info/index.js +22 -11
- package/_style/player.style.js +0 -90
- package/package.json +3 -2
- package/player.js +23 -23
- package/_lib/background.js +0 -28
- package/_lib/get-cover.js +0 -22
- package/_lib/playlist/modules/item-duration.js +0 -16
- package/_lib/playlist/modules/show-cover.js +0 -12
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @outbook/webcomponents-player
|
|
2
2
|
|
|
3
|
-
This package provides a web component for an audio player with playlist functionality.
|
|
3
|
+
This package provides a web component for an audio player with playlist functionality. It includes playback controls, a timeline, track information with artwork, prominent-color backgrounds, and OS media controls through the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) when supported.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -22,20 +22,14 @@ const myPlaylist = [
|
|
|
22
22
|
title: 'Song Title 1',
|
|
23
23
|
artist: 'Artist Name 1',
|
|
24
24
|
album: 'Album Name 1',
|
|
25
|
-
|
|
26
|
-
name: 'cover1.jpg' // or a URL
|
|
27
|
-
// Other cover properties if available
|
|
28
|
-
}
|
|
25
|
+
artwork: '/artwork/[size]/[density]/cover1.jpg'
|
|
29
26
|
},
|
|
30
27
|
{
|
|
31
28
|
file: 'path/to/your/audio2.mp3',
|
|
32
29
|
title: 'Song Title 2',
|
|
33
30
|
artist: 'Artist Name 2',
|
|
34
31
|
album: 'Album Name 2',
|
|
35
|
-
|
|
36
|
-
name: 'cover2.png' // or a URL
|
|
37
|
-
// Other cover properties if available
|
|
38
|
-
}
|
|
32
|
+
artwork: '/artwork/[size]/[density]/cover2.jpg'
|
|
39
33
|
}
|
|
40
34
|
];
|
|
41
35
|
|
|
@@ -74,18 +68,14 @@ import '@outbook/webcomponents-player';
|
|
|
74
68
|
"title": "Song Title 1",
|
|
75
69
|
"artist": "Artist Name 1",
|
|
76
70
|
"album": "Album Name 1",
|
|
77
|
-
"
|
|
78
|
-
"name": "cover1.jpg"
|
|
79
|
-
}
|
|
71
|
+
"artwork": "/artwork/[size]/[density]/cover1.jpg"
|
|
80
72
|
},
|
|
81
73
|
{
|
|
82
74
|
"file": "path/to/your/audio2.mp3",
|
|
83
75
|
"title": "Song Title 2",
|
|
84
76
|
"artist": "Artist Name 2",
|
|
85
77
|
"album": "Album Name 2",
|
|
86
|
-
"
|
|
87
|
-
"name": "cover2.png"
|
|
88
|
-
}
|
|
78
|
+
"artwork": "/artwork/[size]/[density]/cover2.jpg"
|
|
89
79
|
}
|
|
90
80
|
]'
|
|
91
81
|
></outbook-player>
|
|
@@ -93,21 +83,36 @@ import '@outbook/webcomponents-player';
|
|
|
93
83
|
|
|
94
84
|
**Note:** When using the `playlist` property directly in HTML, you must pass a JSON string. Ensure the JSON is valid and properly escaped if necessary.
|
|
95
85
|
|
|
86
|
+
### Track Object
|
|
87
|
+
|
|
88
|
+
Each item in `playlist` describes one track:
|
|
89
|
+
|
|
90
|
+
| Field | Type | Required | Description |
|
|
91
|
+
| --------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `file` | `String` | Yes | URL of the audio file. Used as the `src` of the underlying `<audio>` element. |
|
|
93
|
+
| `title` | `String` | No | Track title, shown in the playlist, track info, and OS media controls. |
|
|
94
|
+
| `artist` | `String` | No | Artist name. |
|
|
95
|
+
| `album` | `String` | No | Album name. |
|
|
96
|
+
| `artwork` | `String` | No | Artwork URL template. `[size]` and `[density]` placeholders are replaced with the requested size (e.g. `384`) and density (e.g. `1x`). Example: `/artwork/[size]/[density]/cover.jpg`. |
|
|
97
|
+
| `cover` | `Object` | No | Optional. Only `cover.extension` (e.g. `'jpg'`, `'png'`) is read, to set the artwork MIME type for OS media controls. |
|
|
98
|
+
|
|
99
|
+
Tracks without `artwork` are shown without cover art, and the background falls back to the default color.
|
|
100
|
+
|
|
96
101
|
## Component: `outbook-player`
|
|
97
102
|
|
|
98
103
|
This is the underlying web component. It can be used directly in HTML or in any framework.
|
|
99
104
|
|
|
100
105
|
### Properties
|
|
101
106
|
|
|
102
|
-
| Attribute | Property | Type | Default | Description
|
|
103
|
-
| --------------------- | ------------------- | --------- | ----------- |
|
|
104
|
-
| `lang` | `lang` | `String` | `'en'` | The language to use for localized strings.
|
|
105
|
-
| `playlist` | `playlist` | `Array` | `[]` | An array of track objects, each with `file`, `title`, `artist`, `album`, and `
|
|
106
|
-
| `extraClasses` | `extraClasses` | `String` | `undefined` | Classes to add to the host element.
|
|
107
|
-
| `show-file-extension` | `showFileExtension` | `Boolean` | `true` | Whether to display file extensions in the playlist.
|
|
108
|
-
| `show-artwork` | `showArtwork` | `Boolean` | `true` | Whether to display artwork in the playlist.
|
|
109
|
-
| `show-track-number` | `showTrackNumber` | `Boolean` | `true` | Whether to display track numbers in the playlist.
|
|
110
|
-
| `playlist-align` | `playlistAlign` | `String` | `'right'` | Aligns the playlist to 'left' or 'right' of the track info.
|
|
107
|
+
| Attribute | Property | Type | Default | Description |
|
|
108
|
+
| --------------------- | ------------------- | --------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| `lang` | `lang` | `String` | `'en'` | The language to use for localized strings. |
|
|
110
|
+
| `playlist` | `playlist` | `Array` | `[]` | An array of track objects, each with `file`, `title`, `artist`, `album`, and `artwork` properties (see [Track Object](#track-object)). |
|
|
111
|
+
| `extraClasses` | `extraClasses` | `String` | `undefined` | Classes to add to the host element. |
|
|
112
|
+
| `show-file-extension` | `showFileExtension` | `Boolean` | `true` | Whether to display file extensions in the playlist. |
|
|
113
|
+
| `show-artwork` | `showArtwork` | `Boolean` | `true` | Whether to display artwork in the playlist. |
|
|
114
|
+
| `show-track-number` | `showTrackNumber` | `Boolean` | `true` | Whether to display track numbers in the playlist. |
|
|
115
|
+
| `playlist-align` | `playlistAlign` | `String` | `'right'` | Aligns the playlist to 'left' or 'right' of the track info. |
|
|
111
116
|
|
|
112
117
|
### Styling
|
|
113
118
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// get-background-color.js
|
|
2
|
+
|
|
3
|
+
import { prominent } from 'color.js';
|
|
4
|
+
import { rgbToHex } from './rgb-to-hex.js';
|
|
5
|
+
import { orderByLuminance } from './order-by-luminance.js';
|
|
6
|
+
|
|
7
|
+
export const emptyBackground = '';
|
|
8
|
+
|
|
9
|
+
const PROMINENT_OPTIONS = {
|
|
10
|
+
amount: 5,
|
|
11
|
+
format: 'rgb',
|
|
12
|
+
group: 40,
|
|
13
|
+
sample: 10
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts prominent colors from a cover image and returns a CSS block
|
|
18
|
+
* of custom properties ordered by luminance.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} [coverUrl]
|
|
21
|
+
* @returns {Promise<string>} CSS string like `--player--main-color-0: #aabbcc;...`
|
|
22
|
+
*/
|
|
23
|
+
export async function getBackgroundColor(coverUrl) {
|
|
24
|
+
if (!coverUrl) return emptyBackground;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const colors = await prominent(coverUrl, PROMINENT_OPTIONS);
|
|
28
|
+
|
|
29
|
+
const colorPropsRgb = colors.map(([r, g, b]) => ({ r, g, b }));
|
|
30
|
+
const orderedColorsRgb = orderByLuminance(colorPropsRgb);
|
|
31
|
+
|
|
32
|
+
return orderedColorsRgb
|
|
33
|
+
.map((item, index) => `--player--main-color-${index}: ${rgbToHex(item)};`)
|
|
34
|
+
.join('\n');
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// color.js throws on unreadable images / CORS / network failures
|
|
37
|
+
console.warn('getBackgroundColor failed:', err);
|
|
38
|
+
return emptyBackground;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,9 +1,33 @@
|
|
|
1
|
+
// order-by-luminance.js
|
|
1
2
|
// https://en.wikipedia.org/wiki/Luma_(video)
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calculate relative luminance for an RGB object using Rec. 709 coefficients.
|
|
6
|
+
* @param {{r: number, g: number, b: number}} p
|
|
7
|
+
* @returns {number}
|
|
8
|
+
*/
|
|
9
|
+
export const calculateLuminance = ({ r, g, b }) =>
|
|
10
|
+
0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sort an array of RGB objects by luminance, brightest first.
|
|
14
|
+
* Does not mutate the input array.
|
|
15
|
+
*
|
|
16
|
+
* @param {Array<{r: number, g: number, b: number}>} rgbValues
|
|
17
|
+
* @returns {Array<{r: number, g: number, b: number}>} new sorted array
|
|
18
|
+
*/
|
|
5
19
|
export const orderByLuminance = rgbValues => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
20
|
+
if (!Array.isArray(rgbValues)) {
|
|
21
|
+
throw new TypeError('orderByLuminance expects an array');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return rgbValues
|
|
25
|
+
.map((rgb, index) => ({ rgb, index, luminance: calculateLuminance(rgb) }))
|
|
26
|
+
.sort((a, b) => {
|
|
27
|
+
// Descending luminance
|
|
28
|
+
if (b.luminance !== a.luminance) return b.luminance - a.luminance;
|
|
29
|
+
// Stable tie-break on original position
|
|
30
|
+
return a.index - b.index;
|
|
31
|
+
})
|
|
32
|
+
.map(({ rgb }) => rgb);
|
|
9
33
|
};
|
package/_lib/playlist/index.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { virtual } from 'haunted';
|
|
2
|
-
import { html
|
|
3
|
-
import { isKeyEnterOrKeySpace, isEventClick } from '@outbook/a11y';
|
|
2
|
+
import { html } from 'lit';
|
|
4
3
|
import { classMap } from 'lit/directives/class-map.js';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import { play_arrow, pause } from '@outbook/icons';
|
|
8
|
-
import { BadgeFileExtension } from '@outbook/webcomponents-badge-file-extension';
|
|
9
|
-
import { ItemDuration } from './modules/item-duration.js';
|
|
10
|
-
import { ShowCover } from './modules/show-cover.js';
|
|
4
|
+
|
|
5
|
+
import { PlayerPlaylistItem } from '@outbook/webcomponents-player-playlist-item';
|
|
11
6
|
|
|
12
7
|
export const Playlist = virtual(
|
|
13
8
|
({
|
|
@@ -20,11 +15,9 @@ export const Playlist = virtual(
|
|
|
20
15
|
}) => {
|
|
21
16
|
const { showArtwork, showTrackNumber, showFileExtension } = list;
|
|
22
17
|
function handleButton(index) {
|
|
23
|
-
return (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
}
|
|
18
|
+
return () => {
|
|
19
|
+
handleAudio.setTrack({ index, fromControls: false });
|
|
20
|
+
};
|
|
28
21
|
}
|
|
29
22
|
return html`
|
|
30
23
|
<div class="player-playlist" id="${listId}">
|
|
@@ -37,59 +30,27 @@ export const Playlist = virtual(
|
|
|
37
30
|
});
|
|
38
31
|
return html`
|
|
39
32
|
<li class="${itemClasses}" data-playlist-item="${index}">
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
: nothing}
|
|
62
|
-
${showTrackNumber
|
|
63
|
-
? html`
|
|
64
|
-
<div
|
|
65
|
-
class="player-playlist__item-block track-text__item size-small"
|
|
66
|
-
>
|
|
67
|
-
${item.trackNumber || index + 1}
|
|
68
|
-
</div>
|
|
69
|
-
`
|
|
70
|
-
: nothing}
|
|
71
|
-
<div class="player-playlist__item-block">
|
|
72
|
-
${TrackText({
|
|
73
|
-
title: item.title,
|
|
74
|
-
artist: item.artist,
|
|
75
|
-
album: item.album
|
|
76
|
-
})}
|
|
77
|
-
</div>
|
|
78
|
-
${item.duration || showFileExtension
|
|
79
|
-
? html`
|
|
80
|
-
<div class="player-playlist__item-group is-last-right">
|
|
81
|
-
${ItemDuration({ duration: item.duration })}
|
|
82
|
-
${showFileExtension
|
|
83
|
-
? html`
|
|
84
|
-
<div class="player-playlist__item-block">
|
|
85
|
-
${BadgeFileExtension({ text: item.format })}
|
|
86
|
-
</div>
|
|
87
|
-
`
|
|
88
|
-
: nothing}
|
|
89
|
-
</div>
|
|
90
|
-
`
|
|
91
|
-
: nothing}
|
|
92
|
-
</div>
|
|
33
|
+
${PlayerPlaylistItem({
|
|
34
|
+
...item,
|
|
35
|
+
title: item.title,
|
|
36
|
+
artist: item.artist,
|
|
37
|
+
album: item.album,
|
|
38
|
+
duration: item.duration,
|
|
39
|
+
format: item.format,
|
|
40
|
+
artwork: item.artwork,
|
|
41
|
+
trackNumber: item.trackNumber,
|
|
42
|
+
trackIndex: index,
|
|
43
|
+
isCurrent: isPlayingItem,
|
|
44
|
+
playState: isPlayingItem
|
|
45
|
+
? isPlaying
|
|
46
|
+
? 'playing'
|
|
47
|
+
: 'paused'
|
|
48
|
+
: 'stopped',
|
|
49
|
+
showArtwork,
|
|
50
|
+
showTrackNumber,
|
|
51
|
+
showFileExtension,
|
|
52
|
+
eventFn: handleButton(index)
|
|
53
|
+
})}
|
|
93
54
|
</li>
|
|
94
55
|
`;
|
|
95
56
|
})}
|
package/_lib/rgb-to-hex.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// rgb-to-hex.js
|
|
2
|
+
|
|
3
|
+
export const componentToHex = channel => {
|
|
4
|
+
const clamped = Math.max(0, Math.min(255, Math.round(channel)));
|
|
5
|
+
return clamped.toString(16).padStart(2, '0');
|
|
4
6
|
};
|
|
5
7
|
|
|
6
|
-
export const rgbToHex =
|
|
7
|
-
return (
|
|
8
|
-
'#' +
|
|
9
|
-
componentToHex(item.r) +
|
|
10
|
-
componentToHex(item.g) +
|
|
11
|
-
componentToHex(item.b)
|
|
12
|
-
).toUpperCase();
|
|
8
|
+
export const rgbToHex = ({ r, g, b }) => {
|
|
9
|
+
return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`.toUpperCase();
|
|
13
10
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { playerGetArtwork } from '@outbook/player-get-artwork';
|
|
2
2
|
import { isValidTrackIndex } from './is-valid-track-index.js';
|
|
3
3
|
|
|
4
4
|
const MEDIA_SESSION_ACTIONS = [
|
|
@@ -44,7 +44,11 @@ export function setupMediaSession({ owner, index, playlist, handleAudio }) {
|
|
|
44
44
|
|
|
45
45
|
mediaSessionOwner = owner;
|
|
46
46
|
const metadata = playlist[index];
|
|
47
|
-
const coverUrl =
|
|
47
|
+
const coverUrl = playerGetArtwork({
|
|
48
|
+
artwork: metadata.artwork,
|
|
49
|
+
size: 512,
|
|
50
|
+
density: '1x'
|
|
51
|
+
});
|
|
48
52
|
const extension = metadata.cover?.extension?.toLowerCase().replace(/^\./, '');
|
|
49
53
|
const subtype = { jpg: 'jpeg', svg: 'svg+xml' }[extension] || extension;
|
|
50
54
|
const type = subtype ? `image/${subtype}` : undefined;
|
package/_lib/to-boolean.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
// to-boolean.js
|
|
2
|
+
|
|
1
3
|
export function toBoolean(value) {
|
|
2
|
-
if (typeof value === 'boolean')
|
|
3
|
-
|
|
4
|
+
if (typeof value === 'boolean') {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
if (typeof value === 'string') {
|
|
8
|
+
return value !== 'false';
|
|
9
|
+
}
|
|
4
10
|
return Boolean(value);
|
|
5
11
|
}
|
package/_lib/track-info/index.js
CHANGED
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
import { html } from 'lit';
|
|
2
2
|
import { TrackText } from '../track-text/index.js';
|
|
3
|
-
import { getCover } from '../get-cover.js';
|
|
4
3
|
import { PlayerArtwork } from '@outbook/webcomponents-player-artwork';
|
|
5
4
|
|
|
6
5
|
export function TrackInfo({ track = {}, indexSelected, literals = {} }) {
|
|
7
6
|
const hasTrackInfo = Number.isInteger(indexSelected);
|
|
8
|
-
const
|
|
7
|
+
const artwork = hasTrackInfo && track.artwork ? track.artwork : undefined;
|
|
9
8
|
return html`
|
|
10
9
|
<div class="track-info">
|
|
11
10
|
<div class="track-info__inner">
|
|
12
11
|
${PlayerArtwork({
|
|
13
|
-
|
|
12
|
+
src: artwork,
|
|
13
|
+
sources: [
|
|
14
|
+
{
|
|
15
|
+
media: '(min-width: 800px)',
|
|
16
|
+
sizes: [384]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
media: '(min-width: 0px)',
|
|
20
|
+
sizes: [96]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
14
23
|
extraClasses: 'track-info__image'
|
|
15
24
|
})}
|
|
16
25
|
<div class="track-info__data">
|
|
17
|
-
${
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
${
|
|
27
|
+
hasTrackInfo
|
|
28
|
+
? TrackText({
|
|
29
|
+
title: track.title,
|
|
30
|
+
artist: track.artist,
|
|
31
|
+
album: track.album,
|
|
32
|
+
hasTrackInfo
|
|
33
|
+
})
|
|
34
|
+
: TrackText({ title: literals.noPlaying, hasTrackInfo })
|
|
35
|
+
}
|
|
25
36
|
</div>
|
|
26
37
|
</div>
|
|
27
38
|
</div>
|
package/_style/player.style.js
CHANGED
|
@@ -283,43 +283,6 @@ button[disabled],
|
|
|
283
283
|
height: var(--_controls-size);
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
.player-playlist {
|
|
287
|
-
--_playlist-font-weight: normal;
|
|
288
|
-
--_playlist-font-size: 0.75rem;
|
|
289
|
-
--_playlist-font-size-small: 0.625rem;
|
|
290
|
-
--_playlist-item-background-color: transparent;
|
|
291
|
-
--_playlist-item-image-size: 3rem;
|
|
292
|
-
}
|
|
293
|
-
.player-playlist .is-playing {
|
|
294
|
-
--_playlist-font-weight: 600;
|
|
295
|
-
--_playlist-item-background-color: var(--_accent-200);
|
|
296
|
-
}
|
|
297
|
-
.ambient-dark .player-playlist .is-playing {
|
|
298
|
-
--_playlist-item-background-color: var(--_accent-700);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
@container player (width >= 768px) {
|
|
302
|
-
.player-playlist {
|
|
303
|
-
--_playlist-font-size: 0.875rem;
|
|
304
|
-
--_playlist-font-size-small: 0.75rem;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
@container player (width >= 1024px) {
|
|
308
|
-
.player-playlist {
|
|
309
|
-
--_playlist-font-size: 1rem;
|
|
310
|
-
--_playlist-font-size-small: 0.875rem;
|
|
311
|
-
--_playlist-item-image-size: 4rem;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
.player-playlist .track-text__item {
|
|
315
|
-
font-weight: var(--_playlist-font-weight);
|
|
316
|
-
font-size: var(--_playlist-font-size);
|
|
317
|
-
line-height: 150%;
|
|
318
|
-
}
|
|
319
|
-
.player-playlist .track-text__item.size-small {
|
|
320
|
-
font-size: var(--_playlist-font-size-small);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
286
|
.player-playlist__items {
|
|
324
287
|
padding: 0;
|
|
325
288
|
position: relative;
|
|
@@ -355,59 +318,6 @@ button[disabled],
|
|
|
355
318
|
border-bottom: 0;
|
|
356
319
|
}
|
|
357
320
|
|
|
358
|
-
.player-playlist__item-button {
|
|
359
|
-
display: flex;
|
|
360
|
-
width: 100%;
|
|
361
|
-
border: 0;
|
|
362
|
-
padding: 0.5rem;
|
|
363
|
-
position: relative;
|
|
364
|
-
align-items: center;
|
|
365
|
-
color: inherit;
|
|
366
|
-
background: transparent;
|
|
367
|
-
font: inherit;
|
|
368
|
-
text-align: inherit;
|
|
369
|
-
cursor: pointer;
|
|
370
|
-
}
|
|
371
|
-
.player-playlist__item-button:focus-visible {
|
|
372
|
-
outline: var(--outbook-outline--color, light-dark(oklch(48.8% 0.243 264.376deg), oklch(88.2% 0.059 254.128deg))) solid 2px;
|
|
373
|
-
outline-offset: -4px;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
.player-playlist__item-group,
|
|
377
|
-
.player-playlist__item-block {
|
|
378
|
-
display: flex;
|
|
379
|
-
align-items: center;
|
|
380
|
-
margin-left: 1rem;
|
|
381
|
-
height: var(--_playlist-item-image-size);
|
|
382
|
-
}
|
|
383
|
-
.player-playlist__item-group:first-child,
|
|
384
|
-
.player-playlist__item-block:first-child {
|
|
385
|
-
margin-left: 0;
|
|
386
|
-
}
|
|
387
|
-
.player-playlist__item-group.is-last-right,
|
|
388
|
-
.player-playlist__item-block.is-last-right {
|
|
389
|
-
margin-left: auto;
|
|
390
|
-
padding-left: 0.5rem;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
.player-playlist__item-image {
|
|
394
|
-
width: var(--_playlist-item-image-size);
|
|
395
|
-
height: var(--_playlist-item-image-size);
|
|
396
|
-
flex-shrink: 0;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
.player-playlist__item-playing-icon {
|
|
400
|
-
width: 1rem;
|
|
401
|
-
height: 1rem;
|
|
402
|
-
display: flex;
|
|
403
|
-
align-items: center;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
.player-playlist__item-playing-icon-inner {
|
|
407
|
-
width: 100%;
|
|
408
|
-
aspect-ratio: 1/1;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
321
|
.track-info {
|
|
412
322
|
--track-info--flex-direction: row;
|
|
413
323
|
--track-info--image-width: 4rem;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outbook/webcomponents-player",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"main": "player.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
"@outbook/webcomponents-scrollable": ">=1.1.0",
|
|
36
36
|
"@outbook/webcomponents-type-icon": ">=1.1.5",
|
|
37
37
|
"@outbook/webcomponents-badge-file-extension": ">=1.3.0",
|
|
38
|
-
"@outbook/webcomponents-player-artwork": ">=
|
|
38
|
+
"@outbook/webcomponents-player-artwork": ">=2.0.1",
|
|
39
39
|
"@outbook/webcomponents-player-timeline": ">=1.0.8",
|
|
40
|
+
"@outbook/webcomponents-player-playlist-item": ">=1.0.1",
|
|
40
41
|
"@outbook/icons": ">=1.3.2",
|
|
41
42
|
"@outbook/webcomponents-player-audio-decoy": ">=1.0.0"
|
|
42
43
|
},
|
package/player.js
CHANGED
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
syncMediaSession
|
|
10
10
|
} from './_lib/setup-media-session.js';
|
|
11
11
|
import { isValidTrackIndex } from './_lib/is-valid-track-index.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
emptyBackground,
|
|
14
|
+
getBackgroundColor
|
|
15
|
+
} from './_lib/get-background-color.js';
|
|
14
16
|
import { classMap } from 'lit/directives/class-map.js';
|
|
15
17
|
import { Scrollable } from '@outbook/webcomponents-scrollable';
|
|
16
18
|
import { Playlist as ComponentPlaylist } from './_lib/playlist/index.js';
|
|
@@ -218,9 +220,7 @@ function ComponentPlayer(element) {
|
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
selectedTrackRef.current = selectedTrack;
|
|
221
|
-
setCoverUrl(
|
|
222
|
-
selectedTrack.cover?.name ? getCover(selectedTrack.cover) : null
|
|
223
|
-
);
|
|
223
|
+
setCoverUrl(selectedTrack.artwork || null);
|
|
224
224
|
}, [indexSelected, playlist, playlistMediaSessionSignature]);
|
|
225
225
|
|
|
226
226
|
useEffect(() => {
|
|
@@ -369,9 +369,7 @@ function ComponentPlayer(element) {
|
|
|
369
369
|
handleScroll({ index });
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
setCoverUrl(
|
|
373
|
-
playlist[index].cover?.name ? getCover(playlist[index].cover) : null
|
|
374
|
-
);
|
|
372
|
+
setCoverUrl(playlist[index].artwork || null);
|
|
375
373
|
requestInitialPlayback();
|
|
376
374
|
return true;
|
|
377
375
|
},
|
|
@@ -468,21 +466,23 @@ function ComponentPlayer(element) {
|
|
|
468
466
|
</div>
|
|
469
467
|
</div>
|
|
470
468
|
${isPlaying ? html`${PlayerAudioDecoy({ mode: 'auto' })}` : nothing}
|
|
471
|
-
${
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
469
|
+
${
|
|
470
|
+
Number.isInteger(indexSelected)
|
|
471
|
+
? html`
|
|
472
|
+
<audio
|
|
473
|
+
src="${playlist[indexSelected]?.file}"
|
|
474
|
+
id="${mediaId}"
|
|
475
|
+
@ended="${handleAudioEnd}"
|
|
476
|
+
@timeupdate="${handleTimeUpdate}"
|
|
477
|
+
@loadedmetadata="${handleDurationChange}"
|
|
478
|
+
@durationchange="${handleDurationChange}"
|
|
479
|
+
@play="${handleAudioNativePlay}"
|
|
480
|
+
@pause="${handleAudioNativePause}"
|
|
481
|
+
@error="${handleAudioNativeError}"
|
|
482
|
+
></audio>
|
|
483
|
+
`
|
|
484
|
+
: nothing
|
|
485
|
+
}
|
|
486
486
|
`
|
|
487
487
|
: html`Loading...`;
|
|
488
488
|
}
|
package/_lib/background.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { prominent } from 'color.js';
|
|
2
|
-
import { rgbToHex } from './rgb-to-hex.js';
|
|
3
|
-
import { orderByLuminance } from './order-by-luminance.js';
|
|
4
|
-
|
|
5
|
-
export const emptyBackground = '';
|
|
6
|
-
|
|
7
|
-
export function getBackgroundColor(coverUrl) {
|
|
8
|
-
return prominent(coverUrl, {
|
|
9
|
-
amount: 5,
|
|
10
|
-
format: 'rgb',
|
|
11
|
-
group: 40,
|
|
12
|
-
sample: 10
|
|
13
|
-
}).then(colors => {
|
|
14
|
-
const colorPropsRgb = colors.map(item => ({
|
|
15
|
-
r: item[0],
|
|
16
|
-
g: item[1],
|
|
17
|
-
b: item[2]
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
const orderedColorsRgb = orderByLuminance(colorPropsRgb);
|
|
21
|
-
|
|
22
|
-
return orderedColorsRgb
|
|
23
|
-
.map((item, index) => {
|
|
24
|
-
return `--player--main-color-${index}: ${rgbToHex(item)};`;
|
|
25
|
-
})
|
|
26
|
-
.join(''); // Return the string!
|
|
27
|
-
});
|
|
28
|
-
}
|
package/_lib/get-cover.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export function getCover(data) {
|
|
2
|
-
if (!data || typeof data !== 'object') {
|
|
3
|
-
return undefined;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const { path = '', extension, name } = data;
|
|
7
|
-
if (typeof name !== 'string' || !name.trim()) {
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const normalizedName = name.trim();
|
|
12
|
-
const fileName =
|
|
13
|
-
typeof extension === 'string' && extension.trim()
|
|
14
|
-
? `${normalizedName}.${extension.trim().replace(/^\./, '')}`
|
|
15
|
-
: normalizedName;
|
|
16
|
-
const normalizedPath =
|
|
17
|
-
typeof path === 'string' && path !== '/' ? path.replace(/\/+$/, '') : path;
|
|
18
|
-
|
|
19
|
-
return normalizedPath
|
|
20
|
-
? `${normalizedPath}${normalizedPath === '/' ? '' : '/'}${fileName}`
|
|
21
|
-
: fileName;
|
|
22
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { html } from 'lit';
|
|
2
|
-
import {
|
|
3
|
-
readable as formatTime,
|
|
4
|
-
iso as formatTimeDuration
|
|
5
|
-
} from '@outbook/format-time';
|
|
6
|
-
|
|
7
|
-
export function ItemDuration({ duration }) {
|
|
8
|
-
return html`
|
|
9
|
-
<time
|
|
10
|
-
class="player-playlist__item-block track-text__item size-small"
|
|
11
|
-
datetime="${formatTimeDuration(duration)}"
|
|
12
|
-
>
|
|
13
|
-
${formatTime(duration)}
|
|
14
|
-
</time>
|
|
15
|
-
`;
|
|
16
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { html } from 'lit';
|
|
2
|
-
import { getCover } from '../../get-cover.js';
|
|
3
|
-
import { PlayerArtwork } from '@outbook/webcomponents-player-artwork';
|
|
4
|
-
|
|
5
|
-
export const ShowCover = ({ cover }) => {
|
|
6
|
-
return html`
|
|
7
|
-
${PlayerArtwork({
|
|
8
|
-
src1x: cover ? getCover(cover) : undefined,
|
|
9
|
-
extraClasses: 'player-playlist__item-block player-playlist__item-image'
|
|
10
|
-
})}
|
|
11
|
-
`;
|
|
12
|
-
};
|