Elias207 commited on
Commit
8522950
·
verified ·
1 Parent(s): 35dd89a

Update static/js/editor.js

Browse files
Files changed (1) hide show
  1. static/js/editor.js +501 -199
static/js/editor.js CHANGED
@@ -1,12 +1,25 @@
 
 
 
 
 
 
 
 
 
 
1
  // --- تابع آپدیت گرافیکی نوار اسلایدر ظاهر ---
2
  function updateRange(input, labelId) {
3
  const label = document.getElementById(labelId);
4
  if (label) label.innerText = input.value + '%';
5
- const min = parseFloat(input.min), max = parseFloat(input.max), val = parseFloat(input.value);
 
 
6
  const percentage = ((val - min) / (max - min)) * 100;
7
  input.style.backgroundSize = percentage + '% 100%';
8
  }
9
 
 
10
  function syncUIWithState() {
11
  let fzPercent = ((state.st.fz - 10) / 140) * 100;
12
  const fzInput = document.getElementById('fz');
@@ -31,45 +44,64 @@ function openProject(projectId) {
31
  req.onsuccess = (e) => {
32
  const p = e.target.result;
33
  if (!p) { alert('پروژه یافت نشد'); loadHome(); return; }
 
34
  state = p.state;
35
  document.getElementById('currentProjectTitle').innerText = p.name;
36
  const videoURL = URL.createObjectURL(p.videoBlob);
37
  v.src = videoURL;
38
 
39
- syncUIWithState(); // اسلایدرها
 
 
 
 
40
 
41
  document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked'));
42
  if(state.st.f === 'vazir') document.querySelectorAll('.font-btn')[1].classList.add('ticked');
43
  else if(state.st.f === 'lalezar') document.querySelectorAll('.font-btn')[0].classList.add('ticked');
44
 
45
- // استایل کارت
46
  document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected'));
47
  if(state.st.name === 'karaoke_static') {
48
- const staticCard = Array.from(document.querySelectorAll('.style-card')).find(c => c.innerHTML.includes("ثابت"));
49
  if(staticCard) staticCard.classList.add('selected');
50
- } else if (state.st.name === 'classic' || state.st.name === 'progressive_write') {
51
- // پیدا کردن کارت مناسب در پنل کاستوم (اینجا پیش‌فرض را کلاسیک در نظر می‌گیریم اگر تطبیقی نبود)
52
- const cards = document.querySelectorAll('.custom-style-container .style-card');
53
- if(state.st.name === 'classic' && cards[0]) cards[0].classList.add('selected');
54
- if(state.st.name === 'progressive_write' && cards[1]) cards[1].classList.add('selected');
55
  }
56
 
57
  document.getElementById('homeScreen').style.display = 'none';
58
  document.getElementById('editorScreen').style.display = 'flex';
 
 
 
 
 
 
59
  renderSegList();
60
  fit();
61
  upd();
62
- v.onloadeddata = () => { fit(); if(!p.thumbnail) saveProjectToDB(); };
 
 
 
 
 
 
 
 
 
 
63
  };
64
  }
65
 
66
  function goHome() { v.pause(); saveProjectToDB(); loadHome(); }
 
67
  function fit() {
68
  if(!state.w) return;
69
  const ws = document.getElementById('workspace');
70
- const scale = Math.min((ws.clientWidth - 40) / state.w, (window.innerHeight * 0.6) / state.h);
71
- const c = document.getElementById('videoContainer'); c.style.width = state.w + 'px'; c.style.height = state.h + 'px';
72
- document.getElementById('scaler').style.transform = `scale(${scale})`; ws.style.height = (state.h * scale + 40) + 'px';
 
 
 
73
  }
74
 
75
  function activateCustomStyle() {
@@ -81,194 +113,195 @@ function activateCustomStyle() {
81
  }
82
  }
83
 
84
- /* =========================================
85
- COLOR PICKER LOGIC (IMPORTED & ADAPTED)
86
- ========================================= */
87
- let pickerState = { h: 0, s: 0, v: 100, a: 100 };
88
- let currentTarget = null;
89
- let savedColors = [];
90
- let isDragging = false;
91
- const spectrumArea = document.getElementById('spectrumArea');
92
- const spectrumHandle = document.getElementById('spectrumHandle');
93
-
94
- // تبدیل‌ها
95
- function rgbToHex(r,g,b) {
96
- return "#" + ((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1).toUpperCase();
97
- }
98
- function hexToRgb(hex) {
99
- hex = (hex||'').replace('#','').trim();
100
- if(hex.length === 3) hex = hex.split('').map(ch=>ch+ch).join('');
101
- if(hex.length!==6) return {r:255,g:255,b:255};
102
- return { r: parseInt(hex.substring(0,2),16), g: parseInt(hex.substring(2,4),16), b: parseInt(hex.substring(4,6),16) };
103
- }
104
- function hsvToRgb(h, s, v) {
105
- s/=100; v/=100; let c=v*s, x=c*(1-Math.abs(((h/60)%2)-1)), m=v-c, r=0,g=0,b=0;
106
- if(0<=h&&h<60){r=c;g=x;b=0}else if(60<=h&&h<120){r=x;g=c;b=0}else if(120<=h&&h<180){r=0;g=c;b=x}
107
- else if(180<=h&&h<240){r=0;g=x;b=c}else if(240<=h&&h<300){r=x;g=0;b=c}else if(300<=h&&h<360){r=c;g=0;b=x}
108
- return { r:Math.round((r+m)*255), g:Math.round((g+m)*255), b:Math.round((b+m)*255) };
109
- }
110
- function rgbToHsv(r, g, b) {
111
- r/=255; g/=255; b/=255; let max=Math.max(r,g,b), min=Math.min(r,g,b), d=max-min, h=0, s=(max===0?0:d/max), v=max;
112
- if(d!==0){ switch(max){ case r:h=(g-b)/d+(g<b?6:0);break; case g:h=(b-r)/d+2;break; case b:h=(r-g)/d+4;break; } h*=60; }
113
- return {h, s:s*100, v:v*100};
114
- }
115
 
116
- // Picker Actions
117
- function openColorPicker(target, ev) {
118
- if(ev) ev.stopPropagation();
119
- currentTarget = target;
120
- document.getElementById('pickerBackdrop').classList.add('active');
121
- document.getElementById('colorPickerModal').classList.add('active');
 
 
 
 
 
 
 
 
122
 
123
- // Read initial color from state
124
- let hex = '#FFFFFF';
125
- if(target === 'main') hex = state.st.col || '#FFFFFF';
126
- if(target === 'bg') hex = state.st.bg || '#000000';
127
- if(target === 'static') hex = state.st.col || '#A020F0';
 
 
 
128
 
129
- const rgb = hexToRgb(hex);
130
- const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
131
- pickerState = { h: hsv.h, s: hsv.s, v: hsv.v, a: 100 }; // فعلاً آلفا روی ۱۰۰ (یا اگه بخواید از استیت بخونید)
 
 
 
132
 
133
- initSavedColors();
134
- generateGrid();
135
- switchTab('spectrum');
136
- }
 
 
 
137
 
138
- function closePicker() {
139
- document.getElementById('colorPickerModal').classList.remove('active');
140
- document.getElementById('pickerBackdrop').classList.remove('active');
141
  }
142
 
143
- function saveAndClosePicker() {
144
- const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v);
145
- const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
146
- // برای سادگی فعلاً آلفا را در کد hex اعمال نمی‌کنیم اما اگه بخواید RGBA باشه باید لاجیک `upd` عوض شه
147
- // سیستم شما بر اساس hex رنگ کار می‌کند:
148
-
149
- if (currentTarget === 'main') state.st.col = hex;
150
- else if (currentTarget === 'bg') state.st.bg = hex;
151
- else if (currentTarget === 'static') {
152
- state.st.col = hex;
153
- document.documentElement.style.setProperty('--static-color', hex);
 
 
 
154
  }
155
-
156
- upd(); // Apply to preview
157
- closePicker();
158
  }
159
 
160
- function switchTab(name) {
161
- document.querySelectorAll('.picker-tab').forEach(t=>t.classList.remove('active'));
162
- document.getElementById('tab-'+name).classList.add('active');
163
- document.querySelectorAll('.view-section').forEach(v=>v.classList.remove('active-view'));
164
- document.getElementById('view-'+name).classList.add('active-view');
165
- syncAllUI();
 
 
 
 
 
 
 
 
 
166
  }
167
 
168
- function handleSpectrum(e) {
169
- const rect = spectrumArea.getBoundingClientRect();
170
- let x = Math.max(0, Math.min((e.clientX||e.touches[0].clientX) - rect.left, rect.width));
171
- let y = Math.max(0, Math.min((e.clientY||e.touches[0].clientY) - rect.top, rect.height));
172
- spectrumHandle.style.left = x+'px'; spectrumHandle.style.top = y+'px';
173
- pickerState.h = (x/rect.width)*360; pickerState.s = 100 - ((y/rect.height)*100);
174
- syncAllUI();
 
 
 
 
175
  }
176
- spectrumArea.addEventListener('mousedown', e => { isDragging=true; handleSpectrum(e); });
177
- window.addEventListener('mousemove', e => { if(isDragging) handleSpectrum(e); });
178
- window.addEventListener('mouseup', () => isDragging=false);
179
- spectrumArea.addEventListener('touchstart', e => { isDragging=true; handleSpectrum(e.touches[0]); },{passive:false});
180
- window.addEventListener('touchmove', e => { if(isDragging){e.preventDefault(); handleSpectrum(e.touches[0]);} },{passive:false});
181
- window.addEventListener('touchend', () => isDragging=false);
182
 
183
- function updateBrightness() { pickerState.v = parseInt(document.getElementById('brightness-slider').value); syncAllUI(); }
184
- function updateAlpha() { pickerState.a = parseInt(document.getElementById('alpha-slider').value); syncAllUI(); }
185
- function updateFromHexInput() {
186
- let val = document.getElementById('input-hex').value.replace('#','');
187
- if(/^[0-9A-Fa-f]{6}$/.test(val)) {
188
- let rgb = hexToRgb(val); let hsv = rgbToHsv(rgb.r,rgb.g,rgb.b);
189
- pickerState.h=hsv.h; pickerState.s=hsv.s; pickerState.v=hsv.v;
190
- syncAllUI();
 
191
  }
192
  }
193
- function updateRGBFromInputs() {
194
- let r=parseInt(document.getElementById('input-r').value)||0, g=parseInt(document.getElementById('input-g').value)||0, b=parseInt(document.getElementById('input-b').value)||0;
195
- let hsv=rgbToHsv(r,g,b); pickerState.h=hsv.h; pickerState.s=hsv.s; pickerState.v=hsv.v; syncAllUI();
196
- }
197
- function updateRGBFromSliders() {
198
- let r=parseInt(document.getElementById('slider-r').value), g=parseInt(document.getElementById('slider-g').value), b=parseInt(document.getElementById('slider-b').value);
199
- let hsv=rgbToHsv(r,g,b); pickerState.h=hsv.h; pickerState.s=hsv.s; pickerState.v=hsv.v; syncAllUI();
 
 
 
200
  }
 
201
 
202
- function syncAllUI() {
203
- let rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v);
204
- let hex = rgbToHex(rgb.r, rgb.g, rgb.b);
205
- let bg = `rgb(${rgb.r},${rgb.g},${rgb.b})`;
206
-
207
- // Sliders
208
- document.getElementById('slider-r').value = document.getElementById('input-r').value = rgb.r;
209
- document.getElementById('slider-g').value = document.getElementById('input-g').value = rgb.g;
210
- document.getElementById('slider-b').value = document.getElementById('input-b').value = rgb.b;
211
- document.getElementById('input-hex').value = hex.replace('#','');
212
- document.getElementById('largeColorPreview').style.backgroundColor = bg;
213
- document.getElementById('brightness-slider').value = Math.round(pickerState.v);
214
- document.getElementById('alpha-slider').value = Math.round(pickerState.a);
215
- document.getElementById('disp-brightness').innerText = Math.round(pickerState.v);
216
- document.getElementById('disp-alpha').innerText = Math.round(pickerState.a);
217
 
218
- // Spectrum Handle
219
- if(!isDragging) {
220
- spectrumHandle.style.left = (pickerState.h/360*100)+'%';
221
- spectrumHandle.style.top = (100-pickerState.s)+'%';
 
 
 
 
 
 
 
 
 
 
 
222
  }
223
  }
224
 
225
- // Saved Colors & Grid
226
- function initSavedColors() {
227
- try { savedColors = JSON.parse(localStorage.getItem('mySavedColors')) || []; } catch{ savedColors=[]; }
228
- renderSavedColors();
229
- }
230
- function saveCurrentColor() {
231
- let rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v);
232
- savedColors.push(rgbToHex(rgb.r, rgb.g, rgb.b));
233
- localStorage.setItem('mySavedColors', JSON.stringify(savedColors));
234
- renderSavedColors();
235
- document.getElementById('toastNotification').classList.add('show');
236
- setTimeout(()=>document.getElementById('toastNotification').classList.remove('show'), 2000);
237
  }
238
- function renderSavedColors() {
239
- const c = document.getElementById('savedColorsContainer');
240
- // keep only save button
241
- while(c.children.length > 1) c.removeChild(c.lastChild);
242
- savedColors.forEach((hex, i) => {
243
- let d = document.createElement('div'); d.className='saved-color-wrapper';
244
- d.innerHTML = `<div class="saved-circle" style="background:${hex}" onclick="loadColor('${hex}')"></div><div class="mini-delete-btn" onclick="deleteColor(${i})"><i class="fa-solid fa-xmark"></i></div>`;
245
- c.appendChild(d);
246
- });
247
- }
248
- function deleteColor(i) {
249
- savedColors.splice(i, 1); localStorage.setItem('mySavedColors', JSON.stringify(savedColors)); renderSavedColors();
250
- }
251
- function loadColor(hex) {
252
- let rgb = hexToRgb(hex); let hsv = rgbToHsv(rgb.r,rgb.g,rgb.b);
253
- pickerState.h=hsv.h; pickerState.s=hsv.s; pickerState.v=hsv.v; syncAllUI();
254
- }
255
- function generateGrid() {
256
- const cont = document.getElementById('gridContainer'); cont.innerHTML='';
257
- const colors = ['#FFFFFF','#000000','#FF3B30','#FF9500','#FFCC00','#34C759','#00C7BE','#32ADE6','#007AFF','#5856D6','#AF52DE','#FF2D55'];
258
- // Generate palette
259
- for(let h=0; h<360; h+=15) {
260
- let rgb = hsvToRgb(h, 80, 90);
261
- colors.push(rgbToHex(rgb.r, rgb.g, rgb.b));
262
- }
263
- colors.forEach(hex => {
264
- let d = document.createElement('div'); d.className='grid-item'; d.style.background=hex;
265
- d.onclick=()=>{loadColor(hex)}; cont.appendChild(d);
266
- });
 
 
267
  }
268
 
269
- // ------------------------------------
270
- // Original UPD Logic (Synced)
271
- // ------------------------------------
272
  function upd() {
273
  saveProjectToDB();
274
  const uiFz = parseFloat(document.getElementById('fz').value);
@@ -276,61 +309,330 @@ function upd() {
276
  const uiX = parseFloat(document.getElementById('posX').value);
277
  state.st.fz = Math.round(10 + (uiFz / 100) * 140);
278
  state.st.y = Math.round((uiY / 100) * 1200);
279
- state.st.x = Math.round((uiX / 100) * 500);
280
 
281
- // Apply color button previews
282
- document.getElementById('preview-main-btn').style.backgroundColor = state.st.col || '#fff';
283
- document.getElementById('preview-bg-btn').style.backgroundColor = state.st.bg || '#000';
284
 
285
  let font = 'Vazirmatn';
286
  if(state.st.f === 'lalezar') font = 'Lalezar'; if(state.st.f === 'bangers') font = 'Impact'; if(state.st.f === 'roboto') font = 'Arial';
287
 
288
- tEl.style.fontFamily = font;
289
- tEl.style.fontSize = state.st.fz + 'px';
290
- tEl.style.bottom = state.st.y + 'px';
291
- tEl.style.textAlign = 'center';
292
- tEl.style.left = '50%';
293
- tEl.style.transform = `translateX(calc(-50% + ${state.st.x}px))`;
294
- tEl.style.borderRadius = '0px';
295
 
296
  if(state.st.name === 'karaoke_static' || state.st.name === 'auto_director') {
297
  tEl.style.backgroundColor = 'transparent'; tEl.style.color = '#FFFFFF'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none';
298
  } else if (state.st.name === 'plain_white') {
299
  tEl.style.color = '#FFFFFF'; tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none';
300
  } else if (state.st.name === 'white_outline') {
301
- tEl.style.color = '#FFFFFF'; tEl.style.backgroundColor = 'transparent'; const s = Math.max(3, state.st.fz / 4.5); tEl.style.webkitTextStroke = `${s}px #000000`; tEl.style.paintOrder = 'stroke fill'; tEl.style.webkitTextStroke = `${s}px #000000`; tEl.style.textShadow = 'none';
302
  } else {
 
 
 
303
  tEl.style.color = state.st.col;
304
  if(state.st.type === 'solid') { tEl.style.backgroundColor = state.st.bg; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; tEl.style.borderRadius = '12px'; }
305
  else if (state.st.type === 'transparent') {
306
- let c = state.st.bg.replace('#', '');
307
- if(c.length===6) {
308
- let r = parseInt(c.substring(0, 2), 16); let g = parseInt(c.substring(2, 4), 16); let b = parseInt(c.substring(4, 6), 16);
309
- tEl.style.backgroundColor = `rgba(${r},${g},${b},0.6)`;
310
- } else { tEl.style.backgroundColor = state.st.bg; } // fallback
 
 
 
 
311
  tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; tEl.style.borderRadius = '12px';
312
  } else if (state.st.type === 'outline') {
313
- tEl.style.backgroundColor = 'transparent'; tEl.style.color = state.st.col; const s = Math.max(3, state.st.fz / 4.5); tEl.style.webkitTextStroke = `${s}px ${state.st.bg}`; tEl.style.paintOrder = 'stroke fill'; tEl.style.textShadow = 'none';
314
  } else { tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; }
315
  }
316
  }
317
 
 
 
 
 
 
 
 
 
 
318
  function setStylePreset(name, el, skipModeSet = false) {
319
  state.st.name = name;
320
  document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected'));
321
  if(el) el.classList.add('selected');
 
322
  if(name === 'karaoke_static' && !skipModeSet) {
323
- state.st.col = '#A020F0';
324
- document.documentElement.style.setProperty('--static-color', '#A020F0');
 
325
  }
 
326
  if(name === 'classic' && !skipModeSet) setMode('solid');
327
  else if (name === 'progressive_write') setMode('none');
328
  else if (name === 'plain_white' || name === 'white_outline') {
329
- state.st.col = '#FFFFFF'; state.st.bg = '#000000'; setMode('outline');
 
330
  }
331
  upd();
332
  }
333
 
334
- function handleClassicDoubleClick(element) { event.stopPropagation(); activateCustomStyle(); setStylePreset('classic', element, true); setMode('none'); }
 
 
 
 
 
 
335
  function setFont(f, el) { document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked')); el.classList.add('ticked'); state.st.f = f; upd(); }
336
- function setMode(m) { state.st.type = m; syncModeButtons(); upd(); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // منطق اصلی ادیتور (نمایش ویدیو، سینک متن، ابزارها)
2
+
3
+ // --- Color Picker State & Utils ---
4
+ let pickerState = { h: 0, s: 0, v: 100, a: 100 };
5
+ let currentTarget = null;
6
+ let savedColors = [];
7
+ let isDraggingSpectrum = false;
8
+ let currentDeleteIndex = -1;
9
+ let toastTimeout;
10
+
11
  // --- تابع آپدیت گرافیکی نوار اسلایدر ظاهر ---
12
  function updateRange(input, labelId) {
13
  const label = document.getElementById(labelId);
14
  if (label) label.innerText = input.value + '%';
15
+ const min = parseFloat(input.min);
16
+ const max = parseFloat(input.max);
17
+ const val = parseFloat(input.value);
18
  const percentage = ((val - min) / (max - min)) * 100;
19
  input.style.backgroundSize = percentage + '% 100%';
20
  }
21
 
22
+ // --- هماهنگ‌سازی اسلایدرهای ظاهر با وضعیت ---
23
  function syncUIWithState() {
24
  let fzPercent = ((state.st.fz - 10) / 140) * 100;
25
  const fzInput = document.getElementById('fz');
 
44
  req.onsuccess = (e) => {
45
  const p = e.target.result;
46
  if (!p) { alert('پروژه یافت نشد'); loadHome(); return; }
47
+
48
  state = p.state;
49
  document.getElementById('currentProjectTitle').innerText = p.name;
50
  const videoURL = URL.createObjectURL(p.videoBlob);
51
  v.src = videoURL;
52
 
53
+ // همگام سازی دکمه‌های رنگ با مقادیر ذخیره شده
54
+ updateColorPreviewButtons();
55
+
56
+ // تنظیم اولیه اسلایدرها
57
+ syncUIWithState();
58
 
59
  document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked'));
60
  if(state.st.f === 'vazir') document.querySelectorAll('.font-btn')[1].classList.add('ticked');
61
  else if(state.st.f === 'lalezar') document.querySelectorAll('.font-btn')[0].classList.add('ticked');
62
 
 
63
  document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected'));
64
  if(state.st.name === 'karaoke_static') {
65
+ const staticCard = Array.from(document.querySelectorAll('.style-card')).find(c => c.dataset.styleName === 'karaoke_static');
66
  if(staticCard) staticCard.classList.add('selected');
 
 
 
 
 
67
  }
68
 
69
  document.getElementById('homeScreen').style.display = 'none';
70
  document.getElementById('editorScreen').style.display = 'flex';
71
+
72
+ // Load Color Picker Assets
73
+ initSavedColors();
74
+ const palette = generateGridPalette();
75
+ renderGridWithData(palette);
76
+
77
  renderSegList();
78
  fit();
79
  upd();
80
+
81
+ v.onloadeddata = () => {
82
+ fit();
83
+ if(!p.thumbnail && v.duration > 1) {
84
+ v.currentTime = 1.0;
85
+ let captured = false;
86
+ v.addEventListener('seeked', function cap() {
87
+ if(captured) return; captured = true; saveProjectToDB(); v.currentTime = 0;
88
+ }, {once:true});
89
+ } else if (!p.thumbnail) { saveProjectToDB(); }
90
+ };
91
  };
92
  }
93
 
94
  function goHome() { v.pause(); saveProjectToDB(); loadHome(); }
95
+
96
  function fit() {
97
  if(!state.w) return;
98
  const ws = document.getElementById('workspace');
99
+ const availableHeight = window.innerHeight * 0.6;
100
+ const scale = Math.min((ws.clientWidth - 40) / state.w, availableHeight / state.h);
101
+ const c = document.getElementById('videoContainer');
102
+ c.style.width = state.w + 'px'; c.style.height = state.h + 'px';
103
+ document.getElementById('scaler').style.transform = `scale(${scale})`;
104
+ ws.style.height = (state.h * scale + 40) + 'px';
105
  }
106
 
107
  function activateCustomStyle() {
 
113
  }
114
  }
115
 
116
+ function renderSegList() {
117
+ saveProjectToDB();
118
+ const timeline = document.getElementById('timelineScroll');
119
+ const spacers = timeline.querySelectorAll('.spacer');
120
+ timeline.innerHTML = '';
121
+ timeline.appendChild(spacers[0]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ state.segs.forEach((seg, sIdx) => {
124
+ if (!seg.words || seg.words.length === 0) {
125
+ const wordsArr = seg.text.trim().split(/\s+/).filter(w => w.length > 0);
126
+ const duration = seg.end - seg.start;
127
+ const timePerWord = duration / Math.max(1, wordsArr.length);
128
+ let wStart = seg.start;
129
+ seg.words = wordsArr.map((wStr, i) => {
130
+ let wEnd = wStart + timePerWord;
131
+ if (i === wordsArr.length - 1) wEnd = seg.end;
132
+ let obj = { word: wStr, start: parseFloat(wStart.toFixed(2)), end: parseFloat(wEnd.toFixed(2)) };
133
+ wStart = wEnd;
134
+ return obj;
135
+ });
136
+ }
137
 
138
+ seg.words.forEach((w, wIdx) => {
139
+ const el = document.createElement('div');
140
+ el.className = 'word-chip';
141
+ el.innerText = w.word;
142
+ const uid = `${sIdx}-${wIdx}`;
143
+ el.id = `w-${uid}`;
144
+
145
+ if (activeWordId === uid) el.classList.add('active');
146
 
147
+ el.onclick = (e) => {
148
+ e.stopPropagation();
149
+ highlightWord(sIdx, wIdx, true);
150
+ };
151
+ timeline.appendChild(el);
152
+ });
153
 
154
+ if (sIdx < state.segs.length - 1) {
155
+ const nl = document.createElement('div');
156
+ nl.className = 'newline-indicator';
157
+ nl.innerHTML = '<i class="fa-solid fa-arrow-turn-down" style="transform: rotate(90deg) scaleX(-1);"></i>';
158
+ timeline.appendChild(nl);
159
+ }
160
+ });
161
 
162
+ timeline.appendChild(spacers[1]);
163
+ updateSplitButton();
 
164
  }
165
 
166
+ function highlightWord(sIdx, wIdx, showToolbar) {
167
+ activeWordId = `${sIdx}-${wIdx}`;
168
+ v.pause();
169
+ togglePlayIcon(false);
170
+ const seg = state.segs[sIdx];
171
+ const word = seg.words[wIdx];
172
+ v.currentTime = word.start;
173
+ manualOverride = true;
174
+ updateOverlayContent(v.currentTime);
175
+ document.querySelectorAll('.word-chip').forEach(c => c.classList.remove('active'));
176
+ const el = document.getElementById(`w-${activeWordId}`);
177
+ if(el) {
178
+ el.classList.add('active');
179
+ el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
180
  }
181
+ if(showToolbar) document.getElementById('toolbar').classList.add('show');
182
+ updateSplitButton();
 
183
  }
184
 
185
+ function playPreviewWord() {
186
+ if (!activeWordId) return;
187
+ const [sIdx, wIdx] = activeWordId.split('-').map(Number);
188
+ const word = state.segs[sIdx].words[wIdx];
189
+ if(previewInterval) clearInterval(previewInterval);
190
+ v.pause(); v.currentTime = word.start;
191
+ const icon = document.getElementById('btnPreviewPlay').querySelector('i');
192
+ icon.className = "fa-solid fa-pause";
193
+ v.play();
194
+ previewInterval = setInterval(() => {
195
+ if(v.currentTime >= word.end) {
196
+ v.pause(); clearInterval(previewInterval); icon.className = "fa-solid fa-play";
197
+ if (v.currentTime >= word.end) v.currentTime = word.end - 0.01;
198
+ }
199
+ }, 20);
200
  }
201
 
202
+ function saveText() {
203
+ if (!activeWordId) return;
204
+ const [sIdx, wIdx] = activeWordId.split('-').map(Number);
205
+ const val = document.getElementById('textInput').value.trim();
206
+ if (val) {
207
+ state.segs[sIdx].words[wIdx].word = val;
208
+ state.segs[sIdx].text = state.segs[sIdx].words.map(w => w.word).join(' ');
209
+ renderSegList();
210
+ highlightWord(sIdx, wIdx, true);
211
+ }
212
+ closeAllSheets();
213
  }
 
 
 
 
 
 
214
 
215
+ function adjustTime(type, am) {
216
+ if(type === 'start') {
217
+ let v = parseFloat((tempStartTime + am).toFixed(2));
218
+ if(v < 0) v = 0; if(v >= tempEndTime) v = parseFloat((tempEndTime - 0.1).toFixed(2));
219
+ tempStartTime = v; document.getElementById('valStart').innerText = fmt(tempStartTime);
220
+ } else {
221
+ let v = parseFloat((tempEndTime + am).toFixed(2));
222
+ if(v <= tempStartTime) v = parseFloat((tempStartTime + 0.1).toFixed(2));
223
+ tempEndTime = v; document.getElementById('valEnd').innerText = fmt(tempEndTime);
224
  }
225
  }
226
+
227
+ function confirmTimeChanges() {
228
+ if (!activeWordId) return;
229
+ const [sIdx, wIdx] = activeWordId.split('-').map(Number);
230
+ const seg = state.segs[sIdx];
231
+ seg.words[wIdx].start = tempStartTime;
232
+ seg.words[wIdx].end = tempEndTime;
233
+ if (wIdx === 0 && tempStartTime < seg.start) seg.start = tempStartTime;
234
+ if (wIdx === seg.words.length - 1 && tempEndTime > seg.end) seg.end = tempEndTime;
235
+ closeAllSheets(); renderSegList(); highlightWord(sIdx, wIdx, true);
236
  }
237
+ function cancelTimeChanges() { closeAllSheets(); }
238
 
239
+ function confirmDeleteWord() { /* Renamed to differ from Color Picker Delete */
240
+ if (!activeWordId) return;
241
+ const [sIdx, wIdx] = activeWordId.split('-').map(Number);
242
+ const seg = state.segs[sIdx];
243
+ seg.words.splice(wIdx, 1);
244
+ if (seg.words.length === 0) { state.segs.splice(sIdx, 1); activeWordId = null; document.getElementById('toolbar').classList.remove('show'); }
245
+ else { seg.text = seg.words.map(w => w.word).join(' '); activeWordId = null; document.getElementById('toolbar').classList.remove('show'); }
246
+ closeAllSheets(); renderSegList();
247
+ }
 
 
 
 
 
 
248
 
249
+ function toggleSplit() {
250
+ if (!activeWordId) return;
251
+ const [sIdx, wIdx] = activeWordId.split('-').map(Number);
252
+ if (wIdx === 0 && sIdx > 0) {
253
+ const prevSeg = state.segs[sIdx - 1]; const currSeg = state.segs[sIdx];
254
+ prevSeg.words = prevSeg.words.concat(currSeg.words); prevSeg.end = currSeg.end;
255
+ prevSeg.text = prevSeg.words.map(w => w.word).join(' '); state.segs.splice(sIdx, 1);
256
+ const newWIdx = prevSeg.words.length - currSeg.words.length;
257
+ renderSegList(); highlightWord(sIdx - 1, newWIdx, true);
258
+ } else {
259
+ const seg = state.segs[sIdx]; const wordsFirstHalf = seg.words.slice(0, wIdx); const wordsSecondHalf = seg.words.slice(wIdx);
260
+ seg.words = wordsFirstHalf; seg.text = seg.words.map(w => w.word).join(' '); seg.end = wordsFirstHalf[wordsFirstHalf.length-1].end;
261
+ const newSeg = { text: wordsSecondHalf.map(w => w.word).join(' '), start: wordsSecondHalf[0].start, end: wordsSecondHalf[wordsSecondHalf.length-1].end, words: wordsSecondHalf, isHidden: false };
262
+ state.segs.splice(sIdx + 1, 0, newSeg);
263
+ renderSegList(); highlightWord(sIdx + 1, 0, true);
264
  }
265
  }
266
 
267
+ function updateSplitButton() {
268
+ const btn = document.getElementById('btnSplit'); if(!activeWordId) { btn.classList.remove('active-state'); return; }
269
+ const [sIdx, wIdx] = activeWordId.split('-').map(Number);
270
+ if (wIdx === 0 && sIdx > 0) { btn.classList.add('active-state'); btn.style.transform = "rotate(180deg)"; } else { btn.classList.remove('active-state'); btn.style.transform = ""; }
 
 
 
 
 
 
 
 
271
  }
272
+
273
+ function togglePlay() { if(v.paused) { v.play(); togglePlayIcon(true); } else { v.pause(); togglePlayIcon(false); } }
274
+ function togglePlayIcon(isPlaying) { const overlay = document.getElementById('playOverlay'); overlay.className = isPlaying ? 'playing' : ''; }
275
+
276
+ function updateOverlayContent(currentTime) {
277
+ const idx = state.segs.findIndex(s => currentTime >= s.start && currentTime <= s.end);
278
+ if(idx !== -1) {
279
+ const seg = state.segs[idx];
280
+ if(seg.isHidden) { tEl.style.opacity = 0; }
281
+ else {
282
+ tEl.style.opacity = 1;
283
+ if(state.st.name === 'auto_director' && seg.words) {
284
+ let html = ""; seg.words.forEach((w, i) => {
285
+ let isActive = (currentTime >= w.start && currentTime <= w.end); let boxColor = (i % 2 === 0) ? '#00D7FF' : '#FF0080';
286
+ if(isActive) html += `<span style="background-color: ${boxColor}; color: #ffffff !important; box-shadow: 0 0 15px ${boxColor}; transform: scale(1.1); display:inline-block; border-radius: 6px; padding: 0 6px; text-shadow:none; font-family: inherit;">${w.word}</span> `;
287
+ else html += `<span style="color: #ffffff !important; text-shadow:none; font-family: inherit;">${w.word}</span> `;
288
+ }); tEl.innerHTML = html;
289
+ }
290
+ else if(state.st.name === 'karaoke_static' && seg.words) {
291
+ let html = ""; seg.words.forEach(w => {
292
+ let isActive = (currentTime >= w.start && currentTime <= w.end); let cls = isActive ? "word-active" : "";
293
+ let styleAttr = ""; if(isActive) { let boxColor = state.st.col; styleAttr = `style="background-color: ${boxColor} !important; color: #fff !important; box-shadow: 0 2px 8px ${boxColor};"`; }
294
+ html += `<span class="${cls}" ${styleAttr}>${w.word}</span> `;
295
+ }); tEl.innerHTML = html;
296
+ }
297
+ else if (state.st.name === 'progressive_write' && seg.words) {
298
+ let html = ""; seg.words.forEach(w => { if(currentTime >= w.start) html += `<span style="opacity:1">${w.word}</span> `; else html += `<span style="opacity:0">${w.word}</span> `; }); tEl.innerHTML = html.trim();
299
+ }
300
+ else { tEl.innerText = seg.text; }
301
+ }
302
+ } else { tEl.style.opacity = 0; }
303
  }
304
 
 
 
 
305
  function upd() {
306
  saveProjectToDB();
307
  const uiFz = parseFloat(document.getElementById('fz').value);
 
309
  const uiX = parseFloat(document.getElementById('posX').value);
310
  state.st.fz = Math.round(10 + (uiFz / 100) * 140);
311
  state.st.y = Math.round((uiY / 100) * 1200);
312
+ state.st.x = Math.round((uiX / 100) * 500);
313
 
314
+ updateColorPreviewButtons();
 
 
315
 
316
  let font = 'Vazirmatn';
317
  if(state.st.f === 'lalezar') font = 'Lalezar'; if(state.st.f === 'bangers') font = 'Impact'; if(state.st.f === 'roboto') font = 'Arial';
318
 
319
+ tEl.style.fontFamily = font; tEl.style.fontSize = state.st.fz + 'px'; tEl.style.bottom = state.st.y + 'px';
320
+ tEl.style.textAlign = 'center'; tEl.style.left = '50%'; tEl.style.transform = `translateX(calc(-50% + ${state.st.x}px))`;
321
+ tEl.style.paintOrder = 'normal'; tEl.style.webkitPaintOrder = 'normal'; tEl.style.borderRadius = '0px';
 
 
 
 
322
 
323
  if(state.st.name === 'karaoke_static' || state.st.name === 'auto_director') {
324
  tEl.style.backgroundColor = 'transparent'; tEl.style.color = '#FFFFFF'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none';
325
  } else if (state.st.name === 'plain_white') {
326
  tEl.style.color = '#FFFFFF'; tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none';
327
  } else if (state.st.name === 'white_outline') {
328
+ tEl.style.color = '#FFFFFF'; tEl.style.backgroundColor = 'transparent'; const s = Math.max(3, state.st.fz / 4.5); tEl.style.webkitTextStroke = `${s}px #000000`; tEl.style.paintOrder = 'stroke fill'; tEl.style.webkitPaintOrder = 'stroke fill'; tEl.style.textShadow = 'none';
329
  } else {
330
+ // Handling Colors from state
331
+ // If BG color is transparent from picker logic, we apply rgba
332
+ // To support old hex logic, we convert if needed
333
  tEl.style.color = state.st.col;
334
  if(state.st.type === 'solid') { tEl.style.backgroundColor = state.st.bg; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; tEl.style.borderRadius = '12px'; }
335
  else if (state.st.type === 'transparent') {
336
+ // Apply a slight opacity to whatever bg color is chosen, or parse it
337
+ // Simple heuristic: if it's hex, make it rgba with opacity. If already rgba, leave it.
338
+ let c = state.st.bg;
339
+ if(c.startsWith('#')) {
340
+ let r = parseInt(c.substring(1, 3), 16); let g = parseInt(c.substring(3, 5), 16); let b = parseInt(c.substring(5, 7), 16);
341
+ tEl.style.backgroundColor = `rgba(${r},${g},${b},0.6)`;
342
+ } else {
343
+ tEl.style.backgroundColor = c.replace(/[\d.]+\)$/, '0.6)');
344
+ }
345
  tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; tEl.style.borderRadius = '12px';
346
  } else if (state.st.type === 'outline') {
347
+ tEl.style.backgroundColor = 'transparent'; tEl.style.color = state.st.col; const s = Math.max(3, state.st.fz / 4.5); tEl.style.webkitTextStroke = `${s}px ${state.st.bg}`; tEl.style.paintOrder = 'stroke fill'; tEl.style.webkitPaintOrder = 'stroke fill'; tEl.style.textShadow = 'none';
348
  } else { tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; }
349
  }
350
  }
351
 
352
+ // --- NEW HELPER: Update small colored circles in the accordion ---
353
+ function updateColorPreviewButtons() {
354
+ const mainBtn = document.getElementById('preview-main-btn');
355
+ if(mainBtn) mainBtn.style.backgroundColor = state.st.col;
356
+
357
+ const bgBtn = document.getElementById('preview-bg-btn');
358
+ if(bgBtn) bgBtn.style.backgroundColor = state.st.bg;
359
+ }
360
+
361
  function setStylePreset(name, el, skipModeSet = false) {
362
  state.st.name = name;
363
  document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected'));
364
  if(el) el.classList.add('selected');
365
+
366
  if(name === 'karaoke_static' && !skipModeSet) {
367
+ const defaultPurple = '#A020F0'; state.st.col = defaultPurple;
368
+ document.documentElement.style.setProperty('--static-color', defaultPurple);
369
+ // picker not synced here explicitly, next open will handle
370
  }
371
+
372
  if(name === 'classic' && !skipModeSet) setMode('solid');
373
  else if (name === 'progressive_write') setMode('none');
374
  else if (name === 'plain_white' || name === 'white_outline') {
375
+ state.st.col = '#FFFFFF'; state.st.bg = '#000000';
376
+ setMode('outline');
377
  }
378
  upd();
379
  }
380
 
381
+ function handleClassicDoubleClick(element) {
382
+ event.stopPropagation();
383
+ activateCustomStyle();
384
+ setStylePreset('classic', element, true);
385
+ setMode('none');
386
+ }
387
+
388
  function setFont(f, el) { document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked')); el.classList.add('ticked'); state.st.f = f; upd(); }
389
+ function setMode(m) { state.st.type = m; syncModeButtons(); upd(); }
390
+
391
+ // ============================================
392
+ // ============================================
393
+ // === PRO COLOR PICKER LOGIC ===
394
+ // ============================================
395
+ // ============================================
396
+
397
+ function openColorPicker(target, ev) {
398
+ if (ev) ev.stopPropagation();
399
+ currentTarget = target;
400
+
401
+ document.getElementById('pickerBackdrop').classList.add('active');
402
+ document.getElementById('colorPickerModal').classList.add('active');
403
+
404
+ // Init color from state
405
+ let hex = '#FFFFFF';
406
+ // We treat existing state.st.col/bg as the source.
407
+ // They might be Hex or RGBA strings. We need a parser.
408
+ if(target === 'main') hex = state.st.col;
409
+ else if(target === 'bg') hex = state.st.bg;
410
+ else if(target === 'static') hex = state.st.col; // Reuse text col for static logic as previously defined
411
+
412
+ // Fallback/Convert whatever string is there to valid HSV/A
413
+ let parsed = parseColorStringToState(hex);
414
+ pickerState = parsed;
415
+
416
+ switchTab('spectrum');
417
+ syncAllUI();
418
+ syncGridSelectedFromCurrentColor();
419
+ }
420
+
421
+ function closePicker() {
422
+ document.getElementById('colorPickerModal').classList.remove('active');
423
+ document.getElementById('pickerBackdrop').classList.remove('active');
424
+ }
425
+
426
+ function saveAndClosePicker() {
427
+ // Convert current picker state to Output String (Hex or RGBA if alpha < 100)
428
+ let colorStr;
429
+ const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v);
430
+
431
+ if (pickerState.a >= 100) {
432
+ colorStr = rgbToHex(rgb.r, rgb.g, rgb.b);
433
+ } else {
434
+ colorStr = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${pickerState.a/100})`;
435
+ }
436
+
437
+ if (currentTarget === 'main') {
438
+ state.st.col = colorStr;
439
+ } else if (currentTarget === 'bg') {
440
+ state.st.bg = colorStr;
441
+ } else if (currentTarget === 'static') {
442
+ state.st.col = colorStr;
443
+ document.documentElement.style.setProperty('--static-color', colorStr);
444
+ }
445
+
446
+ upd();
447
+ closePicker();
448
+ }
449
+
450
+ // ... Rest of the picker logic (Tabs, Drag, Calculation) ...
451
+
452
+ const spectrumArea = document.getElementById('spectrumArea');
453
+ const spectrumHandle = document.getElementById('spectrumHandle');
454
+ const slBright = document.getElementById('brightness-slider');
455
+ const slAlpha = document.getElementById('alpha-slider');
456
+ const previewBox = document.getElementById('largeColorPreview');
457
+ const gridCont = document.getElementById('gridContainer');
458
+
459
+ // Tabs
460
+ function switchTab(name) {
461
+ document.querySelectorAll('.picker-tab').forEach(t => t.classList.remove('active'));
462
+ document.getElementById('tab-' + name).classList.add('active');
463
+ document.querySelectorAll('.view-section').forEach(v => v.classList.remove('active-view'));
464
+ document.getElementById('view-' + name).classList.add('active-view');
465
+ // Hide brightness slider in manual to save space? Code sample logic:
466
+ const brRow = document.getElementById('brightness-row');
467
+ if(brRow) brRow.style.display = (name === 'manual') ? 'none' : 'flex';
468
+ syncAllUI();
469
+ if(name === 'grid') syncGridSelectedFromCurrentColor();
470
+ }
471
+
472
+ // Spectrum Handlers
473
+ function handleSpectrum(e) {
474
+ const rect = spectrumArea.getBoundingClientRect();
475
+ let x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
476
+ let y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
477
+ spectrumHandle.style.left = x + 'px';
478
+ spectrumHandle.style.top = y + 'px';
479
+ pickerState.h = (x / rect.width) * 360;
480
+ pickerState.s = 100 - ((y / rect.height) * 100);
481
+ syncAllUI();
482
+ syncGridSelectedFromCurrentColor();
483
+ }
484
+ spectrumArea.addEventListener('mousedown', e => { isDraggingSpectrum = true; handleSpectrum(e); });
485
+ window.addEventListener('mousemove', e => { if(isDraggingSpectrum) handleSpectrum(e); });
486
+ window.addEventListener('mouseup', () => isDraggingSpectrum = false);
487
+ spectrumArea.addEventListener('touchstart', e => { isDraggingSpectrum = true; handleSpectrum(e.touches[0]); }, { passive:false });
488
+ window.addEventListener('touchmove', e => { if(isDraggingSpectrum) { e.preventDefault(); handleSpectrum(e.touches[0]); } }, { passive:false });
489
+ window.addEventListener('touchend', () => isDraggingSpectrum = false);
490
+
491
+ // Global Sliders Update
492
+ function updateBrightness() { pickerState.v = parseInt(slBright.value, 10); syncAllUI(); syncGridSelectedFromCurrentColor(); }
493
+ function updateAlpha() { pickerState.a = parseInt(slAlpha.value, 10); syncAllUI(); }
494
+
495
+ // Manual Inputs Update
496
+ function updateRGBFromSliders() {
497
+ const r = parseInt(document.getElementById('slider-r').value, 10);
498
+ const g = parseInt(document.getElementById('slider-g').value, 10);
499
+ const b = parseInt(document.getElementById('slider-b').value, 10);
500
+ updateFromRGB(r, g, b);
501
+ }
502
+ function updateRGBFromInputs() {
503
+ const r = Math.min(255, Math.max(0, parseInt(document.getElementById('input-r').value, 10) || 0));
504
+ const g = Math.min(255, Math.max(0, parseInt(document.getElementById('input-g').value, 10) || 0));
505
+ const b = Math.min(255, Math.max(0, parseInt(document.getElementById('input-b').value, 10) || 0));
506
+ updateFromRGB(r, g, b);
507
+ }
508
+ function updateFromRGB(r, g, b) {
509
+ const hsv = rgbToHsv(r, g, b);
510
+ pickerState.h = hsv.h; pickerState.s = hsv.s; pickerState.v = hsv.v;
511
+ syncAllUI(); syncGridSelectedFromCurrentColor();
512
+ }
513
+ function updateFromHexInput() {
514
+ let val = document.getElementById('input-hex').value.trim();
515
+ if(val.startsWith('#')) val = val.substring(1);
516
+ if(/^[0-9A-Fa-f]{6}$/.test(val) || /^[0-9A-Fa-f]{3}$/.test(val)) {
517
+ const rgb = hexToRgb('#'+val); const hsv = rgbToHsv(rgb.r,rgb.g,rgb.b);
518
+ pickerState.h = hsv.h; pickerState.s = hsv.s; pickerState.v = hsv.v; pickerState.a = 100;
519
+ syncAllUI(); syncGridSelectedFromCurrentColor();
520
+ }
521
+ }
522
+
523
+ function syncAllUI() {
524
+ const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v);
525
+ const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
526
+ const rgba = `rgba(${rgb.r},${rgb.g},${rgb.b},${pickerState.a/100})`;
527
+
528
+ const slR = document.getElementById('slider-r'), inR = document.getElementById('input-r');
529
+ const slG = document.getElementById('slider-g'), inG = document.getElementById('input-g');
530
+ const slB = document.getElementById('slider-b'), inB = document.getElementById('input-b');
531
+ const inHex = document.getElementById('input-hex');
532
+
533
+ slR.value = inR.value = rgb.r;
534
+ slG.value = inG.value = rgb.g;
535
+ slB.value = inB.value = rgb.b;
536
+ if(document.activeElement !== inHex) inHex.value = hex.replace('#','').toUpperCase();
537
+
538
+ // RGB Gradients
539
+ slR.style.setProperty('--track-bg', `linear-gradient(90deg, rgb(0,${rgb.g},${rgb.b}), rgb(255,${rgb.g},${rgb.b}))`);
540
+ slG.style.setProperty('--track-bg', `linear-gradient(90deg, rgb(${rgb.r},0,${rgb.b}), rgb(${rgb.r},255,${rgb.b}))`);
541
+ slB.style.setProperty('--track-bg', `linear-gradient(90deg, rgb(${rgb.r},${rgb.g},0), rgb(${rgb.r},${rgb.g},255))`);
542
+
543
+ // Bright/Alpha UI
544
+ slBright.value = Math.round(pickerState.v);
545
+ document.getElementById('disp-brightness').innerText = Math.round(pickerState.v);
546
+ const brightColor = hsvToRgb(pickerState.h, pickerState.s, 100);
547
+ slBright.style.setProperty('--track-bg', `linear-gradient(90deg, #000000, rgb(${brightColor.r},${brightColor.g},${brightColor.b}))`);
548
+
549
+ slAlpha.value = Math.round(pickerState.a);
550
+ document.getElementById('disp-alpha').innerText = Math.round(pickerState.a);
551
+ slAlpha.style.setProperty('--track-bg', `linear-gradient(90deg, rgba(${rgb.r},${rgb.g},${rgb.b},0), rgba(${rgb.r},${rgb.g},${rgb.b},1))`);
552
+
553
+ previewBox.style.backgroundColor = rgba;
554
+
555
+ if(!isDraggingSpectrum) {
556
+ spectrumHandle.style.left = (pickerState.h / 360 * 100) + '%';
557
+ spectrumHandle.style.top = (100 - pickerState.s) + '%';
558
+ }
559
+ }
560
+
561
+ // Math Utils
562
+ function rgbToHex(r,g,b) { r=Math.round(r);g=Math.round(g);b=Math.round(b); return "#" + ((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1).toUpperCase(); }
563
+ function hexToRgb(hex) {
564
+ hex = (hex||'#000').replace('#','').trim(); if(hex.length===3) hex=hex.split('').map(c=>c+c).join('');
565
+ if(hex.length!==6) return {r:0,g:0,b:0};
566
+ return {r:parseInt(hex.substring(0,2),16),g:parseInt(hex.substring(2,4),16),b:parseInt(hex.substring(4,6),16)};
567
+ }
568
+ function hsvToRgb(h,s,v){ s/=100;v/=100; let c=v*s; let x=c*(1-Math.abs(((h/60)%2)-1)); let m=v-c; let r=0,g=0,b=0; if(h<60){r=c;g=x;}else if(h<120){r=x;g=c;}else if(h<180){g=c;b=x;}else if(h<240){g=x;b=c;}else if(h<300){r=x;b=c;}else{r=c;b=x;} return {r:Math.round((r+m)*255),g:Math.round((g+m)*255),b:Math.round((b+m)*255)}; }
569
+ function rgbToHsv(r,g,b){ r/=255;g/=255;b/=255; let max=Math.max(r,g,b), min=Math.min(r,g,b), d=max-min, h=0, s=max===0?0:d/max, v=max; if(d!==0){ switch(max){ case r: h=(g-b)/d+(g<b?6:0);break; case g: h=(b-r)/d+2;break; case b: h=(r-g)/d+4;break;} h*=60;} return {h,s:s*100,v:v*100}; }
570
+
571
+ function parseColorStringToState(colorStr) {
572
+ // Takes #ABC, #ABCDEF, rgb(..), rgba(..) and returns HSV+A
573
+ let r=255, g=255, b=255, a=100;
574
+ colorStr = colorStr.trim();
575
+ if(colorStr.startsWith('#')) {
576
+ let rgb = hexToRgb(colorStr); r=rgb.r; g=rgb.g; b=rgb.b;
577
+ } else if(colorStr.startsWith('rgb')) {
578
+ let parts = colorStr.match(/[\d.]+/g);
579
+ if(parts) {
580
+ r = parseFloat(parts[0]); g = parseFloat(parts[1]); b = parseFloat(parts[2]);
581
+ if(parts.length > 3) a = Math.round(parseFloat(parts[3]) * 100);
582
+ }
583
+ }
584
+ const hsv = rgbToHsv(r,g,b);
585
+ return { h:hsv.h, s:hsv.s, v:hsv.v, a:a };
586
+ }
587
+
588
+ // Saved Colors
589
+ function initSavedColors() {
590
+ const s = localStorage.getItem('mySavedColors');
591
+ if(s) { try{savedColors=JSON.parse(s)||[];}catch(e){savedColors=[];} }
592
+ renderSavedColors();
593
+ }
594
+ function saveCurrentColor() {
595
+ const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v);
596
+ const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
597
+ savedColors.push(hex); localStorage.setItem('mySavedColors', JSON.stringify(savedColors));
598
+ renderSavedColors(); showToast();
599
+ }
600
+ function renderSavedColors(){
601
+ const c = document.getElementById('savedColorsContainer');
602
+ // Clear old items (except save btn)
603
+ const svBtn = c.querySelector('.btn-save-text');
604
+ while(svBtn.nextSibling) svBtn.nextSibling.remove();
605
+
606
+ savedColors.forEach((hex, idx) => {
607
+ const w = document.createElement('div'); w.className='saved-color-wrapper';
608
+ w.innerHTML = `<div class="saved-circle" style="background-color:${hex}"></div><div class="mini-delete-btn"><i class="fa-solid fa-xmark"></i></div>`;
609
+ w.querySelector('.saved-circle').onclick = () => { let parsed = parseColorStringToState(hex); pickerState=parsed; syncAllUI(); syncGridSelectedFromCurrentColor(); };
610
+ w.querySelector('.mini-delete-btn').onclick = (e) => { e.stopPropagation(); currentDeleteIndex=idx; document.getElementById('delColorPreview').style.backgroundColor=hex; document.getElementById('deleteModal').classList.add('active'); };
611
+ c.appendChild(w);
612
+ });
613
+ }
614
+ function showToast() { const t=document.getElementById('toastNotification'); t.classList.add('show'); clearTimeout(toastTimeout); toastTimeout=setTimeout(()=>t.classList.remove('show'),2000); }
615
+ function confirmDelete(){ if(currentDeleteIndex>-1) { savedColors.splice(currentDeleteIndex,1); localStorage.setItem('mySavedColors',JSON.stringify(savedColors)); renderSavedColors(); } closeDeleteModal(); }
616
+ function closeDeleteModal(){ document.getElementById('deleteModal').classList.remove('active'); currentDeleteIndex=-1; }
617
+
618
+ // Grid Generation
619
+ function generateGridPalette() {
620
+ const colors = ['#FFFFFF','#F2F2F7','#E5E5EA','#D1D1D6','#C7C7CC','#AEAEB2','#8E8E93','#636366','#48484A','#3A3A3C','#2C2C2E','#1C1C1E','#000000','#FF3B30','#FF453A','#FF9500','#FF9F0A','#FFD60A','#FFCC00','#34C759','#30D158','#00C7BE','#64D2FF','#32ADE6','#0A84FF','#007AFF','#5E5CE6','#5856D6','#AF52DE','#BF5AF2','#FF2D55'];
621
+ for(let i=0;i<=20;i++){ let v=Math.round(i/20*255); colors.push(rgbToHex(v,v,v)); }
622
+ for(let h=0;h<360;h+=12){ for(let s of [92,72]) for(let v of [100,85,70]) { colors.push(rgbToHex(hsvToRgb(h,s,v).r,hsvToRgb(h,s,v).g,hsvToRgb(h,s,v).b)); } }
623
+ return [...new Set(colors)];
624
+ }
625
+ function renderGridWithData(colors){
626
+ gridCont.innerHTML='';
627
+ colors.forEach(hex => {
628
+ const d = document.createElement('div'); d.className='grid-item'; d.style.backgroundColor=hex; d.setAttribute('data-hex', hex.toUpperCase());
629
+ d.onclick=()=>{ document.querySelectorAll('.grid-item').forEach(x=>x.classList.remove('selected')); d.classList.add('selected'); let parsed=parseColorStringToState(hex); pickerState=parsed; syncAllUI(); };
630
+ gridCont.appendChild(d);
631
+ });
632
+ }
633
+ function syncGridSelectedFromCurrentColor() {
634
+ const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v);
635
+ const hex = rgbToHex(rgb.r,rgb.g,rgb.b).toUpperCase();
636
+ const items = gridCont.querySelectorAll('.grid-item');
637
+ items.forEach(it => { if(it.getAttribute('data-hex')===hex) it.classList.add('selected'); else it.classList.remove('selected'); });
638
+ }