Spaces:
Running
Running
Download main.js from Lunaron/lunaron-tools: direct link, hf CLI and curl.
- Browser
- Download file 18.8 kB
-
https://huggingface.co/spaces/Lunaron/lunaron-tools/resolve/main/main.js
- Command line
-
hf download hf://spaces/Lunaron/lunaron-tools/main.js
-
curl -L -o main.js https://huggingface.co/spaces/Lunaron/lunaron-tools/resolve/main/main.js
18.8 kB
| /* ======================================== | |
| LUNARON TOOLS — JavaScript | |
| ======================================== */ | |
| window.LunaronApp = (() => { | |
| 'use strict'; | |
| /* --- STATE --- */ | |
| const state = { | |
| theme: localStorage.getItem('lunaronTheme') || 'light', | |
| isLoggedIn: localStorage.getItem('lunaron_isLoggedIn') === 'true', | |
| currentUser: JSON.parse(localStorage.getItem('lunaron_user') || 'null'), | |
| activeTab: 'login', | |
| }; | |
| /* --- ELEMENTS (resolved lazily) --- */ | |
| const $ = id => document.getElementById(id); | |
| const $$ = sel => document.querySelectorAll(sel); | |
| /* --- THEME --- */ | |
| function applyTheme(theme) { | |
| document.documentElement.setAttribute('data-theme', theme); | |
| state.theme = theme; | |
| localStorage.setItem('lunaronTheme', theme); | |
| } | |
| /* --- FILTER --- */ | |
| function setFilter(filter) { | |
| $$('.filter-btn').forEach(btn => | |
| btn.classList.toggle('active', btn.dataset.filter === filter) | |
| ); | |
| $$('.category-section').forEach(sec => { | |
| if(sec.id !== 'fav-section') sec.style.display = (filter === 'all' || sec.dataset.category === filter) ? '' : 'none'; | |
| }); | |
| } | |
| /* --- SEARCH --- */ | |
| function handleSearch(e) { | |
| const term = e.target.value.toLowerCase(); | |
| if (term.length > 0) { | |
| $$('.filter-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.filter === 'all')); | |
| $$('.category-section').forEach(sec => { if(sec.id !== 'fav-section') sec.style.display = ''; }); | |
| } else { | |
| setFilter('all'); | |
| } | |
| $$('.category-section:not(#fav-section) .tool-card').forEach(card => { | |
| const name = card.querySelector('.tool-name')?.textContent.toLowerCase() || ''; | |
| const desc = card.querySelector('.tool-desc')?.textContent.toLowerCase() || ''; | |
| if (name.includes(term) || desc.includes(term)) { | |
| card.style.display = ''; | |
| card.style.animation = 'none'; | |
| card.style.opacity = '1'; | |
| } else { | |
| card.style.display = 'none'; | |
| } | |
| }); | |
| $$('.category-section:not(#fav-section)').forEach(sec => { | |
| const visibleCards = sec.querySelectorAll('.tool-card[style*="display: none"]'); | |
| const totalCards = sec.querySelectorAll('.tool-card'); | |
| if (term.length > 0) { | |
| sec.style.display = visibleCards.length === totalCards.length ? 'none' : ''; | |
| } | |
| }); | |
| } | |
| /* --- CARD STAGGER ANIMATION --- */ | |
| function initCards() { | |
| $$('.tool-card').forEach((card, i) => { | |
| card.style.animationDelay = (i * 0.04) + 's'; | |
| }); | |
| } | |
| /* --- FAVORITES --- */ | |
| function toggleFavorite(toolId, btnEl) { | |
| let favs = JSON.parse(localStorage.getItem('lunaron_favs') || '[]'); | |
| const isFav = favs.includes(toolId); | |
| if (isFav) { | |
| favs = favs.filter(id => id !== toolId); | |
| btnEl.classList.remove('active'); | |
| btnEl.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`; | |
| } else { | |
| favs.push(toolId); | |
| btnEl.classList.add('active'); | |
| btnEl.innerHTML = `<svg viewBox="0 0 24 24" fill="#f59e0b" stroke="#f59e0b" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`; | |
| } | |
| localStorage.setItem('lunaron_favs', JSON.stringify(favs)); | |
| $$('.tool-card').forEach(card => { | |
| if(card.dataset.toolId === toolId) { | |
| const b = card.querySelector('.fav-btn'); | |
| if(b && b !== btnEl) { | |
| b.classList.toggle('active', !isFav); | |
| b.innerHTML = !isFav ? `<svg viewBox="0 0 24 24" fill="#f59e0b" stroke="#f59e0b" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>` : `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`; | |
| } | |
| } | |
| }); | |
| renderFavorites(); | |
| } | |
| function renderFavorites() { | |
| const favSection = $('fav-section'); | |
| const favGrid = $('fav-grid'); | |
| if (!favSection || !favGrid) return; | |
| let favs = JSON.parse(localStorage.getItem('lunaron_favs') || '[]'); | |
| if (favs.length === 0) { | |
| favSection.style.display = 'none'; | |
| return; | |
| } | |
| favSection.style.display = ''; | |
| favGrid.innerHTML = ''; | |
| favs.forEach(toolId => { | |
| const originalCard = document.querySelector(`.category-section:not(#fav-section) .tool-card[data-tool-id="${toolId}"]`); | |
| if (originalCard) { | |
| const clone = originalCard.cloneNode(true); | |
| const btn = clone.querySelector('.fav-btn'); | |
| if (btn) { | |
| btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleFavorite(toolId, btn); }); | |
| } | |
| favGrid.appendChild(clone); | |
| } | |
| }); | |
| } | |
| function initFavorites() { | |
| if (!window.location.pathname.endsWith('index.html') && !window.location.pathname.endsWith('/')) return; | |
| let favs = JSON.parse(localStorage.getItem('lunaron_favs') || '[]'); | |
| $$('.category-section:not(#fav-section) .tool-card').forEach(card => { | |
| const toolId = card.getAttribute('href'); | |
| card.dataset.toolId = toolId; | |
| const starBtn = document.createElement('button'); | |
| starBtn.className = `fav-btn ${favs.includes(toolId) ? 'active' : ''}`; | |
| starBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="${favs.includes(toolId) ? '#f59e0b' : 'none'}" stroke="${favs.includes(toolId) ? '#f59e0b' : 'currentColor'}" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`; | |
| starBtn.style.cssText = 'position: absolute; top: 16px; right: 16px; background: var(--bg-glass); border: 1px solid var(--border-glass); border-radius: 50%; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 10; transition: all 0.2s; backdrop-filter: blur(4px); color: var(--text-primary);'; | |
| starBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleFavorite(toolId, starBtn); }); | |
| starBtn.addEventListener('mouseenter', () => { starBtn.style.transform = 'scale(1.1)'; }); | |
| starBtn.addEventListener('mouseleave', () => { starBtn.style.transform = 'scale(1)'; }); | |
| card.appendChild(starBtn); | |
| }); | |
| renderFavorites(); | |
| } | |
| /* --- MODAL --- */ | |
| function openModal(tab) { | |
| const overlay = $('authOverlay'); | |
| if (overlay) { overlay.classList.add('open'); document.body.style.overflow = 'hidden'; } | |
| setTab(tab || 'login'); | |
| } | |
| function closeModal() { | |
| const overlay = $('authOverlay'); | |
| if (overlay) { overlay.classList.remove('open'); document.body.style.overflow = ''; } | |
| } | |
| function setTab(tab) { | |
| state.activeTab = tab; | |
| $$('.modal-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab)); | |
| const lf = $('loginForm'), sf = $('signupForm'); | |
| if (lf) lf.style.display = tab === 'login' ? '' : 'none'; | |
| if (sf) sf.style.display = tab === 'signup' ? '' : 'none'; | |
| } | |
| /* --- ACCOUNT MENU --- */ | |
| function openAccountMenu(e) { | |
| e && e.stopPropagation(); | |
| $('accountMenu') && $('accountMenu').classList.toggle('open'); | |
| } | |
| function closeAccountMenu() { | |
| $('accountMenu') && $('accountMenu').classList.remove('open'); | |
| } | |
| /* --- AUTH --- */ | |
| function loginUser(user) { | |
| state.isLoggedIn = true; | |
| state.currentUser = user; | |
| localStorage.setItem('lunaron_isLoggedIn', 'true'); | |
| localStorage.setItem('lunaron_user', JSON.stringify(user)); | |
| updateNavUI(); | |
| closeModal(); | |
| } | |
| function logout() { | |
| state.isLoggedIn = false; | |
| state.currentUser = null; | |
| localStorage.removeItem('lunaron_isLoggedIn'); | |
| localStorage.removeItem('lunaron_user'); | |
| closeAccountMenu(); | |
| updateNavUI(); | |
| } | |
| function updateNavUI() { | |
| const nl = $('navLogin'), ns = $('navSignup'), na = $('navAvatar'); | |
| const ab = $('avatarBtn'), an = $('accountName'), ae = $('accountEmail'), ap = $('accountPlan'); | |
| if (state.isLoggedIn && state.currentUser) { | |
| if (nl) nl.style.display = 'none'; | |
| if (ns) ns.style.display = 'none'; | |
| if (na) na.style.display = ''; | |
| if (ab) ab.textContent = (state.currentUser.name || '?')[0].toUpperCase(); | |
| if (an) an.textContent = state.currentUser.name; | |
| if (ae) ae.textContent = state.currentUser.email; | |
| if (ap) { | |
| const isPro = state.currentUser.plan === 'pro'; | |
| ap.textContent = isPro ? 'Pro Plan' : 'Free Plan'; | |
| ap.className = 'account-plan ' + (isPro ? 'pro-plan' : 'free-plan'); | |
| } | |
| } else { | |
| if (nl) nl.style.display = ''; | |
| if (ns) ns.style.display = ''; | |
| if (na) na.style.display = 'none'; | |
| } | |
| } | |
| /* --- SCROLL --- */ | |
| function scrollTo(id) { | |
| const el = document.getElementById(id); | |
| if (el) { | |
| window.scrollTo({ top: el.getBoundingClientRect().top + scrollY - 80, behavior: 'smooth' }); | |
| } else { | |
| // Not on home page, redirect | |
| window.location.href = (window.location.pathname.includes('/tools/') ? '../index.html' : './index.html') + '#' + id; | |
| } | |
| } | |
| /* --- NAV HIGHLIGHT --- */ | |
| function initNavHighlight() { | |
| const links = $$('.nav-link[data-section]'); | |
| if (!links.length) return; | |
| const obs = new IntersectionObserver(entries => { | |
| entries.forEach(e => { | |
| if (e.isIntersecting) { | |
| links.forEach(l => l.classList.toggle('active', l.dataset.section === e.target.id)); | |
| } | |
| }); | |
| }, { rootMargin: '-35% 0px -35% 0px' }); | |
| links.forEach(l => { const s = document.getElementById(l.dataset.section); if (s) obs.observe(s); }); | |
| } | |
| /* --- SCROLL REVEAL --- */ | |
| function initReveal() { | |
| $$('.category-section, .pricing-section, .tool-page > div').forEach(el => { | |
| el.classList.add('reveal'); | |
| }); | |
| const reveals = $$('.reveal'); | |
| if (!reveals.length) return; | |
| const observer = new IntersectionObserver(entries => { | |
| entries.forEach(e => { | |
| if (e.isIntersecting) { | |
| e.target.classList.add('active'); | |
| observer.unobserve(e.target); | |
| } | |
| }); | |
| }, { rootMargin: '0px 0px -10% 0px' }); | |
| reveals.forEach(r => observer.observe(r)); | |
| } | |
| /* --- EVENT BINDING (Event Delegation for robustness) --- */ | |
| function bindEvents() { | |
| // Delegation for nav/global actions | |
| document.addEventListener('click', e => { | |
| // Theme | |
| const themeBtn = e.target.closest('#themeToggle'); | |
| if (themeBtn) applyTheme(state.theme === 'dark' ? 'light' : 'dark'); | |
| // Modal open | |
| const loginBtn = e.target.closest('[data-action="open-login"]'); | |
| if (loginBtn) openModal('login'); | |
| const signupBtn = e.target.closest('[data-action="open-signup"]'); | |
| if (signupBtn) openModal('signup'); | |
| // Scroll to pricing | |
| const priceBtn = e.target.closest('[data-action="scroll-pricing"]'); | |
| if (priceBtn) scrollTo('pricing'); | |
| // Nav links (section scroll) | |
| const navLink = e.target.closest('.nav-link[data-section]'); | |
| if (navLink) scrollTo(navLink.dataset.section); | |
| // Account menu | |
| const avatarBtn = e.target.closest('#avatarBtn'); | |
| if (avatarBtn) openAccountMenu(e); | |
| const logoutBtn = e.target.closest('#logoutBtn'); | |
| if (logoutBtn) logout(); | |
| // Modal tabs | |
| const modalTab = e.target.closest('.modal-tab'); | |
| if (modalTab) setTab(modalTab.dataset.tab); | |
| // Switch tab links | |
| const switchLink = e.target.closest('[data-switch-tab]'); | |
| if (switchLink) { e.preventDefault(); setTab(switchLink.dataset.switchTab); } | |
| // Close modal on overlay click | |
| if (e.target === $('authOverlay')) closeModal(); | |
| // Close account menu on outside click | |
| const menu = $('accountMenu'); | |
| const abtn = $('avatarBtn'); | |
| if (menu && abtn && !menu.contains(e.target) && e.target !== abtn) closeAccountMenu(); | |
| }); | |
| // Filters (usually static) | |
| $$('.filter-btn').forEach(btn => btn.addEventListener('click', () => setFilter(btn.dataset.filter))); | |
| // Search | |
| const searchInput = $('toolSearchInput'); | |
| if (searchInput) searchInput.addEventListener('input', handleSearch); | |
| // Modal close (static) | |
| $('authClose') && $('authClose').addEventListener('click', closeModal); | |
| // Keyboard | |
| document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal(); closeAccountMenu(); } }); | |
| // Forms — Supabase 認証 | |
| document.addEventListener('submit', async e => { | |
| const auth = window.LunaronAuth; | |
| // ログインフォーム | |
| if (e.target.id === 'loginFormEl') { | |
| e.preventDefault(); | |
| const email = e.target.querySelector('[name="email"]').value.trim(); | |
| const password = e.target.querySelector('[name="password"]')?.value || ''; | |
| const btn = e.target.querySelector('button[type="submit"]'); | |
| if (btn) { btn.disabled = true; btn.textContent = '認証中...'; } | |
| if (auth) { | |
| const { data, error } = await auth.signIn(email, password); | |
| if (error) { | |
| alert('ログインに失敗しました: ' + (error.message || '入力内容を確認してください')); | |
| } | |
| // onAuthStateChange が UI を更新する | |
| } else { | |
| // Supabase未設定時はフォールバック | |
| loginUser({ name: email.split('@')[0], email, plan: 'free' }); | |
| } | |
| if (btn) { btn.disabled = false; btn.textContent = 'ログイン'; } | |
| } | |
| // 新規登録フォーム | |
| if (e.target.id === 'signupFormEl') { | |
| e.preventDefault(); | |
| const name = e.target.querySelector('[name="name"]').value.trim(); | |
| const email = e.target.querySelector('[name="email"]').value.trim(); | |
| const password = e.target.querySelector('[name="password"]')?.value || ''; | |
| const btn = e.target.querySelector('button[type="submit"]'); | |
| if (btn) { btn.disabled = true; btn.textContent = '登録中...'; } | |
| if (auth) { | |
| const { data, error } = await auth.signUp(email, password, name); | |
| if (error) { | |
| alert('登録に失敗しました: ' + (error.message || '入力内容を確認してください')); | |
| } else { | |
| alert('確認メールを送信しました。メールをご確認ください。'); | |
| closeModal(); | |
| } | |
| } else { | |
| loginUser({ name, email, plan: 'free' }); | |
| } | |
| if (btn) { btn.disabled = false; btn.textContent = '無料で登録する'; } | |
| } | |
| }); | |
| // Google OAuth ボタン | |
| document.addEventListener('click', async e => { | |
| const googleBtn = e.target.closest('.social-btn'); | |
| if (googleBtn) { | |
| if (window.LunaronAuth) { | |
| googleBtn.disabled = true; | |
| googleBtn.textContent = 'Googleに接続中...'; | |
| const { error } = await window.LunaronAuth.signInWithGoogle(); | |
| if (error) { | |
| alert('Google認証に失敗しました。'); | |
| googleBtn.disabled = false; | |
| } | |
| // リダイレクトするので復元不要 | |
| } else { | |
| alert('認証設定が必要です。.env に SUPABASE_URL と SUPABASE_ANON_KEY を設定してください。'); | |
| } | |
| } | |
| }); | |
| // Pricing CTA | |
| document.addEventListener('click', e => { | |
| const pCta = e.target.closest('[data-action="pricing-cta"]'); | |
| if (pCta && !state.isLoggedIn) openModal('signup'); | |
| }); | |
| } | |
| /* --- GLOBAL UX --- */ | |
| function initGlobalUX() { | |
| if (!document.getElementById('globalLoading')) { | |
| const loader = document.createElement('div'); | |
| loader.id = 'globalLoading'; | |
| loader.className = 'global-loading-overlay'; | |
| loader.innerHTML = '<div class="moon-loader"></div><div class="moon-loader-text">処理中...</div>'; | |
| document.body.appendChild(loader); | |
| } | |
| document.addEventListener('dragover', e => { | |
| e.preventDefault(); | |
| }); | |
| document.addEventListener('drop', e => { | |
| // 特定の要素やクラスを持つ場合はグローバルな処理を無視する | |
| if (e.target.closest('.drop-zone, .drop-zone-wrap, [type="file"], canvas, textarea, input')) return; | |
| e.preventDefault(); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| const type = files[0].type; | |
| const basePath = window.location.pathname.includes('/tools/') ? '' : 'tools/'; | |
| if (type.startsWith('image/')) { | |
| window.location.href = basePath + 'compress.html'; | |
| } else if (type.startsWith('video/')) { | |
| window.location.href = basePath + 'video-compress.html'; | |
| } | |
| } | |
| }); | |
| document.addEventListener('keydown', e => { | |
| if (e.ctrlKey || e.metaKey) { | |
| if (e.key.toLowerCase() === 'k') { | |
| e.preventDefault(); | |
| const searchInput = document.getElementById('toolSearchInput'); | |
| if (searchInput) { | |
| searchInput.focus(); | |
| scrollTo('tools-section'); | |
| } | |
| } else if (e.key.toLowerCase() === 's') { | |
| e.preventDefault(); | |
| const saveBtn = document.querySelector('[id^="dl-"], .dl-btn, [data-action="save"]'); | |
| if (saveBtn) saveBtn.click(); | |
| } else if (e.key.toLowerCase() === 'z') { | |
| // Allow default undo for inputs | |
| if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { | |
| const undoBtn = document.querySelector('[data-action="undo"]'); | |
| if (undoBtn) { | |
| e.preventDefault(); | |
| undoBtn.click(); | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| /* --- INIT --- */ | |
| function init() { | |
| applyTheme(state.theme); | |
| initCards(); | |
| initFavorites(); | |
| bindEvents(); | |
| updateNavUI(); | |
| initNavHighlight(); | |
| initReveal(); | |
| initGlobalUX(); | |
| // Handle hash on load (for redirects from tool pages) | |
| if (window.location.hash) { | |
| const id = window.location.hash.substring(1); | |
| setTimeout(() => { | |
| const el = document.getElementById(id); | |
| if (el) scrollTo(id); | |
| }, 500); | |
| } | |
| } | |
| // Global helper for loader | |
| window.LunaronLoader = { | |
| show: () => { const l = $('globalLoading'); if (l) l.classList.add('active'); }, | |
| hide: () => { const l = $('globalLoading'); if (l) l.classList.remove('active'); } | |
| }; | |
| return { init, updateNavUI }; | |
| })(); | |
| // Initialize or re-initialize if needed | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', window.LunaronApp.init); | |
| } else { | |
| window.LunaronApp.init(); | |
| } | |