diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 9f23112..b3e620a 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -60,6 +60,7 @@ body { align-items: center; justify-content: space-between; padding: 12px 16px; + padding-top: calc(12px + var(--safe-area-top)); background: var(--surface); border-bottom: 1px solid var(--border); position: sticky; @@ -198,29 +199,7 @@ body { transform: scale(0.95); } -/* Bottom nav */ -.bottom-nav { - display: flex; - justify-content: space-around; - padding: 10px 0; - background: var(--surface); - border-top: 1px solid var(--border); - position: sticky; - bottom: 0; -} - -.bottom-nav button { - background: none; - border: none; - color: var(--muted); - font-size: 12px; - padding: 4px 16px; - cursor: pointer; -} - -.bottom-nav button.active { - color: var(--primary); -} +/* Bottom nav — see .nav-btn below for button styles */ /* Modal / add entry overlay */ .modal-overlay { @@ -379,9 +358,11 @@ body { .bottom-nav { order: 2; + display: flex; + background: var(--surface); + border-top: 1px solid var(--border); position: sticky; bottom: 0; - padding-bottom: var(--safe-area-bottom); } .screen-content { @@ -392,18 +373,27 @@ body { } .nav-btn { + flex: 1; background: none; border: none; color: var(--muted); - font-size: 13px; - padding: 8px 12px; + font-size: 12px; + padding: 14px 4px; + padding-bottom: calc(14px + var(--safe-area-bottom)); cursor: pointer; font-family: inherit; + text-align: center; + -webkit-tap-highlight-color: rgba(187,134,252,0.15); +} + +.nav-btn:active { + background: rgba(187,134,252,0.1); } .nav-btn.active { color: var(--primary); font-weight: 600; + border-top: 2px solid var(--primary); } /* Day navigation */ @@ -647,6 +637,82 @@ body { /* Products screen */ .products-screen, .stub-screen, .settings-screen { padding: 16px; + padding-top: calc(16px + var(--safe-area-top)); +} + +.settings-section { + margin-bottom: 16px; + background: var(--surface); + border-radius: var(--radius); + overflow: hidden; +} + +.settings-section-title { + font-size: 11px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 8px 14px 4px; +} + +.settings-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + border-bottom: 1px solid var(--border); + cursor: pointer; +} + +.settings-item:last-child { border-bottom: none; } +.settings-item:active { background: rgba(255,255,255,0.03); } + +.settings-item-title { + font-size: 15px; + color: var(--on-surface); +} + +.settings-item-desc { + font-size: 12px; + color: var(--muted); + margin-top: 2px; +} + +.settings-arrow { + font-size: 20px; + color: var(--muted); +} + +.settings-toggle { + width: 44px; + height: 24px; + border-radius: 12px; + border: none; + background: var(--border); + position: relative; + cursor: pointer; + flex-shrink: 0; + transition: background 0.2s; +} + +.settings-toggle::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--on-bg); + transition: transform 0.2s; +} + +.settings-toggle.on { + background: var(--primary); +} + +.settings-toggle.on::after { + transform: translateX(20px); } .products-screen h2, .stub-screen h2, .settings-screen h2 { @@ -879,8 +945,67 @@ body { z-index: 1; } +/* Recording screen */ +.rec-screen { + display: flex; + flex-direction: column; + height: 100%; +} + +.rec-map { + flex: 1; + min-height: 300px; +} + +.rec-stats { + display: flex; + justify-content: space-around; + padding: 12px 8px; + background: var(--surface); + border-top: 1px solid var(--border); +} + +.rec-stat { + display: flex; + flex-direction: column; + align-items: center; +} + +.rec-val { + font-size: 22px; + font-weight: 700; + color: var(--on-surface); +} + +.rec-lbl { + font-size: 10px; + color: var(--muted); + text-transform: uppercase; +} + +.rec-bottom { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + padding-bottom: calc(8px + var(--safe-area-bottom)); + background: var(--surface); +} + +.rec-bottom .rec-status { + font-size: 12px; + color: var(--muted); + flex: 1; +} + +.rec-bottom .gps-rec { + padding: 12px 32px; + flex-shrink: 0; +} + .gps-header { padding: 10px 12px; + padding-top: calc(10px + var(--safe-area-top)); background: var(--surface); border-bottom: 1px solid var(--border); } @@ -1013,6 +1138,14 @@ body { border-radius: 0; } +.sd-map-full .leaflet-top { + top: var(--safe-area-top) !important; +} + +.sd-map-full .leaflet-bottom { + bottom: var(--safe-area-bottom) !important; +} + .sd-map-fs-btn { position: relative; z-index: 10000; @@ -1023,13 +1156,14 @@ body { border: 1px solid var(--border); border-radius: var(--radius); color: var(--on-bg); - width: 32px; - height: 32px; + width: 36px; + height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; - font-size: 16px; + font-size: 18px; + box-shadow: 0 2px 6px rgba(0,0,0,0.3); } .zoom-display { diff --git a/frontend/index.html b/frontend/index.html index 91dfff8..a1cbc49 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,6 +14,8 @@ then main.js uses them. Tauri injects window.__TAURI__ before page scripts. --> + + diff --git a/frontend/js/db.js b/frontend/js/db.js index d97e54b..040e8ec 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -366,13 +366,13 @@ async function getActivityTotalsForDate(date) { // --------------------------------------------------------------------------- async function createGpsSession(date, startTime, activityType) { + const id = Date.now(); await execute( - `INSERT INTO gps_sessions (date, start_time, activity_type, source) - VALUES (?1, ?2, ?3, 'recorded')`, - [date, startTime, activityType] + `INSERT INTO gps_sessions (id, date, start_time, activity_type, source) + VALUES (?1, ?2, ?3, ?4, 'recorded')`, + [id, date, startTime, activityType] ); - const rows = await select("SELECT last_insert_rowid() as id"); - return rows[0].id; + return id; } async function addGpsPoint(sessionId, ts, lat, lon, altGps, speedMs) { @@ -424,7 +424,11 @@ async function importGpxSession(session, points) { session.max_speed_kmh, session.elevation_gain_m, session.elevation_loss_m, session.source ?? "gpx_import"] ); - const rows = await select("SELECT last_insert_rowid() as id"); + // Get id of just-inserted session by matching unique fields + const rows = await select( + "SELECT id FROM gps_sessions WHERE date=?1 AND start_time=?2 AND source=?3 ORDER BY id DESC LIMIT 1", + [session.date, session.start_time, session.source ?? "gpx_import"] + ); const sid = rows[0].id; // Batch insert points — build multi-row INSERT to avoid 1 IPC per point const BATCH = 50; @@ -489,4 +493,27 @@ window.DB.getGpsPoints = getGpsPoints; window.DB.deleteGpsSession = deleteGpsSession; window.DB.importGpxSession = importGpxSession; +// JS-side migrations — safe for any DB state (checks before altering) +async function runMigrations() { + async function hasColumn(table, col) { + const info = await select(`PRAGMA table_info(${table})`); + return info.some(r => r.name === col); + } + if (!(await hasColumn("products", "composition"))) { + await execute("ALTER TABLE products ADD COLUMN composition TEXT"); + } + if (!(await hasColumn("products", "name_lower"))) { + await execute("ALTER TABLE products ADD COLUMN name_lower TEXT"); + } + if (!(await hasColumn("gps_points", "seq"))) { + await execute("ALTER TABLE gps_points ADD COLUMN seq INTEGER"); + } +} + +window.DB._migrationsReady = runMigrations().then(() => { + console.log("[db.js] migrations done"); +}).catch(e => { + console.error("[db.js] migration error:", e); +}); + console.log("[db.js] window.DB ready, keys:", Object.keys(window.DB).length); diff --git a/frontend/js/gps.js b/frontend/js/gps.js index 7dd46c5..0f38988 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -133,11 +133,15 @@ GPS.startRecording = async function(activityType, weightKg) { GPS.recording.watchId = navigator.geolocation.watchPosition( async (pos) => { + // Skip inaccurate fixes (>100m accuracy = unreliable) + if (pos.coords.accuracy > 100) return; + const p = { lat: pos.coords.latitude, lon: pos.coords.longitude, altitude: pos.coords.altitude, speed_ms: pos.coords.speed, + accuracy: pos.coords.accuracy, ts: new Date(pos.timestamp).toISOString(), }; GPS.recording.points.push(p); @@ -150,7 +154,7 @@ GPS.startRecording = async function(activityType, weightKg) { console.error("GPS error:", err.message); if (GPS.recording.onError) GPS.recording.onError(err); }, - { enableHighAccuracy: true, maximumAge: 2000, timeout: 10000 } + { enableHighAccuracy: true, maximumAge: 0, timeout: 10000 } ); }; @@ -231,8 +235,8 @@ GPS.importGPX = async function(xmlString, weightKg) { source: "gpx_import", }; - const dbPoints = points.map(p => ({ - ts: p.ts, lat: p.lat, lon: p.lon, altitude: p.altitude, speed: p.speed, + const dbPoints = points.map((p, idx) => ({ + ts: p.ts, lat: p.lat, lon: p.lon, altitude: p.altitude, speed: p.speed, seq: idx, })); return DB.importGpxSession(session, dbPoints); diff --git a/frontend/js/main.js b/frontend/js/main.js index 80aa14e..20a55e4 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -24,10 +24,22 @@ async function init() { return; } + // Wait for DB migrations + if (DB._migrationsReady) await DB._migrationsReady; + + // Apply safe area insets (Android status bar / nav bar) + try { + const insets = await window.__TAURI__.core.invoke("get_safe_insets"); + if (insets.top || insets.bottom) { + document.documentElement.style.setProperty("--safe-area-top", insets.top + "px"); + document.documentElement.style.setProperty("--safe-area-bottom", insets.bottom + "px"); + } + } catch {} + state.profile = await DB.getFullProfile(); state.latestWeight = await DB.getLatestWeight(); - if (!state.profile.height_cm) { + if (false) { renderScreen("settings"); } else { renderScreen("today"); @@ -63,7 +75,7 @@ function buildNav(active) { { id: "products", label: "Продукты" }, { id: "gps", label: "Трек" }, { id: "supplements", label: "Добавки" }, - { id: "settings", label: "Профиль" }, + { id: "settings", label: "Настройки" }, ].forEach(item => { const btn = document.createElement("button"); btn.className = "nav-btn" + (item.id === active ? " active" : ""); @@ -688,8 +700,7 @@ async function renderGpsScreen(container) {
@@ -718,20 +729,19 @@ async function renderGpsScreen(container) { // Recording button container.querySelector("#gps-rec-btn").onclick = async () => { if (GPS.recording.active) { - const result = await GPS.stopRecording(); - renderScreen("gps"); + showRecordingScreen(); } else { const type = container.querySelector("#gps-type").value; const wkg = state.latestWeight?.weight_kg ?? 80; await GPS.startRecording(type, wkg); - setupLiveUpdates(container); - container.querySelector("#gps-rec-btn").textContent = "Стоп"; - container.querySelector("#gps-rec-btn").classList.add("recording"); + showRecordingScreen(); } }; - // If already recording, hook up live updates - if (GPS.recording.active) setupLiveUpdates(container); + if (GPS.recording.active) { + // If already recording, go straight to recording screen + showRecordingScreen(); + } // Session click → detail view container.querySelectorAll(".gps-session-item").forEach(item => { @@ -759,39 +769,171 @@ async function renderGpsScreen(container) { container.querySelector("#gps-clear-cache").onclick = async () => { menu.classList.add("hidden"); const count = await TileCache.count(); - if (!confirm(`Удалить ${count} кэшированных тайлов?`)) return; await TileCache.clear(); - alert("Кэш очищен"); + console.log("Cache cleared"); }; - // GPX import - container.querySelector("#gps-import-gpx").onclick = () => { + // Unified import — detect by extension + container.querySelector("#gps-import").onclick = () => { menu.classList.add("hidden"); - pickFile(".gpx", async (text) => { + pickFile("", async (data, file) => { + console.log("[IMPORT] file:", file.name, "size:", data.byteLength || data.length, "type:", typeof data); + const ext = file.name.split(".").pop().toLowerCase(); + console.log("[IMPORT] ext:", ext); const wkg = state.latestWeight?.weight_kg ?? 80; - await GPS.importGPX(text, wkg); - renderScreen("gps"); - }); - }; - - // FitoTrack .ftb import - container.querySelector("#gps-import-ftb").onclick = () => { - menu.classList.add("hidden"); - pickFile(".ftb,.zip", async (blob, file) => { - console.log("[FTB] file:", file.name, "size:", blob.byteLength); try { - const wkg = state.latestWeight?.weight_kg ?? 80; - const count = await GPS.importFTB(blob, wkg); - alert(`Импортировано ${count} тренировок`); + if (ext === "gpx") { + const text = typeof data === "string" ? data : new TextDecoder().decode(data); + await GPS.importGPX(text, wkg); + } else if (ext === "ftb" || ext === "zip") { + const buf = data instanceof ArrayBuffer ? data : (typeof data === "string" ? new TextEncoder().encode(data).buffer : data); + console.log("[IMPORT] FTB buf size:", buf.byteLength); + const count = await GPS.importFTB(buf, wkg); + console.log("[IMPORT] done, count:", count); + } else { + console.error("[IMPORT] Unsupported file type: " + ext); + } renderScreen("gps"); } catch (e) { - console.error("[FTB] import error:", e); - alert("FTB import error: " + (e.message || e)); + console.error("[IMPORT] error:", e, String(e)); } }, true); }; } +function showRecordingScreen() { + showModal(` +