cdnhost 2.7.6 → 2.7.7
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 +1 -1
- package/ws_cdn.js +1 -1
package/package.json
CHANGED
package/ws_cdn.js
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
무단 복제, 수정, 배포를 금지합니다.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
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 Array.isArray(data.shortcuts) ? data.shortcuts.filter(item => item && item.name && item.url) : []; } 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 = decodeURIComponent(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=${encodeURIComponent(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') => { if (!item || !item.name || !item.url) return document.createDocumentFragment(); 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 = '×'; 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 = '>'; 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 = Array.isArray(items) ? 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.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.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.psychology, url: 'https://simpong.oduc.kr/', img: '//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://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); });
|
|
7
|
+
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 Array.isArray(data.shortcuts) ? data.shortcuts.filter(item => item && item.name && item.url) : []; } 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 = decodeURIComponent(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=${encodeURIComponent(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') => { if (!item || !item.name || !item.url) return document.createDocumentFragment(); 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 = '×'; 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 = '>'; 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 = Array.isArray(items) ? 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.blog, url: 'https://blog.099.kr', icon: 'ph-article-medium' }, { name: t.characterChat, url: 'https://lai.oduc.kr/', img: 'https://cdn.jsdelivr.net/npm/cdnhost@2.7.6/lai48.png' },{ name: t.translate, url: 'https://translato.isai.kr/', icon: 'ph-translate' }, { name: t.tarot, url: 'https://tarot.isai.kr/', icon: 'ph-cards' }, { 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: '//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://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); });
|