nocopyrightsounds-widget 1.0.1 → 1.0.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +205 -74
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nocopyrightsounds-widget",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Un widget musical persistant pour site web utilisant l'API NoCopyrightSounds",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  class NCSWidget {
2
2
  constructor(options = {}) {
3
3
  this.position = options.position || 'bottom-right';
4
-
5
- // 🌟 L'URL de VOTRE serveur par défaut (les autres devs n'auront rien à faire)
6
- this.apiUrl = options.apiUrl || 'https://ncs-backend-api.onrender.com';
4
+ this.apiUrl = options.apiUrl || 'https://ncs-backend-api.onrender.com'; // ⚠️ METTEZ VOTRE URL RENDER ICI
7
5
 
8
6
  this.audio = new Audio();
9
7
  this.isPlaying = false;
10
8
 
11
- // Persistance des données (Volume, Temps, et Musique en cours)
9
+ // Historique et File d'attente (Préchargement)
10
+ this.trackHistory = [];
11
+ this.currentHistoryIndex = -1;
12
+ this.nextTracksQueue = [];
13
+ this.isPreloading = false;
14
+
12
15
  this.isBrowser = typeof window !== 'undefined';
13
16
  if (this.isBrowser) {
14
17
  this.audio.volume = localStorage.getItem('ncs_volume') || 0.5;
@@ -16,16 +19,18 @@ class NCSWidget {
16
19
  this.savedTrack = localStorage.getItem('ncs_currentTrack') || null;
17
20
  this.savedCover = localStorage.getItem('ncs_currentCover') || null;
18
21
  this.savedTitle = localStorage.getItem('ncs_currentTitle') || null;
22
+ this.isWidgetOpen = localStorage.getItem('ncs_isOpen') === 'true';
19
23
  }
20
24
 
21
25
  this.initDOM();
22
26
  this.attachEvents();
23
27
 
24
- // Reprendre la lecture ou charger une nouvelle piste
28
+ // Initialisation de la musique
25
29
  if (this.savedTrack && this.savedTime > 0) {
26
30
  this.restoreTrack();
31
+ this.fillQueue(this.genreSelect.value); // Lancer le préchargement en fond
27
32
  } else {
28
- this.loadTrack('electronic');
33
+ this.changeGenre(this.genreSelect.value);
29
34
  }
30
35
  }
31
36
 
@@ -38,44 +43,68 @@ class NCSWidget {
38
43
  const style = document.createElement('style');
39
44
  style.textContent = `
40
45
  #ncs-persistent-widget { position: fixed; ${this.getPositionStyles()} z-index: 99999; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
46
+
47
+ /* État Réduit */
41
48
  .ncs-minimized { width: 55px; height: 55px; border-radius: 50%; background: linear-gradient(135deg, #1DB954, #1ed760); cursor: pointer; display: flex; justify-content: center; align-items: center; box-shadow: 0 6px 15px rgba(29, 185, 84, 0.4); font-size: 24px; transition: transform 0.2s; }
42
49
  .ncs-minimized:hover { transform: scale(1.1); }
43
- .ncs-expanded { width: 300px; background: #181818; color: white; border-radius: 16px; padding: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: none; border: 1px solid #282828; }
44
- .ncs-expanded.active { display: block; animation: ncsFadeIn 0.3s ease; }
45
50
  .ncs-minimized.hidden { display: none; }
46
51
 
52
+ /* État Agrandit */
53
+ .ncs-expanded { width: 320px; background: #181818; color: white; border-radius: 16px; padding: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: none; border: 1px solid #282828; }
54
+ .ncs-expanded.active { display: block; animation: ncsFadeIn 0.3s ease; }
55
+
56
+ /* Header & Infos */
47
57
  .ncs-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
48
- .ncs-header strong { font-size: 14px; font-weight: 600; color: #b3b3b3; letter-spacing: 1px; text-transform: uppercase; }
58
+ .ncs-header strong { font-size: 14px; font-weight: 600; color: #b3b3b3; letter-spacing: 1px; text-transform: uppercase; display: flex; align-items: center; gap: 8px; }
49
59
  .ncs-close-btn { background: transparent; border: none; color: #b3b3b3; font-size: 18px; cursor: pointer; padding: 0; transition: color 0.2s; }
50
60
  .ncs-close-btn:hover { color: white; }
51
61
 
62
+ /* Animation Visualizer */
63
+ .ncs-visualizer { display: flex; gap: 2px; height: 12px; align-items: flex-end; opacity: 0; transition: opacity 0.3s; }
64
+ .ncs-visualizer.playing { opacity: 1; }
65
+ .ncs-bar { width: 3px; background: #1DB954; border-radius: 2px; animation: bounce 0.5s infinite alternate; }
66
+ .ncs-bar:nth-child(2) { animation-delay: 0.15s; }
67
+ .ncs-bar:nth-child(3) { animation-delay: 0.3s; }
68
+ @keyframes bounce { from { height: 3px; } to { height: 12px; } }
69
+
52
70
  .ncs-track-info { display: flex; align-items: center; margin-bottom: 15px; }
53
- .ncs-cover { width: 60px; height: 60px; border-radius: 8px; background: #282828; margin-right: 15px; object-fit: cover; box-shadow: 0 4px 10px rgba(0,0,0,0.3); }
71
+ .ncs-cover { width: 65px; height: 65px; border-radius: 8px; background: #282828; margin-right: 15px; object-fit: cover; box-shadow: 0 4px 10px rgba(0,0,0,0.3); }
54
72
  .ncs-details { flex: 1; overflow: hidden; }
55
- #ncs-track-name { font-size: 15px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 5px; }
73
+ #ncs-track-name { font-size: 14px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 5px; }
56
74
  #ncs-genre { width: 100%; padding: 4px 8px; background: #282828; color: #b3b3b3; border: 1px solid #333; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none; }
57
75
 
76
+ /* Progress & Controls */
58
77
  .ncs-progress-container { margin-bottom: 15px; display:flex; align-items:center; gap: 10px; font-size: 11px; color: #b3b3b3; }
59
78
  .ncs-slider { -webkit-appearance: none; width: 100%; height: 4px; background: #535353; border-radius: 2px; outline: none; cursor: pointer; }
60
79
  .ncs-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #1DB954; cursor: pointer; transition: transform 0.1s; }
61
80
  .ncs-slider::-webkit-slider-thumb:hover { transform: scale(1.2); }
62
81
 
63
- .ncs-controls { display: flex; justify-content: center; align-items: center; gap: 20px; margin-bottom: 15px; }
64
- .ncs-btn-circle { width: 45px; height: 45px; border-radius: 50%; background: white; color: black; border: none; font-size: 18px; cursor: pointer; display:flex; justify-content:center; align-items:center; transition: transform 0.2s; }
82
+ .ncs-controls { display: flex; justify-content: center; align-items: center; gap: 15px; margin-bottom: 15px; }
83
+ .ncs-btn-circle { width: 50px; height: 50px; border-radius: 50%; background: white; color: black; border: none; font-size: 20px; cursor: pointer; display:flex; justify-content:center; align-items:center; transition: transform 0.2s; padding-left: 4px; }
84
+ .ncs-btn-circle.paused { padding-left: 0; }
65
85
  .ncs-btn-circle:hover { transform: scale(1.05); }
66
- .ncs-btn-icon { background: transparent; border: none; color: #b3b3b3; font-size: 20px; cursor: pointer; transition: color 0.2s; }
86
+ .ncs-btn-icon { background: transparent; border: none; color: #b3b3b3; font-size: 20px; cursor: pointer; transition: color 0.2s; padding: 5px; }
67
87
  .ncs-btn-icon:hover { color: white; }
88
+ .ncs-btn-icon:disabled { color: #333; cursor: not-allowed; }
68
89
 
69
- .ncs-volume-container { display: flex; align-items: center; gap: 10px; color: #b3b3b3; }
90
+ .ncs-bottom-bar { display: flex; justify-content: space-between; align-items: center; }
91
+ .ncs-volume-container { display: flex; align-items: center; gap: 8px; color: #b3b3b3; flex: 1; margin-right: 15px; }
92
+ .ncs-download-btn { color: #b3b3b3; text-decoration: none; font-size: 18px; transition: color 0.2s; }
93
+ .ncs-download-btn:hover { color: #1DB954; }
70
94
 
71
95
  @keyframes ncsFadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
72
96
  `;
73
97
 
74
98
  this.container.innerHTML = `
75
- <div class="ncs-minimized">🎧</div>
76
- <div class="ncs-expanded">
99
+ <div class="ncs-minimized ${this.isWidgetOpen ? 'hidden' : ''}">🎧</div>
100
+ <div class="ncs-expanded ${this.isWidgetOpen ? 'active' : ''}">
77
101
  <div class="ncs-header">
78
- <strong>NCS Player</strong>
102
+ <strong>
103
+ NCS Player
104
+ <div class="ncs-visualizer" id="ncs-vis">
105
+ <div class="ncs-bar"></div><div class="ncs-bar"></div><div class="ncs-bar"></div>
106
+ </div>
107
+ </strong>
79
108
  <button class="ncs-close-btn">✖</button>
80
109
  </div>
81
110
 
@@ -99,13 +128,17 @@ class NCSWidget {
99
128
  </div>
100
129
 
101
130
  <div class="ncs-controls">
131
+ <button id="ncs-prev" class="ncs-btn-icon" disabled>⏮</button>
102
132
  <button id="ncs-play-pause" class="ncs-btn-circle">▶</button>
103
133
  <button id="ncs-next" class="ncs-btn-icon">⏭</button>
104
134
  </div>
105
135
 
106
- <div class="ncs-volume-container">
107
- <span>🔉</span>
108
- <input type="range" id="ncs-volume" class="ncs-slider" min="0" max="1" step="0.05" value="${this.audio.volume}">
136
+ <div class="ncs-bottom-bar">
137
+ <div class="ncs-volume-container">
138
+ <span>🔉</span>
139
+ <input type="range" id="ncs-volume" class="ncs-slider" min="0" max="1" step="0.05" value="${this.audio.volume}">
140
+ </div>
141
+ <a id="ncs-download" class="ncs-download-btn" href="#" target="_blank" title="Télécharger ce titre">⬇️</a>
109
142
  </div>
110
143
  </div>
111
144
  `;
@@ -113,11 +146,13 @@ class NCSWidget {
113
146
  document.head.appendChild(style);
114
147
  document.body.appendChild(this.container);
115
148
 
116
- // Références
117
149
  this.minimized = this.container.querySelector('.ncs-minimized');
118
150
  this.expanded = this.container.querySelector('.ncs-expanded');
119
151
  this.playBtn = this.container.querySelector('#ncs-play-pause');
120
152
  this.nextBtn = this.container.querySelector('#ncs-next');
153
+ this.prevBtn = this.container.querySelector('#ncs-prev');
154
+ this.downloadBtn = this.container.querySelector('#ncs-download');
155
+ this.visualizer = this.container.querySelector('#ncs-vis');
121
156
  this.genreSelect = this.container.querySelector('#ncs-genre');
122
157
  this.trackName = this.container.querySelector('#ncs-track-name');
123
158
  this.coverImg = this.container.querySelector('#ncs-cover');
@@ -140,32 +175,37 @@ class NCSWidget {
140
175
  attachEvents() {
141
176
  if (!this.isBrowser) return;
142
177
 
143
- // UI Events
144
- this.minimized.addEventListener('click', () => this.toggleState());
145
- this.container.querySelector('.ncs-close-btn').addEventListener('click', () => this.toggleState());
178
+ this.minimized.addEventListener('click', () => this.toggleState(true));
179
+ this.container.querySelector('.ncs-close-btn').addEventListener('click', () => this.toggleState(false));
180
+
146
181
  this.playBtn.addEventListener('click', () => this.togglePlay());
147
- this.nextBtn.addEventListener('click', () => this.loadTrack(this.genreSelect.value));
148
- this.genreSelect.addEventListener('change', (e) => this.loadTrack(e.target.value));
182
+ this.nextBtn.addEventListener('click', () => this.handleNext());
183
+ this.prevBtn.addEventListener('click', () => this.handlePrev());
184
+ this.genreSelect.addEventListener('change', (e) => this.changeGenre(e.target.value));
149
185
 
150
- // Audio Events
186
+ this.audio.addEventListener('play', () => {
187
+ this.playBtn.innerHTML = '⏸';
188
+ this.playBtn.classList.add('paused');
189
+ this.visualizer.classList.add('playing');
190
+ });
191
+ this.audio.addEventListener('pause', () => {
192
+ this.playBtn.innerHTML = '▶';
193
+ this.playBtn.classList.remove('paused');
194
+ this.visualizer.classList.remove('playing');
195
+ });
151
196
  this.audio.addEventListener('timeupdate', () => this.updateProgress());
152
197
  this.audio.addEventListener('loadedmetadata', () => {
153
198
  this.progressBar.max = this.audio.duration;
154
199
  this.timeTotal.innerText = this.formatTime(this.audio.duration);
155
200
  });
156
- this.audio.addEventListener('ended', () => this.loadTrack(this.genreSelect.value)); // Auto-play suivant
201
+ this.audio.addEventListener('ended', () => this.handleNext());
157
202
 
158
- // Inputs interactifs
159
- this.progressBar.addEventListener('input', (e) => {
160
- this.audio.currentTime = e.target.value;
161
- });
162
-
203
+ this.progressBar.addEventListener('input', (e) => { this.audio.currentTime = e.target.value; });
163
204
  this.volumeBar.addEventListener('input', (e) => {
164
205
  this.audio.volume = e.target.value;
165
206
  localStorage.setItem('ncs_volume', e.target.value);
166
207
  });
167
208
 
168
- // Sauvegarde de la progression chaque seconde
169
209
  setInterval(() => {
170
210
  if (this.isPlaying && this.audio.currentTime > 0) {
171
211
  localStorage.setItem('ncs_currentTime', this.audio.currentTime);
@@ -173,22 +213,25 @@ class NCSWidget {
173
213
  }, 1000);
174
214
  }
175
215
 
176
- toggleState() {
177
- this.minimized.classList.toggle('hidden');
178
- this.expanded.classList.toggle('active');
216
+ toggleState(isOpen) {
217
+ if (isOpen) {
218
+ this.minimized.classList.add('hidden');
219
+ this.expanded.classList.add('active');
220
+ } else {
221
+ this.minimized.classList.remove('hidden');
222
+ this.expanded.classList.remove('active');
223
+ }
224
+ localStorage.setItem('ncs_isOpen', isOpen);
179
225
  }
180
226
 
181
227
  togglePlay() {
182
228
  if (this.isPlaying) {
183
229
  this.audio.pause();
184
- this.playBtn.innerHTML = '▶';
185
230
  } else {
186
- // Reprendre là où on en était si c'est le 1er clic après un changement de page
187
231
  if (this.savedTime > 0 && this.audio.currentTime === 0) {
188
232
  this.audio.currentTime = this.savedTime;
189
233
  }
190
234
  this.audio.play();
191
- this.playBtn.innerHTML = '⏸';
192
235
  }
193
236
  this.isPlaying = !this.isPlaying;
194
237
  }
@@ -205,47 +248,135 @@ class NCSWidget {
205
248
  return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
206
249
  }
207
250
 
251
+ // ----------------------------------------------------
252
+ // GESTION DES REQUÊTES ET DE LA FILE D'ATTENTE (NOUVEAU)
253
+ // ----------------------------------------------------
254
+
255
+ async fetchSingleTrack(genre) {
256
+ try {
257
+ const response = await fetch(`${this.apiUrl}/search?genre=${genre}`);
258
+ const data = await response.json();
259
+ return (data && data.length > 0) ? data[0] : null;
260
+ } catch (error) {
261
+ console.error("Erreur API NCS:", error);
262
+ return null;
263
+ }
264
+ }
265
+
266
+ async fillQueue(genre) {
267
+ if (this.isPreloading) return; // Éviter de lancer plusieurs recherches en même temps
268
+ this.isPreloading = true;
269
+
270
+ // Tant qu'on n'a pas 2 musiques d'avance, on cherche silencieusement
271
+ while (this.nextTracksQueue.length < 2) {
272
+ const track = await this.fetchSingleTrack(genre);
273
+ if (track) {
274
+ // Vérifier qu'on n'ajoute pas un doublon dans la file
275
+ const isDuplicate = this.nextTracksQueue.find(t => t.audioUrl === track.audioUrl);
276
+ if (!isDuplicate) this.nextTracksQueue.push(track);
277
+ } else {
278
+ break; // Stop si l'API ne répond plus
279
+ }
280
+ }
281
+
282
+ this.isPreloading = false;
283
+ }
284
+
285
+ async changeGenre(genre) {
286
+ this.nextTracksQueue = []; // On vide la file d'attente car le genre a changé
287
+ this.trackName.innerText = "Recherche...";
288
+
289
+ const track = await this.fetchSingleTrack(genre);
290
+ if (track) {
291
+ this.setTrack(track, true);
292
+ this.fillQueue(genre); // On lance le préchargement en fond !
293
+ } else {
294
+ this.trackName.innerText = "Aucune piste trouvée.";
295
+ }
296
+ }
297
+
298
+ // ----------------------------------------------------
299
+ // GESTION DU LECTEUR ET HISTORIQUE
300
+ // ----------------------------------------------------
301
+
208
302
  restoreTrack() {
209
303
  this.audio.src = this.savedTrack;
210
304
  this.trackName.innerText = this.savedTitle;
211
305
  if (this.savedCover) this.coverImg.src = this.savedCover;
212
- // On ne met pas play() automatiquement à cause des règles des navigateurs (Autoplay policy)
306
+ this.downloadBtn.href = this.savedTrack;
307
+
308
+ this.trackHistory = [{
309
+ audioUrl: this.savedTrack,
310
+ title: this.savedTitle,
311
+ coverUrl: this.savedCover
312
+ }];
313
+ this.currentHistoryIndex = 0;
213
314
  }
214
315
 
215
- async loadTrack(genre) {
216
- this.trackName.innerText = "Recherche...";
217
- try {
218
- const response = await fetch(`${this.apiUrl}/search?genre=${genre}`);
219
- const data = await response.json();
316
+ setTrack(track, addToHistory = false) {
317
+ this.audio.src = track.audioUrl;
318
+ this.trackName.innerText = track.title;
319
+ if (track.coverUrl) this.coverImg.src = track.coverUrl;
320
+ this.downloadBtn.href = track.audioUrl;
321
+
322
+ if (addToHistory) {
323
+ // Effacer le "futur" si on était revenu en arrière avant d'avancer
324
+ this.trackHistory = this.trackHistory.slice(0, this.currentHistoryIndex + 1);
325
+ this.trackHistory.push(track);
326
+ this.currentHistoryIndex = this.trackHistory.length - 1;
327
+ this.prevBtn.disabled = this.currentHistoryIndex <= 0;
328
+ }
220
329
 
221
- if (data && data.length > 0) {
222
- const track = data[0];
223
- this.audio.src = track.audioUrl;
224
- this.trackName.innerText = track.title;
225
- if (track.coverUrl) this.coverImg.src = track.coverUrl;
226
-
227
- // Sauvegarder la nouvelle piste courante
228
- if (this.isBrowser) {
229
- localStorage.setItem('ncs_currentTrack', track.audioUrl);
230
- localStorage.setItem('ncs_currentTitle', track.title);
231
- localStorage.setItem('ncs_currentCover', track.coverUrl);
232
- localStorage.setItem('ncs_currentTime', 0); // Reset timer
233
- this.savedTime = 0;
234
- }
235
-
236
- // Si on était déjà en train d'écouter, on enchaîne
237
- if (this.isPlaying) {
238
- this.audio.play();
239
- } else if (this.audio.currentTime === 0 && this.playBtn.innerHTML === '⏸') {
240
- // Cas du clic sur "Suivant" alors que c'était en pause
241
- this.audio.play();
242
- this.isPlaying = true;
243
- }
244
- } else {
245
- this.trackName.innerText = "Aucune piste.";
330
+ if (this.isBrowser) {
331
+ localStorage.setItem('ncs_currentTrack', track.audioUrl);
332
+ localStorage.setItem('ncs_currentTitle', track.title);
333
+ localStorage.setItem('ncs_currentCover', track.coverUrl);
334
+ localStorage.setItem('ncs_currentTime', 0);
335
+ this.savedTime = 0;
336
+ }
337
+
338
+ if (this.isPlaying) {
339
+ this.audio.play();
340
+ } else if (this.audio.currentTime === 0 && this.playBtn.innerHTML === '') {
341
+ this.audio.play();
342
+ this.isPlaying = true;
343
+ }
344
+ }
345
+
346
+ async handleNext() {
347
+ const genre = this.genreSelect.value;
348
+
349
+ // Cas 1 : On a reculé dans l'historique et on veut revenir à la musique suivante connue
350
+ if (this.currentHistoryIndex < this.trackHistory.length - 1) {
351
+ this.currentHistoryIndex++;
352
+ this.setTrack(this.trackHistory[this.currentHistoryIndex], false);
353
+ this.prevBtn.disabled = this.currentHistoryIndex <= 0;
354
+ }
355
+ // Cas 2 : L'historique est au bout, on pioche dans la file d'attente (instantané !)
356
+ else if (this.nextTracksQueue.length > 0) {
357
+ const nextTrack = this.nextTracksQueue.shift(); // Prend la 1ère de la file
358
+ this.setTrack(nextTrack, true);
359
+ this.fillQueue(genre); // On refait le plein discrètement
360
+ }
361
+ // Cas 3 : L'utilisateur clique trop vite et la file d'attente est vide (Secours)
362
+ else {
363
+ this.trackName.innerText = "Recherche...";
364
+ const track = await this.fetchSingleTrack(genre);
365
+ if (track) {
366
+ this.setTrack(track, true);
367
+ this.fillQueue(genre);
246
368
  }
247
- } catch (error) {
248
- this.trackName.innerText = "Erreur API";
369
+ }
370
+ }
371
+
372
+ handlePrev() {
373
+ if (this.currentHistoryIndex > 0) {
374
+ this.currentHistoryIndex--;
375
+ this.setTrack(this.trackHistory[this.currentHistoryIndex], false);
376
+ this.prevBtn.disabled = this.currentHistoryIndex <= 0;
377
+
378
+ this.savedTime = 0;
379
+ localStorage.setItem('ncs_currentTime', 0);
249
380
  }
250
381
  }
251
382
  }