feat(mobile): implement iOS Safari responsive layout, bottom tab bar, and mobile bottom sheet

This commit is contained in:
2026-10-05 16:08:05 +03:00
parent d5e9354016
commit c0fdcd776b
4 changed files with 707 additions and 50 deletions
+128 -2
View File
@@ -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;