nocopyrightsounds-widget 1.0.0 → 1.0.1

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 +160 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nocopyrightsounds-widget",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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,56 +1,111 @@
1
- // src/index.js
2
-
3
1
  class NCSWidget {
4
2
  constructor(options = {}) {
5
3
  this.position = options.position || 'bottom-right';
6
- this.apiUrl = options.apiUrl || 'https://ncs-api.kaninchenspeed.workers.dev';
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';
7
+
7
8
  this.audio = new Audio();
8
9
  this.isPlaying = false;
9
10
 
10
- // Côté serveur (SSR comme Next.js), localStorage n'existe pas. On le sécurise.
11
- this.savedTime = typeof window !== 'undefined' && localStorage.getItem('ncs_currentTime')
12
- ? localStorage.getItem('ncs_currentTime')
13
- : 0;
14
-
11
+ // Persistance des données (Volume, Temps, et Musique en cours)
12
+ this.isBrowser = typeof window !== 'undefined';
13
+ if (this.isBrowser) {
14
+ this.audio.volume = localStorage.getItem('ncs_volume') || 0.5;
15
+ this.savedTime = localStorage.getItem('ncs_currentTime') || 0;
16
+ this.savedTrack = localStorage.getItem('ncs_currentTrack') || null;
17
+ this.savedCover = localStorage.getItem('ncs_currentCover') || null;
18
+ this.savedTitle = localStorage.getItem('ncs_currentTitle') || null;
19
+ }
20
+
15
21
  this.initDOM();
16
22
  this.attachEvents();
17
- this.loadTrack('electronic');
23
+
24
+ // Reprendre la lecture ou charger une nouvelle piste
25
+ if (this.savedTrack && this.savedTime > 0) {
26
+ this.restoreTrack();
27
+ } else {
28
+ this.loadTrack('electronic');
29
+ }
18
30
  }
19
31
 
20
32
  initDOM() {
21
- if (typeof document === 'undefined') return; // Sécurité pour le Server-Side Rendering
33
+ if (!this.isBrowser) return;
22
34
 
23
35
  this.container = document.createElement('div');
24
36
  this.container.id = 'ncs-persistent-widget';
25
37
 
26
38
  const style = document.createElement('style');
27
39
  style.textContent = `
28
- #ncs-persistent-widget { position: fixed; ${this.getPositionStyles()} z-index: 9999; font-family: sans-serif; transition: all 0.3s ease; }
29
- .ncs-minimized { width: 50px; height: 50px; border-radius: 50%; background: #1DB954; cursor: pointer; display: flex; justify-content: center; align-items: center; box-shadow: 0 4px 10px rgba(0,0,0,0.2); }
30
- .ncs-expanded { width: 250px; background: #222; color: white; border-radius: 12px; padding: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); display: none; }
31
- .ncs-expanded.active { display: block; }
40
+ #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); }
41
+ .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
+ .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; }
32
45
  .ncs-minimized.hidden { display: none; }
33
- .ncs-controls { display: flex; justify-content: space-between; margin-top: 10px; }
34
- button { background: #1DB954; border: none; color: white; padding: 5px 10px; border-radius: 5px; cursor: pointer; }
35
- select { width: 100%; padding: 5px; margin-bottom: 10px; background: #333; color: white; border: 1px solid #444; }
46
+
47
+ .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; }
49
+ .ncs-close-btn { background: transparent; border: none; color: #b3b3b3; font-size: 18px; cursor: pointer; padding: 0; transition: color 0.2s; }
50
+ .ncs-close-btn:hover { color: white; }
51
+
52
+ .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); }
54
+ .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; }
56
+ #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
+
58
+ .ncs-progress-container { margin-bottom: 15px; display:flex; align-items:center; gap: 10px; font-size: 11px; color: #b3b3b3; }
59
+ .ncs-slider { -webkit-appearance: none; width: 100%; height: 4px; background: #535353; border-radius: 2px; outline: none; cursor: pointer; }
60
+ .ncs-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #1DB954; cursor: pointer; transition: transform 0.1s; }
61
+ .ncs-slider::-webkit-slider-thumb:hover { transform: scale(1.2); }
62
+
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; }
65
+ .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; }
67
+ .ncs-btn-icon:hover { color: white; }
68
+
69
+ .ncs-volume-container { display: flex; align-items: center; gap: 10px; color: #b3b3b3; }
70
+
71
+ @keyframes ncsFadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
36
72
  `;
37
73
 
38
74
  this.container.innerHTML = `
39
- <div class="ncs-minimized">🎵</div>
75
+ <div class="ncs-minimized">🎧</div>
40
76
  <div class="ncs-expanded">
41
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
77
+ <div class="ncs-header">
42
78
  <strong>NCS Player</strong>
43
- <button class="ncs-close-btn" style="background:transparent; padding:0;">✖</button>
79
+ <button class="ncs-close-btn">✖</button>
80
+ </div>
81
+
82
+ <div class="ncs-track-info">
83
+ <img id="ncs-cover" class="ncs-cover" src="data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Cover" />
84
+ <div class="ncs-details">
85
+ <div id="ncs-track-name">Chargement...</div>
86
+ <select id="ncs-genre">
87
+ <option value="electronic">⚡ Électronique</option>
88
+ <option value="house">🏠 House</option>
89
+ <option value="chill">☕ Chill</option>
90
+ <option value="synthwave">🌆 Synthwave</option>
91
+ </select>
92
+ </div>
44
93
  </div>
45
- <select id="ncs-genre">
46
- <option value="electronic">Électronique</option>
47
- <option value="chill">Chill / Lo-Fi</option>
48
- <option value="synthwave">Synthwave</option>
49
- </select>
50
- <div id="ncs-track-name" style="font-size:12px; margin-bottom:10px;">Chargement...</div>
94
+
95
+ <div class="ncs-progress-container">
96
+ <span id="ncs-time-current">0:00</span>
97
+ <input type="range" id="ncs-progress" class="ncs-slider" min="0" max="100" value="0">
98
+ <span id="ncs-time-total">0:00</span>
99
+ </div>
100
+
51
101
  <div class="ncs-controls">
52
- <button id="ncs-play-pause">Play</button>
53
- <button id="ncs-next">Suivant</button>
102
+ <button id="ncs-play-pause" class="ncs-btn-circle">▶</button>
103
+ <button id="ncs-next" class="ncs-btn-icon">⏭</button>
104
+ </div>
105
+
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}">
54
109
  </div>
55
110
  </div>
56
111
  `;
@@ -58,33 +113,61 @@ class NCSWidget {
58
113
  document.head.appendChild(style);
59
114
  document.body.appendChild(this.container);
60
115
 
116
+ // Références
61
117
  this.minimized = this.container.querySelector('.ncs-minimized');
62
118
  this.expanded = this.container.querySelector('.ncs-expanded');
63
119
  this.playBtn = this.container.querySelector('#ncs-play-pause');
120
+ this.nextBtn = this.container.querySelector('#ncs-next');
64
121
  this.genreSelect = this.container.querySelector('#ncs-genre');
65
122
  this.trackName = this.container.querySelector('#ncs-track-name');
123
+ this.coverImg = this.container.querySelector('#ncs-cover');
124
+ this.progressBar = this.container.querySelector('#ncs-progress');
125
+ this.volumeBar = this.container.querySelector('#ncs-volume');
126
+ this.timeCurrent = this.container.querySelector('#ncs-time-current');
127
+ this.timeTotal = this.container.querySelector('#ncs-time-total');
66
128
  }
67
129
 
68
130
  getPositionStyles() {
69
131
  const positions = {
70
- 'bottom-right': 'bottom: 20px; right: 20px;',
71
- 'bottom-left': 'bottom: 20px; left: 20px;',
72
- 'top-right': 'top: 20px; right: 20px;',
73
- 'top-left': 'top: 20px; left: 20px;'
132
+ 'bottom-right': 'bottom: 25px; right: 25px;',
133
+ 'bottom-left': 'bottom: 25px; left: 25px;',
134
+ 'top-right': 'top: 25px; right: 25px;',
135
+ 'top-left': 'top: 25px; left: 25px;'
74
136
  };
75
137
  return positions[this.position] || positions['bottom-right'];
76
138
  }
77
139
 
78
140
  attachEvents() {
79
- if (typeof document === 'undefined') return;
141
+ if (!this.isBrowser) return;
80
142
 
143
+ // UI Events
81
144
  this.minimized.addEventListener('click', () => this.toggleState());
82
145
  this.container.querySelector('.ncs-close-btn').addEventListener('click', () => this.toggleState());
83
146
  this.playBtn.addEventListener('click', () => this.togglePlay());
147
+ this.nextBtn.addEventListener('click', () => this.loadTrack(this.genreSelect.value));
84
148
  this.genreSelect.addEventListener('change', (e) => this.loadTrack(e.target.value));
85
149
 
150
+ // Audio Events
151
+ this.audio.addEventListener('timeupdate', () => this.updateProgress());
152
+ this.audio.addEventListener('loadedmetadata', () => {
153
+ this.progressBar.max = this.audio.duration;
154
+ this.timeTotal.innerText = this.formatTime(this.audio.duration);
155
+ });
156
+ this.audio.addEventListener('ended', () => this.loadTrack(this.genreSelect.value)); // Auto-play suivant
157
+
158
+ // Inputs interactifs
159
+ this.progressBar.addEventListener('input', (e) => {
160
+ this.audio.currentTime = e.target.value;
161
+ });
162
+
163
+ this.volumeBar.addEventListener('input', (e) => {
164
+ this.audio.volume = e.target.value;
165
+ localStorage.setItem('ncs_volume', e.target.value);
166
+ });
167
+
168
+ // Sauvegarde de la progression chaque seconde
86
169
  setInterval(() => {
87
- if (this.isPlaying && typeof window !== 'undefined') {
170
+ if (this.isPlaying && this.audio.currentTime > 0) {
88
171
  localStorage.setItem('ncs_currentTime', this.audio.currentTime);
89
172
  }
90
173
  }, 1000);
@@ -98,31 +181,66 @@ class NCSWidget {
98
181
  togglePlay() {
99
182
  if (this.isPlaying) {
100
183
  this.audio.pause();
101
- this.playBtn.innerText = 'Play';
184
+ this.playBtn.innerHTML = '';
102
185
  } else {
186
+ // Reprendre là où on en était si c'est le 1er clic après un changement de page
187
+ if (this.savedTime > 0 && this.audio.currentTime === 0) {
188
+ this.audio.currentTime = this.savedTime;
189
+ }
103
190
  this.audio.play();
104
- this.playBtn.innerText = 'Pause';
191
+ this.playBtn.innerHTML = '';
105
192
  }
106
193
  this.isPlaying = !this.isPlaying;
107
194
  }
108
195
 
196
+ updateProgress() {
197
+ this.progressBar.value = this.audio.currentTime;
198
+ this.timeCurrent.innerText = this.formatTime(this.audio.currentTime);
199
+ }
200
+
201
+ formatTime(seconds) {
202
+ if (isNaN(seconds)) return "0:00";
203
+ const mins = Math.floor(seconds / 60);
204
+ const secs = Math.floor(seconds % 60);
205
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
206
+ }
207
+
208
+ restoreTrack() {
209
+ this.audio.src = this.savedTrack;
210
+ this.trackName.innerText = this.savedTitle;
211
+ if (this.savedCover) this.coverImg.src = this.savedCover;
212
+ // On ne met pas play() automatiquement à cause des règles des navigateurs (Autoplay policy)
213
+ }
214
+
109
215
  async loadTrack(genre) {
110
216
  this.trackName.innerText = "Recherche...";
111
217
  try {
112
- const response = await fetch(`${this.apiUrl}/search?genre=${genre}&limit=1`);
218
+ const response = await fetch(`${this.apiUrl}/search?genre=${genre}`);
113
219
  const data = await response.json();
114
220
 
115
221
  if (data && data.length > 0) {
116
222
  const track = data[0];
117
- this.audio.src = track.audioUrl;
223
+ this.audio.src = track.audioUrl;
118
224
  this.trackName.innerText = track.title;
225
+ if (track.coverUrl) this.coverImg.src = track.coverUrl;
119
226
 
120
- if (this.savedTime > 0) {
121
- this.audio.currentTime = this.savedTime;
122
- this.savedTime = 0;
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;
123
234
  }
124
235
 
125
- if (this.isPlaying) this.audio.play();
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
+ }
126
244
  } else {
127
245
  this.trackName.innerText = "Aucune piste.";
128
246
  }
@@ -132,5 +250,4 @@ class NCSWidget {
132
250
  }
133
251
  }
134
252
 
135
- // Export par défaut pour l'utilisation en module
136
253
  export default NCSWidget;