feat(mobile): implement iOS Safari responsive layout, bottom tab bar, and mobile bottom sheet
This commit is contained in:
1 parent
d5e9354016
commit
c0fdcd776b
4 files changed
+707
-50
No files matched your search
+128
-2
@@ -392,7 +392,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function showApp() {
|
||||
loginOverlay.classList.add('hidden');
|
||||
if (appShell) appShell.classList.remove('hidden');
|
||||
userDisplayName.textContent = currentUser ? currentUser.username : 'user';
|
||||
const name = currentUser ? currentUser.username : 'user';
|
||||
userDisplayName.textContent = name;
|
||||
const mobileUserName = document.getElementById('mobile-user-name');
|
||||
if (mobileUserName) mobileUserName.textContent = name;
|
||||
}
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
@@ -506,7 +509,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const resp = await fetch('/api/srs/stats');
|
||||
if (resp.ok) {
|
||||
const stats = await resp.json();
|
||||
srsBadge.textContent = stats.due_cards || 0;
|
||||
const due = stats.due_cards || 0;
|
||||
srsBadge.textContent = due;
|
||||
const tabSrsBadge = document.getElementById('tab-srs-badge');
|
||||
if (tabSrsBadge) {
|
||||
tabSrsBadge.textContent = due;
|
||||
if (due > 0) {
|
||||
tabSrsBadge.classList.remove('hidden');
|
||||
} else {
|
||||
tabSrsBadge.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -970,10 +983,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
renderSrsCurrentCard();
|
||||
});
|
||||
|
||||
function updateMobileActiveTab(tabName) {
|
||||
document.querySelectorAll('.mobile-tab-bar .tab-item').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
const target = document.getElementById(`tab-nav-${tabName}`);
|
||||
if (target) target.classList.add('active');
|
||||
}
|
||||
|
||||
function openDictionaryView() {
|
||||
srsView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
dictionaryView.classList.remove('hidden');
|
||||
updateMobileActiveTab('dict');
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@@ -981,6 +1003,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
dictionaryView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
srsView.classList.remove('hidden');
|
||||
updateMobileActiveTab('srs');
|
||||
startSrsTrainingSession(false, tagFilter);
|
||||
}
|
||||
|
||||
@@ -988,6 +1011,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
srsView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
dictionaryView.classList.remove('hidden');
|
||||
updateMobileActiveTab('dict');
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@@ -995,11 +1019,107 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
dictionaryView.classList.add('hidden');
|
||||
srsView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.remove('hidden');
|
||||
updateMobileActiveTab('reader');
|
||||
loadBookshelf();
|
||||
}
|
||||
|
||||
function initMobileNavigation() {
|
||||
const tabDict = document.getElementById('tab-nav-dict');
|
||||
const tabSrs = document.getElementById('tab-nav-srs');
|
||||
const tabReader = document.getElementById('tab-nav-reader');
|
||||
const tabStats = document.getElementById('tab-nav-stats');
|
||||
const tabMenu = document.getElementById('tab-nav-menu');
|
||||
|
||||
const mobileMenuModal = document.getElementById('mobile-menu-modal');
|
||||
const mobileMenuToggleBtn = document.getElementById('mobile-menu-toggle-btn');
|
||||
const mobileMenuCloseBtn = document.getElementById('mobile-menu-close-btn');
|
||||
|
||||
const mobileStatsBtn = document.getElementById('mobile-stats-btn');
|
||||
const mobileExportBtn = document.getElementById('mobile-export-btn');
|
||||
const mobilePwdBtn = document.getElementById('mobile-pwd-btn');
|
||||
const mobileThemeBtn = document.getElementById('mobile-theme-btn');
|
||||
const mobileLogoutBtn = document.getElementById('mobile-logout-btn');
|
||||
|
||||
if (tabDict) tabDict.addEventListener('click', () => openDictionaryView());
|
||||
if (tabSrs) tabSrs.addEventListener('click', () => openSrsView());
|
||||
if (tabReader) tabReader.addEventListener('click', () => openReaderView());
|
||||
|
||||
if (tabStats) {
|
||||
tabStats.addEventListener('click', () => {
|
||||
if (statsModal) {
|
||||
statsModal.classList.remove('hidden');
|
||||
loadStatsAndHeatmap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openMobileMenu() {
|
||||
if (mobileMenuModal) {
|
||||
const mobileUserName = document.getElementById('mobile-user-name');
|
||||
if (mobileUserName && currentUser) {
|
||||
mobileUserName.textContent = currentUser.username;
|
||||
}
|
||||
mobileMenuModal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeMobileMenu() {
|
||||
if (mobileMenuModal) {
|
||||
mobileMenuModal.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
if (tabMenu) tabMenu.addEventListener('click', openMobileMenu);
|
||||
if (mobileMenuToggleBtn) mobileMenuToggleBtn.addEventListener('click', openMobileMenu);
|
||||
if (mobileMenuCloseBtn) mobileMenuCloseBtn.addEventListener('click', closeMobileMenu);
|
||||
|
||||
if (mobileMenuModal) {
|
||||
mobileMenuModal.addEventListener('click', (e) => {
|
||||
if (e.target === mobileMenuModal) closeMobileMenu();
|
||||
});
|
||||
}
|
||||
|
||||
if (mobileStatsBtn) {
|
||||
mobileStatsBtn.addEventListener('click', () => {
|
||||
closeMobileMenu();
|
||||
if (statsModal) {
|
||||
statsModal.classList.remove('hidden');
|
||||
loadStatsAndHeatmap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (mobileExportBtn) {
|
||||
mobileExportBtn.addEventListener('click', () => {
|
||||
closeMobileMenu();
|
||||
if (exportModal) exportModal.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (mobilePwdBtn) {
|
||||
mobilePwdBtn.addEventListener('click', () => {
|
||||
closeMobileMenu();
|
||||
if (changePwdModal) changePwdModal.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (mobileThemeBtn) {
|
||||
mobileThemeBtn.addEventListener('click', () => {
|
||||
themeToggle.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (mobileLogoutBtn) {
|
||||
mobileLogoutBtn.addEventListener('click', () => {
|
||||
closeMobileMenu();
|
||||
logoutBtn.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
navBrandHome.addEventListener('click', () => openDictionaryView());
|
||||
if (navReaderBtn) navReaderBtn.addEventListener('click', () => openReaderView());
|
||||
initMobileNavigation();
|
||||
|
||||
async function startSrsTrainingSession(forceAll = false, tagFilter = null) {
|
||||
srsLoading.classList.remove('hidden');
|
||||
@@ -2396,6 +2516,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Precise popup positioning near target element without scroll double-offset
|
||||
function positionWordPopup(targetElement) {
|
||||
if (!targetElement || !readerWordPopup) return;
|
||||
if (window.innerWidth <= 768) {
|
||||
readerWordPopup.style.top = '';
|
||||
readerWordPopup.style.left = '';
|
||||
return;
|
||||
}
|
||||
const rect = targetElement.getBoundingClientRect();
|
||||
const popupWidth = 320;
|
||||
const popupHeight = readerWordPopup.offsetHeight || 220;
|
||||
@@ -2533,6 +2658,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
function updatePopupPositionOnScroll() {
|
||||
if (!readerWordPopup || readerWordPopup.classList.contains('hidden')) return;
|
||||
if (window.innerWidth <= 768) return; // Mobile uses fixed bottom sheet
|
||||
if (!popupCurrentData || !popupCurrentData.targetElement || !popupCurrentData.targetElement.isConnected) {
|
||||
readerWordPopup.classList.add('hidden');
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user