cdnhost 2.5.8 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdnhost",
3
- "version": "2.5.8",
3
+ "version": "2.6.0",
4
4
  "description": "cdnhost",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/ws_cdn.js CHANGED
@@ -1,15 +1,9 @@
1
1
 
2
2
  document.addEventListener('DOMContentLoaded', function() {
3
-
4
- const languages = [
5
- { code: 'ko', name: '한국어' }, { code: 'en', name: 'English' }, { code: 'es', name: 'Español' },
6
- { code: 'hi', name: 'हिन्दी' }, { code: 'ar', name: 'العربية' }, { code: 'de', name: 'Deutsch' },
7
- { code: 'fr', name: 'Français' }, { code: 'pt', name: 'Português' }, { code: 'bn', name: 'বাংলা' },
8
- { code: 'ja', name: '日本語' }, { code: 'ru', name: 'Русский' }, { code: 'zh', name: '简体中文' },
9
- { code: 'th', name: 'ไทย' }, { code: 'vi', name: 'Tiếng Việt' }, { code: 'id', name: 'Indonesia' },
10
- { code: 'tr', name: 'Türkçe' }, { code: 'ur', name: 'اردو' }
11
- ];
12
-
3
+ if (window.FHLWidgetInitialized) return;
4
+ window.FHLWidgetInitialized = true;
5
+
6
+ const languages = [ { code: 'ko', name: '한국어' }, { code: 'en', name: 'English' }, { code: 'es', name: 'Español' }, { code: 'hi', name: 'हिन्दी' }, { code: 'ar', name: 'العربية' }, { code: 'de', name: 'Deutsch' }, { code: 'fr', name: 'Français' }, { code: 'pt', name: 'Português' }, { code: 'bn', name: 'বাংলা' }, { code: 'ja', name: '日本語' }, { code: 'ru', name: 'Русский' }, { code: 'zh', name: '简体中文' }, { code: 'th', name: 'ไทย' }, { code: 'vi', name: 'Tiếng Việt' }, { code: 'id', name: 'Indonesia' }, { code: 'tr', name: 'Türkçe' }, { code: 'ur', name: 'اردو' }];
13
7
  let fullApiItemsData = null;
14
8
  let currentDisplayItems = [];
15
9
  let currentPage = 1;
@@ -98,14 +92,7 @@
98
92
  });
99
93
  };
100
94
 
101
- const showToast = (message) => {
102
- clearTimeout(toastTimeout);
103
- toastElement.textContent = message;
104
- toastElement.classList.add('visible');
105
- toastTimeout = setTimeout(() => {
106
- toastElement.classList.remove('visible');
107
- }, 2500);
108
- };
95
+ const showToast = (message) => { clearTimeout(toastTimeout); toastElement.textContent = message; toastElement.classList.add('visible'); toastTimeout = setTimeout(() => { toastElement.classList.remove('visible'); }, 2500); };
109
96
 
110
97
  const applyLanguage = (langCode) => {
111
98
  if (typeof translations === 'undefined') {
@@ -166,32 +153,22 @@
166
153
 
167
154
 
168
155
 
169
-
170
-
171
-
172
-
173
- const createIconItem = (item, type = 'default') => {
156
+ const createIconItem = (item, type = 'default') => {
174
157
  const link = document.createElement('a');
175
158
 
176
- // ✨ [수정됨] 검색 결과 링크에도 GET 파라미터를 추가하도록 로직 변경
177
159
  if (type === 'search') {
178
160
  const paramString = shortcutManager.getAsParam();
179
161
  let finalUrl = item.url;
180
162
  if (paramString) {
181
163
  try {
182
- // URL 객체를 사용하여 기존 파라미터는 유지하고 새 파라미터 추가/수정
183
164
  let newUrl = new URL(item.url);
184
165
  const [key, value] = paramString.split('=');
185
166
  newUrl.searchParams.set(key, value);
186
167
  finalUrl = newUrl.toString();
187
- } catch (e) {
188
- // URL이 유효하지 않은 경우(예: javascript:void(0)) 원본 URL을 그대로 사용
189
- console.error('Invalid URL for search result link:', item.url);
190
- }
168
+ } catch (e) { console.error('Invalid URL for search result link:', item.url); }
191
169
  }
192
170
  link.href = finalUrl;
193
171
  } else {
194
- // 검색 결과가 아닌 다른 아이콘들은 updateAllWidgetLinks 함수가 처리하므로 원본 URL 사용
195
172
  link.href = item.url;
196
173
  }
197
174
 
@@ -200,11 +177,7 @@ const createIconItem = (item, type = 'default') => {
200
177
 
201
178
  if (type === 'search') {
202
179
  link.addEventListener('mousedown', () => {
203
- fetch('update_view_count.php', {
204
- method: 'POST',
205
- headers: { 'Content-Type': 'application/json' },
206
- body: JSON.stringify({ url: item.url })
207
- }).catch(console.error);
180
+ fetch('https://isai.kr/update_view_count.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: item.url }) }).catch(console.error);
208
181
  });
209
182
  link.target = '_blank';
210
183
  }
@@ -304,49 +277,10 @@ const createIconItem = (item, type = 'default') => {
304
277
 
305
278
 
306
279
 
307
- const appendNextPage = () => {
308
- if (isLoadingNextPage) return;
309
- isLoadingNextPage = true;
310
- const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
311
- const endIndex = currentPage * ITEMS_PER_PAGE;
312
- if (startIndex >= currentDisplayItems.length) {
313
- isLoadingNextPage = false;
314
- return;
315
- }
316
- const itemsToAppend = currentDisplayItems.slice(startIndex, endIndex);
317
- itemsToAppend.forEach(item => {
318
- searchResultsContainer.appendChild(createIconItem(item, 'search'));
319
- });
320
- currentPage++;
321
- isLoadingNextPage = false;
322
- };
323
-
324
- const renderInitialResults = (items) => {
325
- searchResultsContainer.innerHTML = '';
326
- currentPage = 1;
327
- currentDisplayItems = items;
328
- appendNextPage();
329
- };
330
-
331
- const openSearch = async () => {
332
- widgetContainer.classList.add('search-mode');
333
- widgetWrapper.classList.add('search-mode');
334
- searchInput.focus();
335
- if (fullApiItemsData === null) {
336
- try {
337
- const response = await fetch('appapi2.php');
338
- if (!response.ok) throw new Error('API 응답 오류');
339
- fullApiItemsData = await response.json();
340
- } catch (error) {
341
- console.error('전체 앱 목록 로드 실패:', error);
342
- searchResultsContainer.innerHTML = '<p>목록 로드 실패</p>';
343
- fullApiItemsData = [];
344
- return;
345
- }
346
- }
347
- renderInitialResults(fullApiItemsData);
348
- };
349
-
280
+ const appendNextPage = () => { if (isLoadingNextPage) return; isLoadingNextPage = true; const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; const endIndex = currentPage * ITEMS_PER_PAGE; if (startIndex >= currentDisplayItems.length) { isLoadingNextPage = false; return; } const itemsToAppend = currentDisplayItems.slice(startIndex, endIndex); itemsToAppend.forEach(item => { searchResultsContainer.appendChild(createIconItem(item, 'search')); }); currentPage++; isLoadingNextPage = false; };
281
+ const renderInitialResults = (items) => { searchResultsContainer.innerHTML = ''; currentPage = 1; currentDisplayItems = items; appendNextPage(); };
282
+ const openSearch = async () => { widgetContainer.classList.add('search-mode'); widgetWrapper.classList.add('search-mode'); searchInput.focus(); if (fullApiItemsData === null) { try { const response = await fetch('https://isai.kr/appapi2.php'); if (!response.ok) throw new Error('API 응답 오류'); fullApiItemsData = await response.json(); } catch (error) { console.error('전체 앱 목록 로드 실패:', error); searchResultsContainer.innerHTML = '<p>목록 로드 실패</p>'; fullApiItemsData = []; return; } } renderInitialResults(fullApiItemsData); };
283
+
350
284
  const initialRender = () => {
351
285
  const t = translations[currentLang] || translations['en'];
352
286
  const initialItemsData = [
@@ -360,120 +294,34 @@ const createIconItem = (item, type = 'default') => {
360
294
  { name: t.psychology, url: 'https://simpong.oduc.kr/', img: 'http://cdn.jsdelivr.net/npm/cdnhost@2.2.0/_simpong.png' },
361
295
  { name: t.ads, url: 'https://gig.snapp.im/', icon: 'ph-currency-circle-dollar' },
362
296
  ];
363
-
364
297
  listContainer.innerHTML = '';
365
298
  const storedItems = shortcutManager.get();
366
299
  let combinedItems = [...initialItemsData];
367
300
  combinedItems.splice(2, 0, ...storedItems);
368
-
369
- combinedItems.forEach(item => {
370
- const isLocal = storedItems.some(stored => stored.url === item.url);
371
- const itemType = isLocal && !item.icon && !item.img ? 'local' : 'default';
372
- listContainer.appendChild(createIconItem(item, itemType));
373
- });
374
-
301
+ combinedItems.forEach(item => { const isLocal = storedItems.some(stored => stored.url === item.url); const itemType = isLocal && !item.icon && !item.img ? 'local' : 'default'; listContainer.appendChild(createIconItem(item, itemType)); });
375
302
  checkScrollability();
376
303
  updateAllWidgetLinks();
377
304
  };
378
305
 
379
- const updateScrollIndicator = () => {
380
- const scrollLeft = listContainer.scrollLeft;
381
- const maxScrollLeft = listContainer.scrollWidth - listContainer.clientWidth;
382
- if (maxScrollLeft <= 0) return;
383
- const scrollFraction = scrollLeft / maxScrollLeft;
384
- const trackWidth = indicatorTrack.clientWidth;
385
- const indicatorWidth = scrollIndicator.clientWidth;
386
- const maxIndicatorLeft = trackWidth - indicatorWidth;
387
- const indicatorLeft = scrollFraction * maxIndicatorLeft;
388
- scrollIndicator.style.transform = `translateY(-50%) translateX(${indicatorLeft}px)`;
389
- };
390
-
391
- const checkScrollability = () => {
392
- const isScrollable = listContainer.scrollWidth > listContainer.clientWidth;
393
- widgetContainer.classList.toggle('scrollable', isScrollable);
394
- if (isScrollable) updateScrollIndicator();
395
- };
396
-
306
+ const updateScrollIndicator = () => { const scrollLeft = listContainer.scrollLeft; const maxScrollLeft = listContainer.scrollWidth - listContainer.clientWidth; if (maxScrollLeft <= 0) return; const scrollFraction = scrollLeft / maxScrollLeft; const trackWidth = indicatorTrack.clientWidth; const indicatorWidth = scrollIndicator.clientWidth; const maxIndicatorLeft = trackWidth - indicatorWidth; const indicatorLeft = scrollFraction * maxIndicatorLeft; scrollIndicator.style.transform = `translateY(-50%) translateX(${indicatorLeft}px)`; };
307
+ const checkScrollability = () => { const isScrollable = listContainer.scrollWidth > listContainer.clientWidth; widgetContainer.classList.toggle('scrollable', isScrollable); if (isScrollable) updateScrollIndicator(); };
397
308
  let isDragging = false;
398
- const handleDragMove = (e) => {
399
- if (!isDragging) return;
400
- e.preventDefault();
401
- const trackRect = indicatorTrack.getBoundingClientRect();
402
- const maxScrollLeft = listContainer.scrollWidth - listContainer.clientWidth;
403
- let positionRatio = (e.clientX - trackRect.left) / trackRect.width;
404
- positionRatio = Math.max(0, Math.min(1, positionRatio));
405
- listContainer.scrollLeft = positionRatio * maxScrollLeft;
406
- };
407
- const handleDragEnd = () => {
408
- if (!isDragging) return;
409
- isDragging = false;
410
- document.removeEventListener('mousemove', handleDragMove);
411
- document.removeEventListener('mouseup', handleDragEnd);
412
- };
413
-
414
- indicatorTrack.addEventListener('mousedown', (e) => {
415
- isDragging = true;
416
- handleDragMove(e);
417
- document.addEventListener('mousemove', handleDragMove);
418
- document.addEventListener('mouseup', handleDragEnd);
419
- });
420
-
421
- const closeSearch = () => {
422
- widgetContainer.classList.remove('search-mode');
423
- widgetWrapper.classList.remove('search-mode');
424
- searchInput.value = '';
425
- searchResultsContainer.innerHTML = '';
426
- };
427
-
309
+ const handleDragMove = (e) => { if (!isDragging) return; e.preventDefault(); const trackRect = indicatorTrack.getBoundingClientRect(); const maxScrollLeft = listContainer.scrollWidth - listContainer.clientWidth; let positionRatio = (e.clientX - trackRect.left) / trackRect.width; positionRatio = Math.max(0, Math.min(1, positionRatio)); listContainer.scrollLeft = positionRatio * maxScrollLeft; };
310
+ const handleDragEnd = () => { if (!isDragging) return; isDragging = false; document.removeEventListener('mousemove', handleDragMove); document.removeEventListener('mouseup', handleDragEnd); };
311
+ indicatorTrack.addEventListener('mousedown', (e) => { isDragging = true; handleDragMove(e); document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); });
312
+ const closeSearch = () => { widgetContainer.classList.remove('search-mode'); widgetWrapper.classList.remove('search-mode'); searchInput.value = ''; searchResultsContainer.innerHTML = ''; };
428
313
  let hideTimeout = null;
429
314
  widgetContainer.addEventListener('mouseenter', () => { clearTimeout(hideTimeout); });
430
315
  widgetContainer.addEventListener('mouseleave', () => { hideTimeout = setTimeout(() => { widgetContainer.classList.add('hidden'); }, 6000); });
431
-
432
- document.addEventListener('click', (e) => {
433
- if (!languagePanel.contains(e.target) && !languageBtn.contains(e.target)) {
434
- languagePanel.classList.remove('visible');
435
- }
436
- if (!widgetContainer.classList.contains('hidden') || widgetContainer.contains(e.target)) {
437
- return;
438
- }
439
- widgetContainer.classList.remove('hidden');
440
- clearTimeout(hideTimeout);
441
- hideTimeout = setTimeout(() => {
442
- widgetContainer.classList.add('hidden');
443
- }, 6000);
444
- });
445
-
316
+ document.addEventListener('click', (e) => { if (!languagePanel.contains(e.target) && !languageBtn.contains(e.target)) { languagePanel.classList.remove('visible'); } if (!widgetContainer.classList.contains('hidden') || widgetContainer.contains(e.target)) { return; } widgetContainer.classList.remove('hidden'); clearTimeout(hideTimeout); hideTimeout = setTimeout(() => { widgetContainer.classList.add('hidden'); }, 6000); });
446
317
  listContainer.addEventListener('scroll', updateScrollIndicator);
447
318
  window.addEventListener('resize', checkScrollability);
448
-
449
319
  searchTrigger.addEventListener('click', openSearch);
450
-
451
- languageBtn.addEventListener('click', (e) => {
452
- e.stopPropagation();
453
- languagePanel.classList.toggle('visible');
454
- });
455
-
456
- searchInput.addEventListener('input', function() {
457
- if (!fullApiItemsData) return;
458
- const searchTerm = this.value.toLowerCase().trim();
459
- const filteredItems = fullApiItemsData.filter(item => item.name.toLowerCase().includes(searchTerm));
460
- renderInitialResults(filteredItems);
461
- });
462
-
463
- searchResultsContainer.addEventListener('scroll', () => {
464
- const isAtBottom = searchResultsContainer.scrollTop + searchResultsContainer.clientHeight >= searchResultsContainer.scrollHeight - 10;
465
- if (isAtBottom) {
466
- appendNextPage();
467
- }
468
- });
469
-
320
+ languageBtn.addEventListener('click', (e) => { e.stopPropagation(); languagePanel.classList.toggle('visible'); });
321
+ searchInput.addEventListener('input', function() { if (!fullApiItemsData) return; const searchTerm = this.value.toLowerCase().trim(); const filteredItems = fullApiItemsData.filter(item => item.name.toLowerCase().includes(searchTerm)); renderInitialResults(filteredItems); });
322
+ searchResultsContainer.addEventListener('scroll', () => { const isAtBottom = searchResultsContainer.scrollTop + searchResultsContainer.clientHeight >= searchResultsContainer.scrollHeight - 10; if (isAtBottom) { appendNextPage(); } });
470
323
  searchClose.addEventListener('click', closeSearch);
471
-
472
- document.addEventListener('keydown', (e) => {
473
- if (e.key === 'Escape' && widgetWrapper.classList.contains('search-mode')) {
474
- closeSearch();
475
- }
476
- });
324
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && widgetWrapper.classList.contains('search-mode')) { closeSearch(); } });
477
325
 
478
326
  const addCurrentPageToWidget = async (e) => {
479
327
  e.preventDefault();
@@ -481,32 +329,19 @@ const createIconItem = (item, type = 'default') => {
481
329
  const baseUrl = getProcessedUrl(window.location.href);
482
330
  let iconName = document.title || t.currentPage;
483
331
  try {
484
- const response = await fetch(`get_title.php?url=${encodeURIComponent(baseUrl)}`);
485
- if (response.ok) {
486
- const data = await response.json();
487
- if (data.title) { iconName = data.title; }
488
- }
332
+ const response = await fetch(`https://isai.kr/get_title.php?url=${encodeURIComponent(baseUrl)}`);
333
+ if (response.ok) { const data = await response.json(); if (data.title) { iconName = data.title; } }
489
334
  } catch (error) { console.error('Failed to fetch title:', error); }
490
-
491
335
  const newIcon = { name: iconName, url: baseUrl };
492
336
  let storedItems = shortcutManager.get();
493
337
  const isAlreadyAdded = storedItems.some(item => getProcessedUrl(item.url) === getProcessedUrl(newIcon.url));
494
-
495
338
  if (!isAlreadyAdded) {
496
- if (storedItems.length >= 10) {
497
- storedItems.shift();
498
- }
339
+ if (storedItems.length >= 10) { storedItems.shift(); }
499
340
  storedItems.push(newIcon);
500
341
  shortcutManager.set(storedItems);
501
-
502
- try {
503
- await fetch('register_app.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newIcon) });
504
- } catch (error) { console.error('DB 등록/갱신 실패:', error); }
505
-
342
+ try { await fetch('https://isai.kr/register_app.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newIcon) }); } catch (error) { console.error('DB 등록/갱신 실패:', error); }
506
343
  initialRender();
507
- if (widgetWrapper.classList.contains('search-mode')) {
508
- closeSearch();
509
- }
344
+ if (widgetWrapper.classList.contains('search-mode')) { closeSearch(); }
510
345
  } else {
511
346
  showToast(t.alreadyAdded);
512
347
  }
@@ -516,41 +351,15 @@ const createIconItem = (item, type = 'default') => {
516
351
  e.preventDefault();
517
352
  const textToCopy = "<script src='https://cdn.jsdelivr.net/npm/cdnhost@latest/ws_cdn.js'><\/script>";
518
353
  const t = translations[currentLang] || translations['en'];
519
-
520
- const fallbackCopy = () => {
521
- const textArea = document.createElement("textarea");
522
- textArea.value = textToCopy;
523
- textArea.style.position = "fixed";
524
- textArea.style.top = 0;
525
- textArea.style.left = "-9999px";
526
- document.body.appendChild(textArea);
527
- textArea.focus();
528
- textArea.select();
529
- try {
530
- document.execCommand('copy');
531
- showToast(t.codeCopied);
532
- } catch (err) {
533
- console.error('Fallback: Oops, unable to copy', err);
534
- }
535
- document.body.removeChild(textArea);
536
- };
537
-
538
- if (navigator.clipboard && window.isSecureContext) {
539
- navigator.clipboard.writeText(textToCopy).then(() => {
540
- showToast(t.codeCopied);
541
- }).catch(() => {
542
- fallbackCopy();
543
- });
544
- } else {
545
- fallbackCopy();
546
- }
354
+ const fallbackCopy = () => { const textArea = document.createElement("textarea"); textArea.value = textToCopy; textArea.style.position = "fixed"; textArea.style.top = 0; textArea.style.left = "-9999px"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); showToast(t.codeCopied); } catch (err) { console.error('Fallback: Oops, unable to copy', err); } document.body.removeChild(textArea); };
355
+ if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(textToCopy).then(() => { showToast(t.codeCopied); }).catch(() => { fallbackCopy(); }); } else { fallbackCopy(); }
547
356
  };
548
357
 
549
358
  addToHomeBtn.addEventListener('click', addCurrentPageToWidget);
550
359
  searchAddBtn.addEventListener('click', copyWidgetScript);
551
360
 
552
361
  shortcutManager.syncFromURL();
553
- shortcutManager.get();
362
+ shortcutManager.get();
554
363
 
555
364
  populateLanguagePanel();
556
365
  applyLanguage(currentLang);
package/ws_cdnhtml.js CHANGED
@@ -5,6 +5,7 @@ const appContainer = document.getElementById('app');
5
5
  // 이렇게 하면 모든 id, class, 속성들이 완벽하게 보존됩니다.
6
6
  const widgetHTML = `
7
7
 
8
+
8
9
  <div id="fhl-toast-notification" class="fhl-toast"></div>
9
10
  <div class="fhl-widget-container" id="fhl-widget-container">
10
11
  <div class="fhl-indicator-track" id="fhl-indicator-track">
@@ -25,8 +26,8 @@ const widgetHTML = `
25
26
  <div class="fhl-search-content">
26
27
  <div class="fhl-search-results" id="fhl-search-results"></div>
27
28
  <div class="fhl-fixed-panel">
28
- <a href="#" id="fhl-search-add-btn" class="fhl-fixed-item"><i class="ph-bold ph-code"></i></a>
29
- <a href="https://isai.kr/#chat" id="fhl-fixed-question" class="fhl-fixed-item"><i class="ph-bold ph-question"></i></a>
29
+ <a href="#" id="fhl-search-add-btn" class="fhl-fixed-item"><i class="ph-bold ph-download-simple"></i></a>
30
+ <a href="#" id="fhl-fixed-image" class="fhl-fixed-item"><i class="ph-bold ph-image"></i></a>
30
31
  <a href="https://zoai.oduc.kr/ko/character/select" id="fhl-fixed-chat" class="fhl-fixed-item"><i class="ph-bold ph-smiley"></i></a>
31
32
  <a href="https://gig.snapp.im/" id="fhl-fixed-ad" class="fhl-fixed-item"><i class="ph-bold ph-megaphone"></i></a>
32
33
  </div>
@@ -36,6 +37,7 @@ const widgetHTML = `
36
37
  <div id="fhl-language-panel" class="fhl-language-panel"></div>
37
38
  </div>
38
39
 
40
+
39
41
  `;
40
42
 
41
43
  // 3. appContainer가 실제로 존재하는지 확인한 후,
package/ws_css.css CHANGED
@@ -1,42 +1,24 @@
1
1
 
2
2
  .fhl-widget-wrapper *, .fhl-widget-wrapper *::before, .fhl-widget-wrapper *::after { box-sizing: border-box; }
3
- .fhl-widget-container {
4
- position: fixed; z-index: 1000; bottom: 20px; left: 50%;
5
- transform: translateX(-50%) translateY(0); display: flex; flex-direction: column; align-items: center;
6
- width: calc(100% - 30px); max-width: 460px; pointer-events: auto;
7
- transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
8
- }
3
+ .fhl-widget-container { position: fixed; z-index: 1000; bottom: 20px; left: 50%; transform: translateX(-50%) translateY(0); display: flex; flex-direction: column; align-items: center; width: calc(100% - 30px); max-width: 460px; pointer-events: auto; transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out; }
9
4
  .fhl-widget-container.hidden { opacity: 0; transform: translateX(-50%) translateY(20px); pointer-events: none; }
10
5
  .fhl-indicator-track { position: relative; width: 100px; height: 14px; margin-bottom: 8px; cursor: pointer; background-color: transparent; transition: opacity 0.3s ease; opacity: 0; pointer-events: auto; }
11
6
  .fhl-widget-container.scrollable .fhl-indicator-track { opacity: 1; }
12
7
  .fhl-widget-container.search-mode .fhl-indicator-track { display: none; }
13
8
  .fhl-scroll-indicator { position: absolute; top: 50%; left: 0; width: 12px; height: 12px; background-color: #555; border-radius: 50%; transform: translateY(-50%); transition: transform 0.1s linear; pointer-events: none; }
14
- .fhl-widget-wrapper {
15
- position: relative; display: flex; align-items: center; width: 100%; max-width: 460px; height: 52px; padding: 7px 10px; border-radius: 26px;
16
- background-color: rgba(255, 255, 255, 0.9); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
17
- box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
18
- transition: all 0.3s ease-in-out; pointer-events: auto;
19
- overflow: visible;
20
- }
9
+ .fhl-widget-wrapper { position: relative; display: flex; align-items: center; width: 100%; max-width: 460px; height: 52px; padding: 7px 10px; border-radius: 26px; background-color: rgba(255, 255, 255, 0.9); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; transition: all 0.3s ease-in-out; pointer-events: auto; overflow: visible; }
21
10
  .fhl-icon-display { position: relative; flex-shrink: 0; display: flex; justify-content: center; align-items: center; width: 38px; height: 38px; color: #333; background-color: rgba(0, 0, 0, 0.05); border: none; border-radius: 50%; text-decoration: none; cursor: pointer; padding: 0; transition: background-color 0.2s ease; }
22
11
  .fhl-icon-display:hover { background-color: rgba(0, 0, 0, 0.1); }
23
12
  .fhl-icon-display i { font-size: 22px; }
24
13
  .fhl-list-container .fhl-icon-display { display: inline-flex; width: auto; padding: 0 8px 0 0; border-radius: 20px; background-color: transparent; }
25
14
  .fhl-list-container .fhl-icon-display:hover { background-color: rgba(0, 0, 0, 0.08); }
26
15
  .fhl-list-container .fhl-icon-display:hover .fhl-icon-circle { background-color: transparent; }
27
- .fhl-item-name {
28
- font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden;
29
- max-width: 0; opacity: 0; transition: max-width 0.3s ease-in-out, opacity 0.2s ease-in-out 0.05s;
30
- }
16
+ .fhl-item-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; max-width: 0; opacity: 0; transition: max-width 0.3s ease-in-out, opacity 0.2s ease-in-out 0.05s; }
31
17
  .fhl-list-container .fhl-icon-display:hover .fhl-item-name { max-width: 150px; opacity: 1; }
32
18
  .fhl-icon-circle { flex-shrink: 0; display: flex; justify-content: center; align-items: center; width: 38px; height: 38px; background-color: rgba(0, 0, 0, 0.05); border-radius: 50%; transition: background-color 0.2s ease; overflow: hidden; }
33
19
  .fhl-icon-circle img { width: 24px; height: 24px; object-fit: cover; border-radius: 50%; }
34
20
  .fhl-icon-bar { display: flex; align-items: center; gap: 8px; width: 100%; transition: opacity 0.3s ease; }
35
- .fhl-list-container {
36
- padding:0 8px; flex-grow: 1; display: flex; align-items: center; gap: 6px; overflow-x: auto; -ms-overflow-style: none; scrollbar-width: none; scroll-snap-type: x mandatory;
37
- mask-image: linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent); -webkit-mask-image: linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent);
38
- padding-bottom: 30px; margin-bottom: -30px; overflow-y: visible;
39
- }
21
+ .fhl-list-container { padding:0 8px; flex-grow: 1; display: flex; align-items: center; gap: 6px; overflow-x: auto; -ms-overflow-style: none; scrollbar-width: none; scroll-snap-type: x mandatory; mask-image: linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent); -webkit-mask-image: linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent); padding-bottom: 30px; margin-bottom: -30px; overflow-y: visible; }
40
22
  .fhl-list-container::-webkit-scrollbar { display: none; }
41
23
  .fhl-list-container > .fhl-icon-display { scroll-snap-align: center; }
42
24
  .fhl-widget-wrapper.search-mode { height: 280px; align-items: flex-start; border-radius: 20px; }
@@ -57,58 +39,13 @@
57
39
  .fhl-fixed-item { display: flex; align-items: center; justify-content: center; width: 42px; height: 42px; border-radius: 10px; text-decoration: none; color: #444; cursor: pointer; transition: background-color 0.2s ease; }
58
40
  .fhl-fixed-item:hover { background-color: rgba(0, 0, 0, 0.08); }
59
41
  .fhl-fixed-item i { font-size: 24px; }
60
- .fhl-item-controls {
61
- position: absolute; bottom: -20px; left: 50%; transform: translateX(-50%); display: flex; gap: 4px;
62
- background-color: rgba(0,0,0,0.7); border-radius: 10px; padding: 2px 4px; opacity: 0; visibility: hidden;
63
- transition: all 0.2s ease; pointer-events: none; z-index: 20;
64
- }
42
+ .fhl-item-controls { position: absolute; bottom: -20px; left: 50%; transform: translateX(-50%); display: flex; gap: 4px; background-color: rgba(0,0,0,0.7); border-radius: 10px; padding: 2px 4px; opacity: 0; visibility: hidden; transition: all 0.2s ease; pointer-events: none; z-index: 20; }
65
43
  .fhl-list-container .fhl-icon-display:hover .fhl-item-controls { opacity: 1; visibility: visible; pointer-events: auto; bottom: -13px; }
66
44
  .fhl-control-btn { background: none; border: none; color: white; cursor: pointer; font-size: 16px; line-height: 1; padding: 2px; font-family: monospace; }
67
45
  .fhl-control-btn:hover { color: #007bff; }
68
- .fhl-toast {
69
- position: fixed; bottom: 90px; left: 50%; transform: translateX(-50%) translateY(10px);
70
- background-color: rgba(0, 0, 0, 0.8); color: #fff; padding: 8px 16px; border-radius: 18px;
71
- font-size: 14px; z-index: 1001; opacity: 0; visibility: hidden; pointer-events: none;
72
- transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s;
73
- }
46
+ .fhl-toast { position: fixed; bottom: 90px; left: 50%; transform: translateX(-50%) translateY(10px); background-color: rgba(0, 0, 0, 0.8); color: #fff; padding: 8px 16px; border-radius: 18px; font-size: 14px; z-index: 1001; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s; }
74
47
  .fhl-toast.visible { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
75
- .fhl-language-panel {
76
- position: absolute;
77
- bottom: 100%;
78
- left: 50%;
79
- transform: translateX(-50%);
80
- margin-bottom: 15px;
81
- background-color: rgba(255, 255, 255, 0.95);
82
- backdrop-filter: blur(10px);
83
- -webkit-backdrop-filter: blur(10px);
84
- border-radius: 12px;
85
- box-shadow: 0 4px 15px rgba(0,0,0,0.2);
86
- padding: 8px;
87
- display: grid;
88
- grid-template-columns: 1fr 1fr;
89
- gap: 4px;
90
- opacity: 0;
91
- visibility: hidden;
92
- pointer-events: none;
93
- transition: opacity 0.3s ease, transform 0.3s ease;
94
- transform-origin: bottom center;
95
- }
96
- .fhl-language-panel.visible {
97
- opacity: 1;
98
- visibility: visible;
99
- pointer-events: auto;
100
- }
101
- .fhl-language-item {
102
- display: block;
103
- padding: 6px 12px;
104
- font-size: 14px;
105
- color: #333;
106
- text-decoration: none;
107
- border-radius: 8px;
108
- transition: background-color 0.2s ease;
109
- cursor: pointer;
110
- text-align: center;
111
- }
112
- .fhl-language-item:hover {
113
- background-color: rgba(0,0,0,0.1);
114
- }
48
+ .fhl-language-panel { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 15px; background-color: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); padding: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 0.3s ease, transform 0.3s ease; transform-origin: bottom center; }
49
+ .fhl-language-panel.visible { opacity: 1; visibility: visible; pointer-events: auto; }
50
+ .fhl-language-item { display: block; padding: 6px 12px; font-size: 14px; color: #333; text-decoration: none; border-radius: 8px; transition: background-color 0.2s ease; cursor: pointer; text-align: center; }
51
+ .fhl-language-item:hover { background-color: rgba(0,0,0,0.1); }
package/ws_lg.js CHANGED
@@ -16,5 +16,5 @@ const translations = {
16
16
  vi: { search: 'Tìm kiếm', question: 'Câu hỏi', forum: 'Diễn đàn', blog: 'Blog', characterChat: 'Trò chuyện nhân vật', translate: 'Dịch', tarot: 'Tarot', psychology: 'Tâm lý học', ads: 'Quảng cáo', openSearch: 'Mở tìm kiếm', addToWidget: 'Thêm vào tiện ích', closeSearch: 'Đóng tìm kiếm', changeLanguage: 'Đổi ngôn ngữ', images: 'Hình ảnh', searchPlaceholder: 'Tìm kiếm...', alreadyAdded: 'Trang này đã được thêm vào tiện ích.', currentPage: 'Trang hiện tại', codeCopied: 'Đã sao chép mã. Vui lòng dán vào trang web của bạn.' },
17
17
  id: { search: 'Cari', question: 'Pertanyaan', forum: 'Forum', blog: 'Blog', characterChat: 'Obrolan Karakter', translate: 'Terjemahkan', tarot: 'Tarot', psychology: 'Psikologi', ads: 'Iklan', openSearch: 'Buka Pencarian', addToWidget: 'Tambah ke Widget', closeSearch: 'Tutup Pencarian', changeLanguage: 'Ubah Bahasa', images: 'Gambar', searchPlaceholder: 'Cari...', alreadyAdded: 'Halaman ini sudah ditambahkan ke widget.', currentPage: 'Halaman Saat Ini', codeCopied: 'Kode disalin. Silakan tempel di situs Anda.' },
18
18
  tr: { search: 'Ara', question: 'Soru', forum: 'Forum', blog: 'Blog', characterChat: 'Karakter Sohbeti', translate: 'Çevir', tarot: 'Tarot', psychology: 'Psikoloji', ads: 'Reklamlar', openSearch: 'Aramayı Aç', addToWidget: 'Widget\'a Ekle', closeSearch: 'Aramayı Kapat', changeLanguage: 'Dili Değiştir', images: 'Görseller', searchPlaceholder: 'Ara...', alreadyAdded: 'Bu sayfa zaten widget\'a eklenmiş.', currentPage: 'Mevcut Sayfa', codeCopied: 'Kod kopyalandı. Lütfen sitenize yapıştırın.' },
19
- ur: { search: 'تلاش', question: 'سوال', forum: 'فورم', blog: 'بلاگ', characterChat: 'کردار چیٹ', translate: 'ترجمہ', tarot: 'ٹیرو', psychology: 'نفسیات', ads: 'اشتہارات', openSearch: 'تلاش کھولیں', addToWidget: 'ویجیٹ میں شامل کریں', closeSearch: 'تلاش بند کریں', changeLanguage: 'زبان تبدیل کریں', images: 'تصاویر', searchPlaceholder: 'تلاش کریں...', alreadyAdded: 'یہ صفحہ پہلے ہی ویجیٹ میں شامل ہے۔', currentPage: 'موجودہ صفحہ', codeCopied: 'کوڈ کاپی ہوگیا ہے۔ براہ کرم اسے اپنی سائٹ پر چسپاں کریں۔' }
19
+ ur: { search: 'تلاش', question: 'سوال', forum: 'فورم', blog: 'بلاگ', characterChat: 'کردار چیٹ', translate: 'ترجمہ', tarot: 'ٹیرو', psychology: 'نفسیات', ads: 'اشتہارات', openSearch: 'تلاش کھولیں', addToWidget: 'ویجیٹ میں شامل کریں', closeSearch: 'تلاش بند کریں', changeLanguage: 'زبان تبدیل کریں', images: 'تصاویر', searchPlaceholder: 'تلاش کریں...', alreadyAdded: 'یہ صفحہ پہلے ہی ویجیট میں شامل ہے۔', currentPage: 'موجودہ صفحہ', codeCopied: 'کوڈ کاپی ہوگیا ہے۔ براہ کرم اسے اپنی سائٹ پر چسپاں کریں۔' }
20
20
  };