vidply 1.0.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.
@@ -0,0 +1,437 @@
1
+ /**
2
+ * VidPly Playlist Manager
3
+ * Manages playlists for audio and video content
4
+ */
5
+
6
+ import { DOMUtils } from '../utils/DOMUtils.js';
7
+ import { createIconElement } from '../icons/Icons.js';
8
+
9
+ export class PlaylistManager {
10
+ constructor(player, options = {}) {
11
+ this.player = player;
12
+ this.tracks = [];
13
+ this.currentIndex = -1;
14
+
15
+ // Options
16
+ this.options = {
17
+ autoAdvance: options.autoAdvance !== false, // Default true
18
+ loop: options.loop || false,
19
+ showPanel: options.showPanel !== false, // Default true
20
+ ...options
21
+ };
22
+
23
+ // UI elements
24
+ this.container = null;
25
+ this.playlistPanel = null;
26
+ this.trackInfoElement = null;
27
+
28
+ // Bind methods
29
+ this.handleTrackEnd = this.handleTrackEnd.bind(this);
30
+ this.handleTrackError = this.handleTrackError.bind(this);
31
+
32
+ // Initialize
33
+ this.init();
34
+ }
35
+
36
+ init() {
37
+ // Listen for track end
38
+ this.player.on('ended', this.handleTrackEnd);
39
+ this.player.on('error', this.handleTrackError);
40
+
41
+ // Create UI if needed
42
+ if (this.options.showPanel) {
43
+ this.createUI();
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Load a playlist
49
+ * @param {Array} tracks - Array of track objects
50
+ */
51
+ loadPlaylist(tracks) {
52
+ this.tracks = tracks;
53
+ this.currentIndex = -1;
54
+
55
+ // Add playlist class to container
56
+ if (this.container) {
57
+ this.container.classList.add('vidply-has-playlist');
58
+ }
59
+
60
+ // Update UI
61
+ if (this.playlistPanel) {
62
+ this.renderPlaylist();
63
+ }
64
+
65
+ // Auto-play first track
66
+ if (tracks.length > 0) {
67
+ this.play(0);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Play a specific track
73
+ * @param {number} index - Track index
74
+ */
75
+ play(index) {
76
+ if (index < 0 || index >= this.tracks.length) {
77
+ console.warn('VidPly Playlist: Invalid track index', index);
78
+ return;
79
+ }
80
+
81
+ const track = this.tracks[index];
82
+
83
+ // Update current index
84
+ this.currentIndex = index;
85
+
86
+ // Load track into player
87
+ this.player.load({
88
+ src: track.src,
89
+ type: track.type,
90
+ poster: track.poster,
91
+ tracks: track.tracks || []
92
+ });
93
+
94
+ // Update UI
95
+ this.updateTrackInfo(track);
96
+ this.updatePlaylistUI();
97
+
98
+ // Emit event
99
+ this.player.emit('playlisttrackchange', {
100
+ index: index,
101
+ item: track,
102
+ total: this.tracks.length
103
+ });
104
+
105
+ // Auto-play
106
+ setTimeout(() => {
107
+ this.player.play();
108
+ }, 100);
109
+ }
110
+
111
+ /**
112
+ * Play next track
113
+ */
114
+ next() {
115
+ let nextIndex = this.currentIndex + 1;
116
+
117
+ if (nextIndex >= this.tracks.length) {
118
+ if (this.options.loop) {
119
+ nextIndex = 0;
120
+ } else {
121
+ return;
122
+ }
123
+ }
124
+
125
+ this.play(nextIndex);
126
+ }
127
+
128
+ /**
129
+ * Play previous track
130
+ */
131
+ previous() {
132
+ let prevIndex = this.currentIndex - 1;
133
+
134
+ if (prevIndex < 0) {
135
+ if (this.options.loop) {
136
+ prevIndex = this.tracks.length - 1;
137
+ } else {
138
+ return;
139
+ }
140
+ }
141
+
142
+ this.play(prevIndex);
143
+ }
144
+
145
+ /**
146
+ * Handle track end
147
+ */
148
+ handleTrackEnd() {
149
+ if (this.options.autoAdvance) {
150
+ this.next();
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Handle track error
156
+ */
157
+ handleTrackError(e) {
158
+ console.error('VidPly Playlist: Track error', e);
159
+
160
+ // Try next track
161
+ if (this.options.autoAdvance) {
162
+ setTimeout(() => {
163
+ this.next();
164
+ }, 1000);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Create playlist UI
170
+ */
171
+ createUI() {
172
+ // Find player container
173
+ this.container = this.player.container;
174
+
175
+ if (!this.container) {
176
+ console.warn('VidPly Playlist: No container found');
177
+ return;
178
+ }
179
+
180
+ // Create track info element (shows current track)
181
+ this.trackInfoElement = DOMUtils.createElement('div', {
182
+ className: 'vidply-track-info'
183
+ });
184
+ this.trackInfoElement.style.display = 'none';
185
+
186
+ this.container.appendChild(this.trackInfoElement);
187
+
188
+ // Create playlist panel
189
+ this.playlistPanel = DOMUtils.createElement('div', {
190
+ className: 'vidply-playlist-panel'
191
+ });
192
+ this.playlistPanel.style.display = 'none';
193
+
194
+ this.container.appendChild(this.playlistPanel);
195
+ }
196
+
197
+ /**
198
+ * Update track info display
199
+ */
200
+ updateTrackInfo(track) {
201
+ if (!this.trackInfoElement) return;
202
+
203
+ const trackNumber = this.currentIndex + 1;
204
+ const totalTracks = this.tracks.length;
205
+
206
+ this.trackInfoElement.innerHTML = `
207
+ <div class="vidply-track-number">Track ${trackNumber} of ${totalTracks}</div>
208
+ <div class="vidply-track-title">${DOMUtils.escapeHTML(track.title || 'Untitled')}</div>
209
+ ${track.artist ? `<div class="vidply-track-artist">${DOMUtils.escapeHTML(track.artist)}</div>` : ''}
210
+ `;
211
+
212
+ this.trackInfoElement.style.display = 'block';
213
+ }
214
+
215
+ /**
216
+ * Render playlist
217
+ */
218
+ renderPlaylist() {
219
+ if (!this.playlistPanel) return;
220
+
221
+ // Clear existing
222
+ this.playlistPanel.innerHTML = '';
223
+
224
+ // Create header
225
+ const header = DOMUtils.createElement('div', {
226
+ className: 'vidply-playlist-header'
227
+ });
228
+ header.textContent = `Playlist (${this.tracks.length})`;
229
+ this.playlistPanel.appendChild(header);
230
+
231
+ // Create list
232
+ const list = DOMUtils.createElement('div', {
233
+ className: 'vidply-playlist-list'
234
+ });
235
+
236
+ this.tracks.forEach((track, index) => {
237
+ const item = this.createPlaylistItem(track, index);
238
+ list.appendChild(item);
239
+ });
240
+
241
+ this.playlistPanel.appendChild(list);
242
+ this.playlistPanel.style.display = 'block';
243
+ }
244
+
245
+ /**
246
+ * Create playlist item element
247
+ */
248
+ createPlaylistItem(track, index) {
249
+ const item = DOMUtils.createElement('div', {
250
+ className: 'vidply-playlist-item',
251
+ role: 'button',
252
+ tabIndex: 0,
253
+ 'aria-label': `Play ${track.title || 'Track ' + (index + 1)}`
254
+ });
255
+
256
+ // Add active class if current
257
+ if (index === this.currentIndex) {
258
+ item.classList.add('vidply-playlist-item-active');
259
+ }
260
+
261
+ // Thumbnail or icon
262
+ const thumbnail = DOMUtils.createElement('div', {
263
+ className: 'vidply-playlist-thumbnail'
264
+ });
265
+
266
+ if (track.poster) {
267
+ thumbnail.style.backgroundImage = `url(${track.poster})`;
268
+ } else {
269
+ // Show music/speaker icon for audio tracks
270
+ const icon = createIconElement('music');
271
+ icon.classList.add('vidply-playlist-thumbnail-icon');
272
+ thumbnail.appendChild(icon);
273
+ }
274
+
275
+ item.appendChild(thumbnail);
276
+
277
+ // Info
278
+ const info = DOMUtils.createElement('div', {
279
+ className: 'vidply-playlist-item-info'
280
+ });
281
+
282
+ const title = DOMUtils.createElement('div', {
283
+ className: 'vidply-playlist-item-title'
284
+ });
285
+ title.textContent = track.title || `Track ${index + 1}`;
286
+ info.appendChild(title);
287
+
288
+ if (track.artist) {
289
+ const artist = DOMUtils.createElement('div', {
290
+ className: 'vidply-playlist-item-artist'
291
+ });
292
+ artist.textContent = track.artist;
293
+ info.appendChild(artist);
294
+ }
295
+
296
+ item.appendChild(info);
297
+
298
+ // Play icon
299
+ const playIcon = createIconElement('play');
300
+ playIcon.classList.add('vidply-playlist-item-icon');
301
+ item.appendChild(playIcon);
302
+
303
+ // Click handler
304
+ item.addEventListener('click', () => {
305
+ this.play(index);
306
+ });
307
+
308
+ // Keyboard handler
309
+ item.addEventListener('keydown', (e) => {
310
+ if (e.key === 'Enter' || e.key === ' ') {
311
+ e.preventDefault();
312
+ this.play(index);
313
+ }
314
+ });
315
+
316
+ return item;
317
+ }
318
+
319
+ /**
320
+ * Update playlist UI (highlight current track)
321
+ */
322
+ updatePlaylistUI() {
323
+ if (!this.playlistPanel) return;
324
+
325
+ const items = this.playlistPanel.querySelectorAll('.vidply-playlist-item');
326
+
327
+ items.forEach((item, index) => {
328
+ if (index === this.currentIndex) {
329
+ item.classList.add('vidply-playlist-item-active');
330
+
331
+ // Scroll into view
332
+ item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
333
+ } else {
334
+ item.classList.remove('vidply-playlist-item-active');
335
+ }
336
+ });
337
+ }
338
+
339
+ /**
340
+ * Get current track
341
+ */
342
+ getCurrentTrack() {
343
+ return this.tracks[this.currentIndex] || null;
344
+ }
345
+
346
+ /**
347
+ * Get playlist info
348
+ */
349
+ getPlaylistInfo() {
350
+ return {
351
+ currentIndex: this.currentIndex,
352
+ totalTracks: this.tracks.length,
353
+ currentTrack: this.getCurrentTrack(),
354
+ hasNext: this.currentIndex < this.tracks.length - 1,
355
+ hasPrevious: this.currentIndex > 0
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Add track to playlist
361
+ */
362
+ addTrack(track) {
363
+ this.tracks.push(track);
364
+
365
+ if (this.playlistPanel) {
366
+ this.renderPlaylist();
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Remove track from playlist
372
+ */
373
+ removeTrack(index) {
374
+ if (index < 0 || index >= this.tracks.length) return;
375
+
376
+ this.tracks.splice(index, 1);
377
+
378
+ // Adjust current index if needed
379
+ if (index < this.currentIndex) {
380
+ this.currentIndex--;
381
+ } else if (index === this.currentIndex) {
382
+ // Current track was removed, play next or stop
383
+ if (this.currentIndex >= this.tracks.length) {
384
+ this.currentIndex = this.tracks.length - 1;
385
+ }
386
+
387
+ if (this.currentIndex >= 0) {
388
+ this.play(this.currentIndex);
389
+ }
390
+ }
391
+
392
+ if (this.playlistPanel) {
393
+ this.renderPlaylist();
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Clear playlist
399
+ */
400
+ clear() {
401
+ this.tracks = [];
402
+ this.currentIndex = -1;
403
+
404
+ if (this.playlistPanel) {
405
+ this.playlistPanel.innerHTML = '';
406
+ this.playlistPanel.style.display = 'none';
407
+ }
408
+
409
+ if (this.trackInfoElement) {
410
+ this.trackInfoElement.innerHTML = '';
411
+ this.trackInfoElement.style.display = 'none';
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Destroy playlist manager
417
+ */
418
+ destroy() {
419
+ // Remove event listeners
420
+ this.player.off('ended', this.handleTrackEnd);
421
+ this.player.off('error', this.handleTrackError);
422
+
423
+ // Remove UI
424
+ if (this.trackInfoElement) {
425
+ this.trackInfoElement.remove();
426
+ }
427
+
428
+ if (this.playlistPanel) {
429
+ this.playlistPanel.remove();
430
+ }
431
+
432
+ // Clear data
433
+ this.clear();
434
+ }
435
+ }
436
+
437
+ export default PlaylistManager;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Internationalization system
3
+ */
4
+
5
+ import { translations } from './translations.js';
6
+
7
+ class I18n {
8
+ constructor() {
9
+ this.currentLanguage = 'en';
10
+ this.translations = translations;
11
+ }
12
+
13
+ setLanguage(lang) {
14
+ if (this.translations[lang]) {
15
+ this.currentLanguage = lang;
16
+ } else {
17
+ console.warn(`Language "${lang}" not found, falling back to English`);
18
+ this.currentLanguage = 'en';
19
+ }
20
+ }
21
+
22
+ getLanguage() {
23
+ return this.currentLanguage;
24
+ }
25
+
26
+ t(key, replacements = {}) {
27
+ const keys = key.split('.');
28
+ let value = this.translations[this.currentLanguage];
29
+
30
+ for (const k of keys) {
31
+ if (value && typeof value === 'object' && k in value) {
32
+ value = value[k];
33
+ } else {
34
+ // Fallback to English
35
+ value = this.translations.en;
36
+ for (const fallbackKey of keys) {
37
+ if (value && typeof value === 'object' && fallbackKey in value) {
38
+ value = value[fallbackKey];
39
+ } else {
40
+ return key; // Return key if not found
41
+ }
42
+ }
43
+ break;
44
+ }
45
+ }
46
+
47
+ // Replace placeholders
48
+ if (typeof value === 'string') {
49
+ Object.entries(replacements).forEach(([placeholder, replacement]) => {
50
+ value = value.replace(new RegExp(`{${placeholder}}`, 'g'), replacement);
51
+ });
52
+ }
53
+
54
+ return value;
55
+ }
56
+
57
+ addTranslation(lang, translations) {
58
+ if (!this.translations[lang]) {
59
+ this.translations[lang] = {};
60
+ }
61
+ Object.assign(this.translations[lang], translations);
62
+ }
63
+ }
64
+
65
+ export const i18n = new I18n();
66
+