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 =
|
'<span class="tab-color-bar" style="background-color:' + cat.color + '"></span>' +
|
'<img src="' + (cat.iconInactive || cat.icon) + '" alt="" class="tab-icon" width="210" height="210">';
|
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 =
|
'<span class="item-name">' + DataStore.getLocalizedName(item) + '</span>' +
|
'<span class="item-category ' + DataStore.getCategoryColorClass(item.categoryId) + '">' +
|
I18n.t(cat.nameKey) + '</span>';
|
} 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());
|