const App = { state: 'home', currentCategoryId: 'recyclable', recognition: null, isRecording: false, longPressTimer: null, longPressDelay: 300, els: {}, async init() { this.cacheElements(); await I18n.load('zh'); await DataStore.load(); I18n.applyToDOM(); this.initLangDropdown(); this.renderTabs(); this.selectCategory('recyclable'); this.bindEvents(); this.initVoice(); this.showWelcomeIfNeeded(); history.replaceState({ view: 'home' }, ''); }, cacheElements() { this.els.searchInput = document.getElementById('search-input'); this.els.btnQuery = document.getElementById('btn-query'); this.els.btnVoice = document.getElementById('btn-voice'); this.els.btnHome = document.getElementById('btn-home'); this.els.btnLang = document.getElementById('btn-lang'); this.els.langMenu = document.getElementById('lang-menu'); this.els.langDropdown = document.getElementById('lang-dropdown'); this.els.tabs = document.getElementById('tabs'); this.els.tabDetail = document.getElementById('tab-detail'); this.els.tabDesc = document.getElementById('tab-desc'); this.els.btnStandard = document.getElementById('btn-standard'); this.els.itemList = document.getElementById('item-list'); this.els.browseView = document.getElementById('browse-view'); this.els.searchView = document.getElementById('search-view'); this.els.searchResults = document.getElementById('search-results'); this.els.welcomeModal = document.getElementById('welcome-modal'); this.els.btnWelcomeConfirm = document.getElementById('btn-welcome-confirm'); this.els.standardModal = document.getElementById('standard-modal'); this.els.standardModalCard = document.getElementById('standard-modal-card'); this.els.btnStandardClose = document.getElementById('btn-standard-close'); this.els.standardTitle = document.getElementById('standard-title'); this.els.standardBody = document.getElementById('standard-body'); this.els.itemModal = document.getElementById('item-modal'); this.els.itemModalCard = document.getElementById('item-modal-card'); this.els.itemModalSuggest = document.getElementById('item-modal-suggest'); this.els.itemModalName = document.getElementById('item-modal-name'); this.els.itemModalIcon = document.getElementById('item-modal-icon'); this.els.itemModalLine1 = document.getElementById('item-modal-line1'); this.els.itemModalLine2 = document.getElementById('item-modal-line2'); this.els.itemModalTips = document.getElementById('item-modal-tips'); this.els.btnItemClose = document.getElementById('btn-item-close'); this.els.noResults = document.getElementById('no-results'); }, initLangDropdown() { const current = I18n.languages.find(l => l.code === I18n.currentLang); this.els.btnLang.textContent = I18n.t(current.labelKey); const onLangSelect = async (code) => { await I18n.load(code); I18n.applyToDOM(); this.els.btnLang.textContent = I18n.t(I18n.languages.find(l => l.code === code).labelKey); I18n.renderLangMenu(this.els.langMenu, onLangSelect); this.els.langMenu.classList.add('hidden'); this.renderTabs(); this.selectCategory(this.currentCategoryId); if (this.state === 'searchFuzzy') { const q = this.els.searchInput.value.trim(); if (q) this.search(q, false); } }; I18n.renderLangMenu(this.els.langMenu, onLangSelect); }, bindEvents() { this.els.btnQuery.addEventListener('click', () => { const q = this.els.searchInput.value.trim(); if (q) this.search(q); }); this.els.searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const q = this.els.searchInput.value.trim(); if (q) this.search(q); } }); this.els.btnHome.addEventListener('click', () => this.goHome()); this.els.btnLang.addEventListener('click', (e) => { e.stopPropagation(); this.els.langMenu.classList.toggle('hidden'); }); document.addEventListener('click', () => { this.els.langMenu.classList.add('hidden'); }); this.els.btnStandard.addEventListener('click', () => this.openStandardModal()); this.els.btnStandardClose.addEventListener('click', () => this.closeStandardModal()); this.els.standardModal.addEventListener('click', (e) => { if (e.target === this.els.standardModal) this.closeStandardModal(); }); this.els.btnItemClose.addEventListener('click', () => this.closeItemModal()); this.els.itemModal.addEventListener('click', (e) => { if (e.target === this.els.itemModal) this.closeItemModal(); }); this.els.btnWelcomeConfirm.addEventListener('click', () => { Storage.dismissWelcome(); this.els.welcomeModal.classList.add('hidden'); }); window.addEventListener('popstate', (e) => { if (!e.state || e.state.view === 'home') { this.goHome(false); } }); }, showWelcomeIfNeeded() { if (!Storage.isWelcomeDismissed()) { this.els.welcomeModal.classList.remove('hidden'); } }, renderTabs() { this.els.tabs.innerHTML = ''; DataStore.categories.forEach(cat => { const tab = document.createElement('div'); tab.className = 'tab'; tab.dataset.categoryId = cat.id; tab.innerHTML = '' + ''; tab.addEventListener('click', () => { if (this.state === 'searchFuzzy') return; this.selectCategory(cat.id); }); this.els.tabs.appendChild(tab); }); }, updateTabStates(categoryId) { this.els.tabs.querySelectorAll('.tab').forEach(tab => { const cat = DataStore.getCategory(tab.dataset.categoryId); if (!cat) return; const isActive = tab.dataset.categoryId === categoryId; tab.classList.toggle('active', isActive); tab.style.backgroundColor = isActive ? cat.color : ''; const iconEl = tab.querySelector('.tab-icon'); if (iconEl) { iconEl.src = isActive ? cat.icon : (cat.iconInactive || cat.icon); } }); }, selectCategory(categoryId) { this.currentCategoryId = categoryId; const cat = DataStore.getCategory(categoryId); if (!cat) return; this.updateTabStates(categoryId); this.els.tabDetail.style.backgroundColor = cat.color; this.els.tabDesc.textContent = I18n.t(cat.descKey); this.renderItemList(DataStore.getItemsByCategory(categoryId), this.els.itemList, false); }, renderItemList(items, container, isSearch) { container.innerHTML = ''; container.classList.toggle('item-list-browse', !isSearch); container.classList.toggle('item-list-search-wrap', isSearch); items.forEach((item, index) => { const row = document.createElement('div'); if (isSearch) { row.className = 'list-item list-item-search' + (index % 2 === 0 ? ' list-item-even' : ' list-item-odd'); const cat = DataStore.getCategory(item.categoryId); row.innerHTML = '' + DataStore.getLocalizedName(item) + '' + '' + I18n.t(cat.nameKey) + ''; } else { row.className = 'list-item' + (index % 2 === 0 ? ' list-item-even' : ' list-item-odd'); row.textContent = DataStore.getLocalizedName(item); } row.addEventListener('click', () => this.openItemModal(item)); container.appendChild(row); }); }, openStandardModal() { const cat = DataStore.getCategory(this.currentCategoryId); if (!cat) return; this.els.standardModalCard.style.backgroundColor = cat.color; this.els.standardTitle.textContent = I18n.t(cat.nameKey) + ' - ' + I18n.t('btn.standard'); this.els.standardBody.textContent = I18n.t(cat.standardKey); this.els.standardModal.classList.remove('hidden'); }, closeStandardModal() { this.els.standardModal.classList.add('hidden'); }, openItemModal(item) { const cat = DataStore.getCategory(item.categoryId); if (!cat) return; const tpl = cat.disposalTemplate; this.els.itemModalName.textContent = DataStore.getLocalizedName(item); this.els.itemModalCard.style.backgroundColor = cat.color; this.els.itemModalIcon.src = cat.icon; this.els.itemModalLine1.textContent = I18n.t(tpl.line1Key); this.els.itemModalLine2.textContent = I18n.t(tpl.line2Key); this.els.itemModalTips.innerHTML = ''; tpl.tipsKeys.forEach((key, i) => { const li = document.createElement('li'); li.textContent = (i + 1) + '. ' + I18n.t(key); this.els.itemModalTips.appendChild(li); }); this.els.itemModal.classList.remove('hidden'); }, closeItemModal() { this.els.itemModal.classList.add('hidden'); }, search(query, pushHistory) { if (pushHistory === undefined) pushHistory = true; const q = query.trim().toLowerCase(); if (!q) return; this.els.noResults.classList.add('hidden'); const allItems = DataStore.getAllItemsLocalized(); const exact = allItems.filter(i => i.localName.toLowerCase() === q); if (exact.length === 1) { this.state = 'searchExact'; this.els.btnHome.classList.add('hidden'); this.openItemModal(exact[0]); return; } const fuzzy = allItems.filter(i => i.localName.toLowerCase().includes(q)); if (fuzzy.length === 0) { this.els.browseView.classList.remove('hidden'); this.els.searchView.classList.add('hidden'); this.els.btnHome.classList.add('hidden'); this.els.noResults.classList.remove('hidden'); this.state = 'home'; return; } this.state = 'searchFuzzy'; this.els.browseView.classList.add('hidden'); this.els.searchView.classList.remove('hidden'); this.els.btnHome.classList.remove('hidden'); this.renderItemList(fuzzy, this.els.searchResults, true); if (pushHistory) { if (this.state === 'searchFuzzy') { history.replaceState({ view: 'search' }, ''); } else { history.pushState({ view: 'search' }, ''); } } }, goHome(fromHistory) { if (fromHistory === undefined) fromHistory = true; this.state = 'home'; this.els.searchInput.value = ''; this.els.browseView.classList.remove('hidden'); this.els.searchView.classList.add('hidden'); this.els.btnHome.classList.add('hidden'); this.els.noResults.classList.add('hidden'); this.closeItemModal(); this.closeStandardModal(); this.selectCategory('recyclable'); if (fromHistory && history.state && history.state.view === 'search') { history.back(); } }, initVoice() { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { this.els.btnVoice.classList.add('hidden'); return; } this.recognition = new SpeechRecognition(); this.recognition.lang = I18n.getLocale(); this.recognition.interimResults = false; this.recognition.maxAlternatives = 1; this.recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; this.els.searchInput.value = transcript; this.search(transcript); }; this.recognition.onerror = () => { this.stopRecording(); }; this.recognition.onend = () => { this.stopRecording(); }; const startRecording = (e) => { e.preventDefault(); this.longPressTimer = setTimeout(() => { this.startRecording(); }, this.longPressDelay); }; const cancelLongPress = () => { if (this.longPressTimer) { clearTimeout(this.longPressTimer); this.longPressTimer = null; } if (this.isRecording) { this.recognition.stop(); } }; this.els.btnVoice.addEventListener('mousedown', startRecording); this.els.btnVoice.addEventListener('mouseup', cancelLongPress); this.els.btnVoice.addEventListener('mouseleave', cancelLongPress); this.els.btnVoice.addEventListener('touchstart', startRecording, { passive: false }); this.els.btnVoice.addEventListener('touchend', cancelLongPress); this.els.btnVoice.addEventListener('touchcancel', cancelLongPress); }, startRecording() { if (this.isRecording) return; this.isRecording = true; this.recognition.lang = I18n.getLocale(); this.els.btnVoice.classList.add('recording'); try { this.recognition.start(); } catch { this.stopRecording(); } }, stopRecording() { this.isRecording = false; this.els.btnVoice.classList.remove('recording'); } }; document.addEventListener('DOMContentLoaded', () => App.init());