cdnhost 2.6.7 → 2.6.8

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.6.7",
3
+ "version": "2.6.8",
4
4
  "description": "cdnhost",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/ws_cdn.js CHANGED
@@ -1,271 +1 @@
1
-
2
- document.addEventListener('DOMContentLoaded', function() {
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: 'اردو' }];
7
-
8
- let currentDisplayItems = [];
9
- let currentPage = 1;
10
- const ITEMS_PER_PAGE = 30;
11
- let isLoadingNextPage = false;
12
- let searchTimeout = null;
13
-
14
- const widgetContainer = document.getElementById('fhl-widget-container');
15
- const widgetWrapper = document.getElementById('fhl-widget-wrapper');
16
- const listContainer = document.getElementById('fhl-list-container');
17
- const searchInput = document.getElementById('fhl-search-input');
18
- const searchResultsContainer = document.getElementById('fhl-search-results');
19
- const searchTrigger = document.getElementById('fhl-search-trigger');
20
- const searchClose = document.getElementById('fhl-search-close');
21
- const indicatorTrack = document.getElementById('fhl-indicator-track');
22
- const scrollIndicator = document.getElementById('fhl-scroll-indicator');
23
- const addToHomeBtn = document.getElementById('fhl-add-to-home-btn');
24
- const searchAddBtn = document.getElementById('fhl-search-add-btn');
25
- const toastElement = document.getElementById('fhl-toast-notification');
26
- const languageBtn = document.getElementById('fhl-language-btn');
27
- const languagePanel = document.getElementById('fhl-language-panel');
28
-
29
- const fixedQuestionBtn = document.getElementById('fhl-fixed-question');
30
- const fixedChatBtn = document.getElementById('fhl-fixed-chat');
31
- const fixedAdBtn = document.getElementById('fhl-fixed-ad');
32
- const fixedInfoBtn = document.getElementById('fhl-fixed-info');
33
-
34
- let toastTimeout = null;
35
- let currentLang = localStorage.getItem('fhl-widget-lang') || 'ko';
36
- const API_BASE_URL = 'https://isai.kr';
37
-
38
- const shortcutManager = {
39
- key: 'fhl-custom-icons-with-expiry',
40
- get: () => {
41
- const rawData = localStorage.getItem(shortcutManager.key);
42
- if (!rawData) return [];
43
- try {
44
- const data = JSON.parse(rawData);
45
- const thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000;
46
- if (Date.now() - data.timestamp > thirtyDaysInMs) {
47
- localStorage.removeItem(shortcutManager.key);
48
- return [];
49
- }
50
- return data.shortcuts || [];
51
- } catch (e) {
52
- localStorage.removeItem(shortcutManager.key);
53
- return [];
54
- }
55
- },
56
- set: (shortcuts) => {
57
- const dataToStore = { timestamp: Date.now(), shortcuts: shortcuts };
58
- localStorage.setItem(shortcutManager.key, JSON.stringify(dataToStore));
59
- },
60
- syncFromURL: () => {
61
- const params = new URLSearchParams(window.location.search);
62
- const shortcutsParam = params.get('shortcuts');
63
- if (shortcutsParam) {
64
- try {
65
- const decodedData = atob(shortcutsParam);
66
- JSON.parse(decodedData);
67
- localStorage.setItem(shortcutManager.key, decodedData);
68
- } catch (e) { console.error("Failed to parse shortcuts from URL:", e); }
69
- }
70
- },
71
- getAsParam: () => {
72
- const rawData = localStorage.getItem(shortcutManager.key);
73
- if (!rawData) return '';
74
- return `shortcuts=${btoa(rawData)}`;
75
- }
76
- };
77
-
78
- const updateAllWidgetLinks = () => {
79
- const paramString = shortcutManager.getAsParam();
80
- if (!paramString) return;
81
- const allLinks = document.querySelectorAll('#fhl-widget-container .fhl-icon-display, #fhl-widget-container .fhl-fixed-item');
82
- allLinks.forEach(link => {
83
- if (link.tagName === 'A') {
84
- const originalHref = link.getAttribute('href');
85
- if (!originalHref || originalHref.startsWith('#')) return;
86
- try {
87
- let newUrl = new URL(originalHref);
88
- const [key, value] = paramString.split('=');
89
- newUrl.searchParams.set(key, value);
90
- link.href = newUrl.toString();
91
- } catch (e) { console.error('Invalid URL for link update:', originalHref); }
92
- }
93
- });
94
- };
95
-
96
- const showToast = (message) => { clearTimeout(toastTimeout); toastElement.textContent = message; toastElement.classList.add('visible'); toastTimeout = setTimeout(() => { toastElement.classList.remove('visible'); }, 2500); };
97
-
98
- const applyLanguage = (langCode) => {
99
- if (typeof translations === 'undefined') { console.error("Translations not loaded! Make sure lang.js is included correctly."); return; }
100
- const t = translations[langCode] || translations['en'];
101
- document.documentElement.lang = langCode;
102
- searchTrigger.setAttribute('aria-label', t.openSearch);
103
- addToHomeBtn.setAttribute('aria-label', t.addToWidget);
104
- searchClose.setAttribute('aria-label', t.closeSearch);
105
- languageBtn.setAttribute('aria-label', t.changeLanguage);
106
- searchAddBtn.setAttribute('aria-label', 'Copy Widget Code');
107
- fixedQuestionBtn.setAttribute('aria-label', t.question);
108
- fixedChatBtn.setAttribute('aria-label', t.characterChat);
109
- fixedAdBtn.setAttribute('aria-label', t.ads);
110
- fixedInfoBtn.setAttribute('aria-label', t.info);
111
- searchInput.placeholder = t.searchPlaceholder;
112
- initialRender();
113
- };
114
-
115
- const populateLanguagePanel = () => { languagePanel.innerHTML = ''; languages.forEach(lang => { const langItem = document.createElement('a'); langItem.href = '#'; langItem.className = 'fhl-language-item'; langItem.textContent = lang.name; langItem.dataset.lang = lang.code; langItem.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const newLang = e.target.dataset.lang; if (newLang !== currentLang) { currentLang = newLang; localStorage.setItem('fhl-widget-lang', currentLang); applyLanguage(currentLang); } languagePanel.classList.remove('visible'); }); languagePanel.appendChild(langItem); }); };
116
- const getProcessedUrl = (fullUrl) => { try { const urlObj = new URL(fullUrl); return `${urlObj.protocol}//${urlObj.hostname}`; } catch (e) { return fullUrl; } };
117
-
118
- const createIconItem = (item, type = 'default') => {
119
- const link = document.createElement('a');
120
- if (type === 'search') {
121
- const paramString = shortcutManager.getAsParam();
122
- let finalUrl = item.url;
123
- if (paramString) { try { let newUrl = new URL(item.url); const [key, value] = paramString.split('='); newUrl.searchParams.set(key, value); finalUrl = newUrl.toString(); } catch (e) { console.error('Invalid URL for search result link:', item.url); } }
124
- link.href = finalUrl;
125
- } else { link.href = item.url; }
126
- link.className = 'fhl-icon-display';
127
- link.setAttribute('aria-label', item.name);
128
- if (type === 'search') { link.addEventListener('mousedown', () => { fetch(`${API_BASE_URL}/update_view_count.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: item.url }) }).catch(console.error); }); link.target = '_blank'; }
129
- const iconCircle = document.createElement('div'); iconCircle.className = 'fhl-icon-circle';
130
- if (item.icon) { const icon = document.createElement('i'); icon.className = `ph-bold ${item.icon}`; iconCircle.appendChild(icon); }
131
- else if (item.img) { const customImage = document.createElement('img'); customImage.src = item.img; customImage.alt = item.name; customImage.onerror = () => { customImage.style.display = 'none'; }; iconCircle.appendChild(customImage); }
132
- else { const favicon = document.createElement('img'); favicon.src = `https://www.google.com/s2/favicons?sz=64&domain_url=${item.url}`; favicon.alt = item.name; favicon.onerror = () => { favicon.style.display = 'none'; }; iconCircle.appendChild(favicon); }
133
- link.appendChild(iconCircle);
134
- if (type === 'search') {
135
- const tooltip = document.createElement('div'); tooltip.className = 'fhl-item-tooltip'; tooltip.textContent = item.name.length > 5 ? item.name.slice(0, 5) + '..' : item.name; link.appendChild(tooltip);
136
- link.addEventListener('mouseenter', (e) => { const currentTooltip = e.currentTarget.querySelector('.fhl-item-tooltip'); if (!currentTooltip) return; currentTooltip.classList.add('visible'); const widgetRect = widgetWrapper.getBoundingClientRect(); const tooltipRect = currentTooltip.getBoundingClientRect(); let offsetX = 0; const padding = 5; if (tooltipRect.left < widgetRect.left) { offsetX = widgetRect.left - tooltipRect.left + padding; } else if (tooltipRect.right > widgetRect.right) { offsetX = widgetRect.right - tooltipRect.right - padding; } if (offsetX !== 0) { currentTooltip.style.transform = `translateX(calc(-50% + ${offsetX}px))`; } });
137
- link.addEventListener('mouseleave', (e) => { const currentTooltip = e.currentTarget.querySelector('.fhl-item-tooltip'); if (!currentTooltip) return; currentTooltip.classList.remove('visible'); currentTooltip.style.transform = 'translateX(-50%)'; });
138
- } else { const nameSpan = document.createElement('span'); nameSpan.className = 'fhl-item-name'; nameSpan.textContent = item.name.length > 5 ? item.name.slice(0, 5) + '..' : item.name; link.appendChild(nameSpan); }
139
- if (type === 'local') {
140
- const controls = document.createElement('div'); controls.className = 'fhl-item-controls';
141
- const deleteBtn = document.createElement('button'); deleteBtn.className = 'fhl-control-btn'; deleteBtn.innerHTML = '&times;'; deleteBtn.title = '삭제';
142
- deleteBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); let storedItems = shortcutManager.get(); storedItems = storedItems.filter(stored => stored.url !== item.url); shortcutManager.set(storedItems); initialRender(); });
143
- const moveBtn = document.createElement('button'); moveBtn.className = 'fhl-control-btn'; moveBtn.innerHTML = '&gt;'; moveBtn.title = '오른쪽으로 이동';
144
- moveBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); let storedItems = shortcutManager.get(); const currentIndex = storedItems.findIndex(stored => stored.url === item.url); if (currentIndex > -1) { const newIndex = (currentIndex + 1) % storedItems.length; const [movedItem] = storedItems.splice(currentIndex, 1); storedItems.splice(newIndex, 0, movedItem); shortcutManager.set(storedItems); initialRender(); } });
145
- controls.appendChild(deleteBtn); controls.appendChild(moveBtn); link.appendChild(controls);
146
- }
147
- return link;
148
- };
149
-
150
- 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; };
151
-
152
- const renderInitialResults = (items) => {
153
- searchResultsContainer.innerHTML = '';
154
- currentPage = 1;
155
- currentDisplayItems = items;
156
- appendNextPage();
157
- };
158
-
159
- const performSearch = async (query = '') => {
160
- try {
161
- const response = await fetch(`${API_BASE_URL}/appapi2.php?query=${encodeURIComponent(query)}`);
162
- if (!response.ok) throw new Error('API 응답 오류');
163
- const results = await response.json();
164
- renderInitialResults(results);
165
- } catch (error) {
166
- console.error('앱 목록 로드 실패:', error);
167
- searchResultsContainer.innerHTML = '<p>목록 로드 실패</p>';
168
- }
169
- };
170
-
171
- const openSearch = () => {
172
- widgetContainer.classList.add('search-mode');
173
- widgetWrapper.classList.add('search-mode');
174
- searchInput.focus();
175
- performSearch();
176
- };
177
-
178
- const initialRender = () => {
179
- const t = translations[currentLang] || translations['en'];
180
- const b = [
181
- { name: t.search, url: 'https://isai.kr', icon: 'ph-sparkle' },
182
- { name: t.question, url: 'https://isai.kr/#chat', icon: 'ph-question-mark' },
183
- { name: t.characterChat, url: 'https://zoai.oduc.kr/ko/character/select', icon: 'ph-chats-circle' },
184
- { name: t.translate, url: 'https://translato.isai.kr/', icon: 'ph-translate' },
185
- { name: t.tarot, url: 'https://tarot.isai.kr/', icon: 'ph-cards' },
186
- { name: t.blog, url: 'https://blog.099.kr', icon: 'ph-article-medium' },
187
- { name: t.novel, url: 'https://ranovel.kr/', img: 'https://cdn.jsdelivr.net/npm/cdnhost@2.2.2/__ra.png' },
188
- { name: t.psychology, url: 'https://simpong.oduc.kr/', img: 'http://cdn.jsdelivr.net/npm/cdnhost@2.2.0/_simpong.png' },
189
- { name: t.forum, url: 'https://logig.im', img: 'https://cdn.jsdelivr.net/npm/cdnhost@2.3.9/_______________logig.png' },
190
- { name: t.ads, url: 'https://gig.snapp.im/', icon: 'ph-currency-circle-dollar' },
191
- ];
192
- listContainer.innerHTML = '';
193
- const c = shortcutManager.get();
194
- let d = [...b];
195
- d.splice(2, 0, ...c);
196
- d.forEach(e => {
197
- const f = c.some(g => g.url === e.url);
198
- const h = f && !e.icon && !e.img ? 'local' : 'default';
199
- listContainer.appendChild(createIconItem(e, h));
200
- });
201
- checkScrollability();
202
- updateAllWidgetLinks();
203
- };
204
-
205
- 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)`; };
206
- const checkScrollability = () => { const isScrollable = listContainer.scrollWidth > listContainer.clientWidth; widgetContainer.classList.toggle('scrollable', isScrollable); if (isScrollable) updateScrollIndicator(); };
207
- let isDragging = false;
208
- 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; };
209
- const handleDragEnd = () => { if (!isDragging) return; isDragging = false; document.removeEventListener('mousemove', handleDragMove); document.removeEventListener('mouseup', handleDragEnd); };
210
- indicatorTrack.addEventListener('mousedown', (e) => { isDragging = true; handleDragMove(e); document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); });
211
- const closeSearch = () => { widgetContainer.classList.remove('search-mode'); widgetWrapper.classList.remove('search-mode'); searchInput.value = ''; searchResultsContainer.innerHTML = ''; };
212
- let hideTimeout = null;
213
- widgetContainer.addEventListener('mouseenter', () => { clearTimeout(hideTimeout); });
214
- widgetContainer.addEventListener('mouseleave', () => { hideTimeout = setTimeout(() => { widgetContainer.classList.add('hidden'); }, 6000); });
215
-
216
- document.addEventListener('click', (e) => {
217
- if (!languagePanel.contains(e.target) && !languageBtn.contains(e.target)) {
218
- languagePanel.classList.remove('visible');
219
- }
220
- if (widgetContainer.contains(e.target)) {
221
- return;
222
- }
223
- if (widgetContainer.classList.contains('hidden')) {
224
- setTimeout(() => {
225
- widgetContainer.classList.remove('hidden');
226
- clearTimeout(hideTimeout);
227
- hideTimeout = setTimeout(() => {
228
- widgetContainer.classList.add('hidden');
229
- }, 6000);
230
- }, 50);
231
- }
232
- });
233
-
234
- listContainer.addEventListener('scroll', updateScrollIndicator);
235
- window.addEventListener('resize', checkScrollability);
236
- searchTrigger.addEventListener('click', openSearch);
237
- languageBtn.addEventListener('click', (e) => { e.stopPropagation(); languagePanel.classList.toggle('visible'); });
238
-
239
- searchInput.addEventListener('input', function() {
240
- clearTimeout(searchTimeout);
241
- const query = this.value.toLowerCase().trim();
242
- searchTimeout = setTimeout(() => {
243
- performSearch(query);
244
- }, 300);
245
- });
246
-
247
- searchResultsContainer.addEventListener('scroll', () => { const isAtBottom = searchResultsContainer.scrollTop + searchResultsContainer.clientHeight >= searchResultsContainer.scrollHeight - 10; if (isAtBottom) { appendNextPage(); } });
248
- searchClose.addEventListener('click', closeSearch);
249
- document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && widgetWrapper.classList.contains('search-mode')) { closeSearch(); } });
250
-
251
-
252
- const _0x3e4a = ['preventDefault', 'en', 'currentPage', 'get_title.php?url=', 'title', 'Failed to fetch title:', 'name', 'url', 'some', 'shift', 'push', 'POST', 'Content-Type', 'application/json', 'register_app.php', 'DB 등록/갱신 실패:', 'search-mode', 'alreadyAdded', 'json', 'ok', 'error', 'classList', 'contains', 'log']; (function(_0x1a8c3d, _0x3e4a3f) { const _0x58ab4a = function(_0x2e8f1c) { while (--_0x2e8f1c) { _0x1a8c3d['push'](_0x1a8c3d['shift']()); } }; _0x58ab4a(++_0x3e4a3f); }(_0x3e4a, 0x18a)); const _0x5a8d = function(_0x1a8c3d, _0x3e4a3f) { _0x1a8c3d = _0x1a8c3d - 0x0; let _0x58ab4a = _0x3e4a[_0x1a8c3d]; return _0x58ab4a; }; const addCurrentPageToWidget = async (_0x1b4c5d) => { _0x1b4c5d[_0x5a8d('0x0')](); const _0x5a7b8e = translations[currentLang] || translations[_0x5a8d('0x1')]; const _0x2a9e3f = getProcessedUrl(window.location.href); let _0x4d2a1c = document[_0x5a8d('0x4')] || _0x5a7b8e[_0x5a8d('0x2')]; try { const _0x4f12f8 = await fetch(`${API_BASE_URL}/${_0x5a8d('0x3')}${encodeURIComponent(_0x2a9e3f)}`); if (_0x4f12f8[_0x5a8d('0x13')]) { const _0x39f2c3 = await _0x4f12f8[_0x5a8d('0x12')](); if (_0x39f2c3[_0x5a8d('0x4')]) { _0x4d2a1c = _0x39f2c3[_0x5a8d('0x4')]; } } } catch (_0x5c4d5d) { console[_0x5a8d('0x14')](_0x5a8d('0x5'), _0x5c4d5d); } const _0x4d5b2e = { [_0x5a8d('0x6')]: _0x4d2a1c, [_0x5a8d('0x7')]: _0x2a9e3f }; let _0x2b8e4f = shortcutManager.get(); if (!_0x2b8e4f[_0x5a8d('0x8')]((_0x5b3e9a) => getProcessedUrl(_0x5b3e9a[_0x5a8d('0x7')]) === getProcessedUrl(_0x4d5b2e[_0x5a8d('0x7')]))) { if (_0x2b8e4f.length >= 0xa) { _0x2b8e4f[_0x5a8d('0x9')](); } _0x2b8e4f[_0x5a8d('0xa')](_0x4d5b2e); shortcutManager.set(_0x2b8e4f); try { await fetch(`${API_BASE_URL}/${_0x5a8d('0xe')}`, { 'method': _0x5a8d('0xb'), 'headers': { [_0x5a8d('0xc')]: _0x5a8d('0xd') }, 'body': JSON.stringify(_0x4d5b2e) }); } catch (_0x3e1b1b) { console[_0x5a8d('0x14')](_0x5a8d('0xf'), _0x3e1b1b); } initialRender(); if (widgetWrapper[_0x5a8d('0x15')][_0x5a8d('0x16')](_0x5a8d('0x10'))) { closeSearch(); } } else { showToast(_0x5a7b8e[_0x5a8d('0x11')]); } };
253
-
254
-
255
- const copyWidgetScript = (e) => {
256
- e.preventDefault();
257
- const textToCopy = '<script src="https://unpkg.com/@phosphor-icons/web"><\/script> <script src="https://cdn.jsdelivr.net/npm/cdnhost@latest/ws_lg.js"><\/script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cdnhost@latest/ws_css.css"> <div id="app"></div> <script src="https://cdn.jsdelivr.net/npm/cdnhost@latest/ws_cdnhtml.js"><\/script> <script src="https://cdn.jsdelivr.net/npm/cdnhost@latest/ws_cdn.js"><\/script>';
258
- const t = translations[currentLang] || translations['en'];
259
- 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); };
260
- if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(textToCopy).then(() => { showToast(t.codeCopied); }).catch(() => { fallbackCopy(); }); } else { fallbackCopy(); }
261
- };
262
-
263
- addToHomeBtn.addEventListener('click', addCurrentPageToWidget);
264
- searchAddBtn.addEventListener('click', copyWidgetScript);
265
-
266
- shortcutManager.syncFromURL();
267
- shortcutManager.get();
268
-
269
- populateLanguagePanel();
270
- applyLanguage(currentLang);
271
- });
1
+ document.addEventListener('DOMContentLoaded', function() { if (window.FHLWidgetInitialized) return; window.FHLWidgetInitialized = true; 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: 'اردو' }]; let currentDisplayItems = []; let currentPage = 1; const ITEMS_PER_PAGE = 30; let isLoadingNextPage = false; let searchTimeout = null; const widgetContainer = document.getElementById('fhl-widget-container'); const widgetWrapper = document.getElementById('fhl-widget-wrapper'); const listContainer = document.getElementById('fhl-list-container'); const searchInput = document.getElementById('fhl-search-input'); const searchResultsContainer = document.getElementById('fhl-search-results'); const searchTrigger = document.getElementById('fhl-search-trigger'); const searchClose = document.getElementById('fhl-search-close'); const indicatorTrack = document.getElementById('fhl-indicator-track'); const scrollIndicator = document.getElementById('fhl-scroll-indicator'); const addToHomeBtn = document.getElementById('fhl-add-to-home-btn'); const searchAddBtn = document.getElementById('fhl-search-add-btn'); const toastElement = document.getElementById('fhl-toast-notification'); const languageBtn = document.getElementById('fhl-language-btn'); const languagePanel = document.getElementById('fhl-language-panel'); const fixedQuestionBtn = document.getElementById('fhl-fixed-question'); const fixedChatBtn = document.getElementById('fhl-fixed-chat'); const fixedAdBtn = document.getElementById('fhl-fixed-ad'); const fixedInfoBtn = document.getElementById('fhl-fixed-info'); let toastTimeout = null; let currentLang = localStorage.getItem('fhl-widget-lang') || 'ko'; const API_BASE_URL = 'https://isai.kr'; const shortcutManager = { key: 'fhl-custom-icons-with-expiry', get: () => { const rawData = localStorage.getItem(shortcutManager.key); if (!rawData) return []; try { const data = JSON.parse(rawData); const thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000; if (Date.now() - data.timestamp > thirtyDaysInMs) { localStorage.removeItem(shortcutManager.key); return []; } return data.shortcuts || []; } catch (e) { localStorage.removeItem(shortcutManager.key); return []; } }, set: (shortcuts) => { const dataToStore = { timestamp: Date.now(), shortcuts: shortcuts }; localStorage.setItem(shortcutManager.key, JSON.stringify(dataToStore)); }, syncFromURL: () => { const params = new URLSearchParams(window.location.search); const shortcutsParam = params.get('shortcuts'); if (shortcutsParam) { try { const decodedData = atob(shortcutsParam); JSON.parse(decodedData); localStorage.setItem(shortcutManager.key, decodedData); } catch (e) { console.error("Failed to parse shortcuts from URL:", e); } } }, getAsParam: () => { const rawData = localStorage.getItem(shortcutManager.key); if (!rawData) return ''; return `shortcuts=${btoa(rawData)}`; } }; const updateAllWidgetLinks = () => { const paramString = shortcutManager.getAsParam(); if (!paramString) return; const allLinks = document.querySelectorAll('#fhl-widget-container .fhl-icon-display, #fhl-widget-container .fhl-fixed-item'); allLinks.forEach(link => { if (link.tagName === 'A') { const originalHref = link.getAttribute('href'); if (!originalHref || originalHref.startsWith('#')) return; try { let newUrl = new URL(originalHref); const [key, value] = paramString.split('='); newUrl.searchParams.set(key, value); link.href = newUrl.toString(); } catch (e) { console.error('Invalid URL for link update:', originalHref); } } }); }; const showToast = (message) => { clearTimeout(toastTimeout); toastElement.textContent = message; toastElement.classList.add('visible'); toastTimeout = setTimeout(() => { toastElement.classList.remove('visible'); }, 2500); }; const applyLanguage = (langCode) => { if (typeof translations === 'undefined') { console.error("Translations not loaded! Make sure lang.js is included correctly."); return; } const t = translations[langCode] || translations['en']; document.documentElement.lang = langCode; searchTrigger.setAttribute('aria-label', t.openSearch); addToHomeBtn.setAttribute('aria-label', t.addToWidget); searchClose.setAttribute('aria-label', t.closeSearch); languageBtn.setAttribute('aria-label', t.changeLanguage); searchAddBtn.setAttribute('aria-label', 'Copy Widget Code'); fixedQuestionBtn.setAttribute('aria-label', t.question); fixedChatBtn.setAttribute('aria-label', t.characterChat); fixedAdBtn.setAttribute('aria-label', t.ads); fixedInfoBtn.setAttribute('aria-label', t.info); searchInput.placeholder = t.searchPlaceholder; initialRender(); }; const populateLanguagePanel = () => { languagePanel.innerHTML = ''; languages.forEach(lang => { const langItem = document.createElement('a'); langItem.href = '#'; langItem.className = 'fhl-language-item'; langItem.textContent = lang.name; langItem.dataset.lang = lang.code; langItem.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const newLang = e.target.dataset.lang; if (newLang !== currentLang) { currentLang = newLang; localStorage.setItem('fhl-widget-lang', currentLang); applyLanguage(currentLang); } languagePanel.classList.remove('visible'); }); languagePanel.appendChild(langItem); }); }; const getProcessedUrl = (fullUrl) => { try { const urlObj = new URL(fullUrl); return `${urlObj.protocol}//${urlObj.hostname}`; } catch (e) { return fullUrl; } }; const createIconItem = (item, type = 'default') => { const link = document.createElement('a'); if (type === 'search') { const paramString = shortcutManager.getAsParam(); let finalUrl = item.url; if (paramString) { try { let newUrl = new URL(item.url); const [key, value] = paramString.split('='); newUrl.searchParams.set(key, value); finalUrl = newUrl.toString(); } catch (e) { console.error('Invalid URL for search result link:', item.url); } } link.href = finalUrl; } else { link.href = item.url; } link.className = 'fhl-icon-display'; link.setAttribute('aria-label', item.name); if (type === 'search') { link.addEventListener('mousedown', () => { fetch(`${API_BASE_URL}/update_view_count.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: item.url }) }).catch(console.error); }); link.target = '_blank'; } const iconCircle = document.createElement('div'); iconCircle.className = 'fhl-icon-circle'; if (item.icon) { const icon = document.createElement('i'); icon.className = `ph-bold ${item.icon}`; iconCircle.appendChild(icon); } else if (item.img) { const customImage = document.createElement('img'); customImage.src = item.img; customImage.alt = item.name; customImage.onerror = () => { customImage.style.display = 'none'; }; iconCircle.appendChild(customImage); } else { const favicon = document.createElement('img'); favicon.src = `https://www.google.com/s2/favicons?sz=64&domain_url=${item.url}`; favicon.alt = item.name; favicon.onerror = () => { favicon.style.display = 'none'; }; iconCircle.appendChild(favicon); } link.appendChild(iconCircle); if (type === 'search') { const tooltip = document.createElement('div'); tooltip.className = 'fhl-item-tooltip'; tooltip.textContent = item.name.length > 5 ? item.name.slice(0, 5) + '..' : item.name; link.appendChild(tooltip); link.addEventListener('mouseenter', (e) => { const currentTooltip = e.currentTarget.querySelector('.fhl-item-tooltip'); if (!currentTooltip) return; currentTooltip.classList.add('visible'); const widgetRect = widgetWrapper.getBoundingClientRect(); const tooltipRect = currentTooltip.getBoundingClientRect(); let offsetX = 0; const padding = 5; if (tooltipRect.left < widgetRect.left) { offsetX = widgetRect.left - tooltipRect.left + padding; } else if (tooltipRect.right > widgetRect.right) { offsetX = widgetRect.right - tooltipRect.right - padding; } if (offsetX !== 0) { currentTooltip.style.transform = `translateX(calc(-50% + ${offsetX}px))`; } }); link.addEventListener('mouseleave', (e) => { const currentTooltip = e.currentTarget.querySelector('.fhl-item-tooltip'); if (!currentTooltip) return; currentTooltip.classList.remove('visible'); currentTooltip.style.transform = 'translateX(-50%)'; }); } else { const nameSpan = document.createElement('span'); nameSpan.className = 'fhl-item-name'; nameSpan.textContent = item.name.length > 5 ? item.name.slice(0, 5) + '..' : item.name; link.appendChild(nameSpan); } if (type === 'local') { const controls = document.createElement('div'); controls.className = 'fhl-item-controls'; const deleteBtn = document.createElement('button'); deleteBtn.className = 'fhl-control-btn'; deleteBtn.innerHTML = '&times;'; deleteBtn.title = '삭제'; deleteBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); let storedItems = shortcutManager.get(); storedItems = storedItems.filter(stored => stored.url !== item.url); shortcutManager.set(storedItems); initialRender(); }); const moveBtn = document.createElement('button'); moveBtn.className = 'fhl-control-btn'; moveBtn.innerHTML = '&gt;'; moveBtn.title = '오른쪽으로 이동'; moveBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); let storedItems = shortcutManager.get(); const currentIndex = storedItems.findIndex(stored => stored.url === item.url); if (currentIndex > -1) { const newIndex = (currentIndex + 1) % storedItems.length; const [movedItem] = storedItems.splice(currentIndex, 1); storedItems.splice(newIndex, 0, movedItem); shortcutManager.set(storedItems); initialRender(); } }); controls.appendChild(deleteBtn); controls.appendChild(moveBtn); link.appendChild(controls); } return link; }; 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; }; const renderInitialResults = (items) => { searchResultsContainer.innerHTML = ''; currentPage = 1; currentDisplayItems = items; appendNextPage(); }; const performSearch = async (query = '') => { try { const response = await fetch(`${API_BASE_URL}/appapi2.php?query=${encodeURIComponent(query)}`); if (!response.ok) throw new Error('API 응답 오류'); const results = await response.json(); renderInitialResults(results); } catch (error) { console.error('앱 목록 로드 실패:', error); searchResultsContainer.innerHTML = '<p>목록 로드 실패</p>'; } }; const openSearch = () => { widgetContainer.classList.add('search-mode'); widgetWrapper.classList.add('search-mode'); searchInput.focus(); performSearch(); }; const initialRender = () => { const t = translations[currentLang] || translations['en']; const b = [ { name: t.search, url: 'https://isai.kr', icon: 'ph-sparkle' }, { name: t.question, url: 'https://isai.kr/#chat', icon: 'ph-question-mark' }, { name: t.characterChat, url: 'https://zoai.oduc.kr/ko/character/select', icon: 'ph-chats-circle' }, { name: t.translate, url: 'https://translato.isai.kr/', icon: 'ph-translate' }, { name: t.tarot, url: 'https://tarot.isai.kr/', icon: 'ph-cards' }, { name: t.blog, url: 'https://blog.099.kr', icon: 'ph-article-medium' }, { name: t.novel, url: 'https://ranovel.kr/', img: 'https://cdn.jsdelivr.net/npm/cdnhost@2.2.2/__ra.png' }, { name: t.psychology, url: 'https://simpong.oduc.kr/', img: 'http://cdn.jsdelivr.net/npm/cdnhost@2.2.0/_simpong.png' }, { name: t.forum, url: 'https://logig.im', img: 'https://cdn.jsdelivr.net/npm/cdnhost@2.3.9/_______________logig.png' }, { name: t.ads, url: 'https://gig.snapp.im/', icon: 'ph-currency-circle-dollar' }, ]; listContainer.innerHTML = ''; const c = shortcutManager.get(); let d = [...b]; d.splice(2, 0, ...c); d.forEach(e => { const f = c.some(g => g.url === e.url); const h = f && !e.icon && !e.img ? 'local' : 'default'; listContainer.appendChild(createIconItem(e, h)); }); checkScrollability(); updateAllWidgetLinks(); }; 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)`; }; const checkScrollability = () => { const isScrollable = listContainer.scrollWidth > listContainer.clientWidth; widgetContainer.classList.toggle('scrollable', isScrollable); if (isScrollable) updateScrollIndicator(); }; let isDragging = false; 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; }; const handleDragEnd = () => { if (!isDragging) return; isDragging = false; document.removeEventListener('mousemove', handleDragMove); document.removeEventListener('mouseup', handleDragEnd); }; indicatorTrack.addEventListener('mousedown', (e) => { isDragging = true; handleDragMove(e); document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); }); const closeSearch = () => { widgetContainer.classList.remove('search-mode'); widgetWrapper.classList.remove('search-mode'); searchInput.value = ''; searchResultsContainer.innerHTML = ''; }; let hideTimeout = null; widgetContainer.addEventListener('mouseenter', () => { clearTimeout(hideTimeout); }); widgetContainer.addEventListener('mouseleave', () => { hideTimeout = setTimeout(() => { widgetContainer.classList.add('hidden'); }, 6000); }); document.addEventListener('click', (e) => { if (!languagePanel.contains(e.target) && !languageBtn.contains(e.target)) { languagePanel.classList.remove('visible'); } if (widgetContainer.contains(e.target)) { return; } if (widgetContainer.classList.contains('hidden')) { setTimeout(() => { widgetContainer.classList.remove('hidden'); clearTimeout(hideTimeout); hideTimeout = setTimeout(() => { widgetContainer.classList.add('hidden'); }, 6000); }, 50); } }); listContainer.addEventListener('scroll', updateScrollIndicator); window.addEventListener('resize', checkScrollability); searchTrigger.addEventListener('click', openSearch); languageBtn.addEventListener('click', (e) => { e.stopPropagation(); languagePanel.classList.toggle('visible'); }); searchInput.addEventListener('input', function() { clearTimeout(searchTimeout); const query = this.value.toLowerCase().trim(); searchTimeout = setTimeout(() => { performSearch(query); }, 300); }); searchResultsContainer.addEventListener('scroll', () => { const isAtBottom = searchResultsContainer.scrollTop + searchResultsContainer.clientHeight >= searchResultsContainer.scrollHeight - 10; if (isAtBottom) { appendNextPage(); } }); searchClose.addEventListener('click', closeSearch); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && widgetWrapper.classList.contains('search-mode')) { closeSearch(); } }); const addCurrentPageToWidget = async (e) => { e.preventDefault(); const t = translations[currentLang] || translations['en']; const baseUrl = getProcessedUrl(window.location.href); let iconName = document.title || t.currentPage; try { const response = await fetch(`${API_BASE_URL}/get_title.php?url=${encodeURIComponent(baseUrl)}`); if (response.ok) { const data = await response.json(); if (data.title) { iconName = data.title; } } } catch (error) { console.error('Failed to fetch title:', error); } const newIcon = { name: iconName, url: baseUrl }; let storedItems = shortcutManager.get(); const isAlreadyAdded = storedItems.some(item => getProcessedUrl(item.url) === getProcessedUrl(newIcon.url)); if (!isAlreadyAdded) { if (storedItems.length >= 10) { storedItems.shift(); } storedItems.push(newIcon); shortcutManager.set(storedItems); try { await fetch(`${API_BASE_URL}/register_app.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newIcon) }); } catch (error) { console.error('DB 등록/갱신 실패:', error); } initialRender(); if (widgetWrapper.classList.contains('search-mode')) { closeSearch(); } } else { showToast(t.alreadyAdded); } }; const copyWidgetScript = (e) => { e.preventDefault(); const textToCopy = '<script src="https://unpkg.com/@phosphor-icons/web"><\/script> <script src="https://cdn.jsdelivr.net/npm/cdnhost@latest/ws_lg.js"><\/script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cdnhost@latest/ws_css.css"> <div id="app"></div> <script src="https://cdn.jsdelivr.net/npm/cdnhost@latest/ws_cdnhtml.js"><\/script> <script src="https://cdn.jsdelivr.net/npm/cdnhost@latest/ws_cdn.js"><\/script>'; const t = translations[currentLang] || translations['en']; 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); }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(textToCopy).then(() => { showToast(t.codeCopied); }).catch(() => { fallbackCopy(); }); } else { fallbackCopy(); } }; addToHomeBtn.addEventListener('click', addCurrentPageToWidget); searchAddBtn.addEventListener('click', copyWidgetScript); shortcutManager.syncFromURL(); shortcutManager.get(); populateLanguagePanel(); applyLanguage(currentLang); });
package/ws_cdnhtml.js CHANGED
@@ -38,7 +38,6 @@ const widgetHTML = `
38
38
  <div id="fhl-language-panel" class="fhl-language-panel"></div>
39
39
  </div>
40
40
 
41
-
42
41
  `;
43
42
 
44
43
  // 3. appContainer가 실제로 존재하는지 확인한 후,
package/ws_css.css CHANGED
@@ -1,57 +1 @@
1
-
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
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
9
- }
10
- #fhl-widget-container.hidden { opacity: 0; transform: translateX(-50%) translateY(20px); pointer-events: none; }
11
- #fhl-widget-container .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; }
12
- #fhl-widget-container.scrollable .fhl-indicator-track { opacity: 1; }
13
- #fhl-widget-container.search-mode .fhl-indicator-track { display: none; }
14
- #fhl-widget-container .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; }
15
- #fhl-widget-container .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); transition: all 0.3s ease-in-out; pointer-events: auto; overflow: visible; }
16
- #fhl-widget-container .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; }
17
- #fhl-widget-container .fhl-icon-display:hover { background-color: rgba(0, 0, 0, 0.1); }
18
- #fhl-widget-container .fhl-icon-display i { font-size: 22px; }
19
- #fhl-widget-container .fhl-list-container .fhl-icon-display { display: inline-flex; width: auto; padding: 0 8px 0 0; border-radius: 20px; background-color: transparent; }
20
- #fhl-widget-container .fhl-list-container .fhl-icon-display:hover { background-color: rgba(0, 0, 0, 0.08); }
21
- #fhl-widget-container .fhl-list-container .fhl-icon-display:hover .fhl-icon-circle { background-color: transparent; }
22
- #fhl-widget-container .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; }
23
- #fhl-widget-container .fhl-list-container .fhl-icon-display:hover .fhl-item-name { max-width: 150px; opacity: 1; }
24
- #fhl-widget-container .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; }
25
- #fhl-widget-container .fhl-icon-circle img { width: 24px; height: 24px; object-fit: cover; border-radius: 50%; }
26
- #fhl-widget-container .fhl-icon-bar { display: flex; align-items: center; gap: 8px; width: 100%; transition: opacity 0.3s ease; }
27
- #fhl-widget-container .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; }
28
- #fhl-widget-container .fhl-list-container::-webkit-scrollbar { display: none; }
29
- #fhl-widget-container .fhl-list-container > .fhl-icon-display { scroll-snap-align: center; }
30
- #fhl-widget-container .fhl-widget-wrapper.search-mode { height: 280px; align-items: flex-start; border-radius: 20px; }
31
- #fhl-widget-container .fhl-widget-wrapper.search-mode .fhl-icon-bar { opacity: 0; pointer-events: none; }
32
- #fhl-widget-container .fhl-search-view { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; padding: 10px; opacity: 0; visibility: hidden; transition: opacity 0.3s 0.1s ease; }
33
- #fhl-widget-container .fhl-widget-wrapper.search-mode .fhl-search-view { opacity: 1; visibility: visible; }
34
- #fhl-widget-container .fhl-search-input-wrapper { display: flex; align-items: center; flex-shrink: 0; margin-bottom: 10px; }
35
- #fhl-widget-container .fhl-search-input { flex-grow: 1; height: 38px; border: none; background: transparent; font-size: 16px; color: #111; padding: 0 10px; outline: none; border-bottom: 2px solid #ccc; transition: border-color 0.2s ease; }
36
- #fhl-widget-container .fhl-search-input:focus { border-bottom-color: #007bff; }
37
- #fhl-widget-container .fhl-search-content { display: flex; flex-grow: 1; overflow: hidden; }
38
- #fhl-widget-container .fhl-search-results { flex-grow: 1; width: 0; overflow-y: auto; display: flex; flex-wrap: wrap; justify-content: flex-start; align-content: flex-start; gap: 12px; padding: 15px; -ms-overflow-style: none; scrollbar-width: none; padding-bottom: 40px; }
39
- #fhl-widget-container .fhl-search-results::-webkit-scrollbar { display: none; }
40
- #fhl-widget-container .fhl-search-results .fhl-icon-display { background-color: transparent; }
41
- #fhl-widget-container .fhl-search-results .fhl-icon-display:hover .fhl-icon-circle { background-color: rgba(0, 0, 0, 0.1); }
42
- #fhl-widget-container .fhl-item-tooltip { position: absolute; bottom: -28px; left: 50%; transform: translateX(-50%); background-color: #333; color: #fff; padding: 3px 6px; border-radius: 5px; font-size: 11px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 0.2s ease-in-out, bottom 0.2s ease-in-out; z-index: 10; }
43
- #fhl-widget-container .fhl-item-tooltip.visible { opacity: 1; visibility: visible; bottom: -32px; }
44
- #fhl-widget-container .fhl-fixed-panel { flex-shrink: 0; width: 70px; height: 100%; padding: 0; border-left: 1px solid rgba(0,0,0,0.1); display: flex; flex-direction: column; justify-content: space-around; align-items: center; }
45
- #fhl-widget-container .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; }
46
- #fhl-widget-container .fhl-fixed-item:hover { background-color: rgba(0, 0, 0, 0.08); }
47
- #fhl-widget-container .fhl-fixed-item i { font-size: 24px; }
48
- #fhl-widget-container .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; }
49
- #fhl-widget-container .fhl-list-container .fhl-icon-display:hover .fhl-item-controls { opacity: 1; visibility: visible; pointer-events: auto; bottom: -13px; }
50
- #fhl-widget-container .fhl-control-btn { background: none; border: none; color: white; cursor: pointer; font-size: 16px; line-height: 1; padding: 2px; font-family: monospace; }
51
- #fhl-widget-container .fhl-control-btn:hover { color: #007bff; }
52
- #fhl-widget-container .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; }
53
- #fhl-widget-container .fhl-language-panel.visible { opacity: 1; visibility: visible; pointer-events: auto; }
54
- #fhl-widget-container .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; }
55
- #fhl-widget-container .fhl-language-item:hover { background-color: rgba(0,0,0,0.1); }
56
- .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; }
57
- .fhl-toast.visible { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
1
+ .fhl-widget-wrapper *, .fhl-widget-wrapper *::before, .fhl-widget-wrapper *::after { box-sizing: border-box; } #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; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #fhl-widget-container.hidden { opacity: 0; transform: translateX(-50%) translateY(20px); pointer-events: none; } #fhl-widget-container .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; } #fhl-widget-container.scrollable .fhl-indicator-track { opacity: 1; } #fhl-widget-container.search-mode .fhl-indicator-track { display: none; } #fhl-widget-container .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; } #fhl-widget-container .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); transition: all 0.3s ease-in-out; pointer-events: auto; overflow: visible; } #fhl-widget-container .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; } #fhl-widget-container .fhl-icon-display:hover { background-color: rgba(0, 0, 0, 0.1); } #fhl-widget-container .fhl-icon-display i { font-size: 22px; } #fhl-widget-container .fhl-list-container .fhl-icon-display { display: inline-flex; width: auto; padding: 0 8px 0 0; border-radius: 20px; background-color: transparent; } #fhl-widget-container .fhl-list-container .fhl-icon-display:hover { background-color: rgba(0, 0, 0, 0.08); } #fhl-widget-container .fhl-list-container .fhl-icon-display:hover .fhl-icon-circle { background-color: transparent; } #fhl-widget-container .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; } #fhl-widget-container .fhl-list-container .fhl-icon-display:hover .fhl-item-name { max-width: 150px; opacity: 1; } #fhl-widget-container .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; } #fhl-widget-container .fhl-icon-circle img { width: 24px; height: 24px; object-fit: cover; border-radius: 50%; } #fhl-widget-container .fhl-icon-bar { display: flex; align-items: center; gap: 8px; width: 100%; transition: opacity 0.3s ease; } #fhl-widget-container .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; } #fhl-widget-container .fhl-list-container::-webkit-scrollbar { display: none; } #fhl-widget-container .fhl-list-container > .fhl-icon-display { scroll-snap-align: center; } #fhl-widget-container .fhl-widget-wrapper.search-mode { height: 280px; align-items: flex-start; border-radius: 20px; } #fhl-widget-container .fhl-widget-wrapper.search-mode .fhl-icon-bar { opacity: 0; pointer-events: none; } #fhl-widget-container .fhl-search-view { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; padding: 10px; opacity: 0; visibility: hidden; transition: opacity 0.3s 0.1s ease; } #fhl-widget-container .fhl-widget-wrapper.search-mode .fhl-search-view { opacity: 1; visibility: visible; } #fhl-widget-container .fhl-search-input-wrapper { display: flex; align-items: center; flex-shrink: 0; margin-bottom: 10px; } #fhl-widget-container .fhl-search-input { flex-grow: 1; height: 38px; border: none; background: transparent; font-size: 16px; color: #111; padding: 0 10px; outline: none; border-bottom: 2px solid #ccc; transition: border-color 0.2s ease; } #fhl-widget-container .fhl-search-input:focus { border-bottom-color: #007bff; } #fhl-widget-container .fhl-search-content { display: flex; flex-grow: 1; overflow: hidden; } #fhl-widget-container .fhl-search-results { flex-grow: 1; width: 0; overflow-y: auto; display: flex; flex-wrap: wrap; justify-content: flex-start; align-content: flex-start; gap: 12px; padding: 15px; -ms-overflow-style: none; scrollbar-width: none; padding-bottom: 40px; } #fhl-widget-container .fhl-search-results::-webkit-scrollbar { display: none; } #fhl-widget-container .fhl-search-results .fhl-icon-display { background-color: transparent; } #fhl-widget-container .fhl-search-results .fhl-icon-display:hover .fhl-icon-circle { background-color: rgba(0, 0, 0, 0.1); } #fhl-widget-container .fhl-item-tooltip { position: absolute; bottom: -28px; left: 50%; transform: translateX(-50%); background-color: #333; color: #fff; padding: 3px 6px; border-radius: 5px; font-size: 11px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 0.2s ease-in-out, bottom 0.2s ease-in-out; z-index: 10; } #fhl-widget-container .fhl-item-tooltip.visible { opacity: 1; visibility: visible; bottom: -32px; } #fhl-widget-container .fhl-fixed-panel { flex-shrink: 0; width: 70px; height: 100%; padding: 0; border-left: 1px solid rgba(0,0,0,0.1); display: flex; flex-direction: column; justify-content: space-around; align-items: center; } #fhl-widget-container .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; } #fhl-widget-container .fhl-fixed-item:hover { background-color: rgba(0, 0, 0, 0.08); } #fhl-widget-container .fhl-fixed-item i { font-size: 24px; } #fhl-widget-container .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; } #fhl-widget-container .fhl-list-container .fhl-icon-display:hover .fhl-item-controls { opacity: 1; visibility: visible; pointer-events: auto; bottom: -13px; } #fhl-widget-container .fhl-control-btn { background: none; border: none; color: white; cursor: pointer; font-size: 16px; line-height: 1; padding: 2px; font-family: monospace; } #fhl-widget-container .fhl-control-btn:hover { color: #007bff; } #fhl-widget-container .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; } #fhl-widget-container .fhl-language-panel.visible { opacity: 1; visibility: visible; pointer-events: auto; } #fhl-widget-container .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; } #fhl-widget-container .fhl-language-item:hover { background-color: rgba(0,0,0,0.1); } .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; } .fhl-toast.visible { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }