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(` +
+
+
+
0 мДистанция
+
0км/ч
+
0:00Время
+
—Высота
+
+
+
GPS...
+ +
+
`, + () => {}); + + // Hide modal OK/Cancel buttons + const overlay = document.querySelector(".modal-overlay"); + const btns = overlay.querySelector(".modal-btns"); + if (btns) btns.style.display = "none"; + + setTimeout(() => { + const mapEl = document.querySelector("#rec-map"); + if (!mapEl) return; + + L.Icon.Default.imagePath = "lib/"; + const map = L.map(mapEl, { attributionControl: false, scrollWheelZoom: false }).setView([55.75, 37.62], 15); + mapEl.addEventListener("wheel", (e) => { + if (e.ctrlKey) { e.preventDefault(); map.setZoom(map.getZoom() + (e.deltaY < 0 ? 1 : -1)); } + }, { passive: false }); + addZoomDisplay(map); + const CachedLayer = TileCache.createLayer(); + new CachedLayer({ maxZoom: 19 }).addTo(map); + + const borderLine = L.polyline([], { color: "#ffffff", weight: 8, opacity: 0.8 }).addTo(map); + const trailLine = L.polyline([], { color: "#bb86fc", weight: 5 }).addTo(map); + let posMarker = null; + + // Center on user + navigator.geolocation?.getCurrentPosition( + pos => { + map.setView([pos.coords.latitude, pos.coords.longitude], 15); + document.querySelector("#rec-status").textContent = "GPS OK"; + }, + () => { document.querySelector("#rec-status").textContent = "GPS не найден"; }, + { enableHighAccuracy: true, timeout: 10000 } + ); + + // Live point updates + const distEl = document.querySelector("#rec-dist"); + const spdEl = document.querySelector("#rec-speed"); + const timeEl = document.querySelector("#rec-time"); + const elevEl = document.querySelector("#rec-elev"); + const statusEl = document.querySelector("#rec-status"); + + // Speed buffer for median + const speedBuf = []; + + GPS.recording.onPoint = (p) => { + const ll = [p.lat, p.lon]; + borderLine.addLatLng(ll); + trailLine.addLatLng(ll); + map.panTo(ll); + + if (posMarker) map.removeLayer(posMarker); + posMarker = L.circleMarker(ll, { radius: 6, color: "#4285f4", fillColor: "#4285f4", fillOpacity: 1, weight: 2 }).addTo(map); + + // Stats + const pts = GPS.recording.points; + const stats = GPS.computeStats(pts, GPS.recording.weightKg); + if (distEl) distEl.textContent = GPS.formatDistance(stats.distance); + + // Speed: from device speed or computed from distance + let curSpeed = 0; + if (p.speed_ms != null && p.speed_ms >= 0) { + curSpeed = p.speed_ms * 3.6; + } else if (pts.length >= 2) { + // Fallback: compute from last 2 points + const prev = pts[pts.length - 2]; + const d = GPS.distance(prev.lat, prev.lon, p.lat, p.lon); + const dt = (new Date(p.ts) - new Date(prev.ts)) / 1000; + if (dt > 0) curSpeed = (d / dt) * 3.6; + } + speedBuf.push(curSpeed); + if (speedBuf.length > 5) speedBuf.shift(); + const sorted = speedBuf.slice().sort((a, b) => a - b); + const median = sorted[Math.floor(sorted.length / 2)] || 0; + if (spdEl) spdEl.textContent = median.toFixed(1); + + // Elevation + const alt = p.altitude; + if (alt != null && elevEl) elevEl.textContent = Math.round(alt) + " м"; + + // Pause detection — only if we have enough data and speed is actually low + if (speedBuf.length >= 3 && median < 1) { + statusEl.textContent = "Пауза"; + statusEl.style.color = "#ffb74d"; + } else { + statusEl.textContent = ""; + statusEl.style.color = ""; + } + }; + + // Timer + const timer = setInterval(() => { + if (!GPS.recording.active) { clearInterval(timer); return; } + const sec = Math.round((Date.now() - GPS.recording.startTime.getTime()) / 1000); + if (timeEl) timeEl.textContent = GPS.formatDuration(sec); + }, 1000); + + // Stop button + document.querySelector("#rec-stop").onclick = async () => { + const result = await GPS.stopRecording(); + clearInterval(timer); + // Ask for name + // TODO: custom naming modal + // Close modal + document.querySelector(".modal-overlay")?.remove(); + history.back(); + renderScreen("gps"); + }; + + // Restore trail if already recording + if (GPS.recording.points.length > 0) { + const coords = GPS.recording.points.map(p => [p.lat, p.lon]); + borderLine.setLatLngs(coords); + trailLine.setLatLngs(coords); + map.fitBounds(trailLine.getBounds().pad(0.1)); + } + }, 200); +} + function setupLiveUpdates(container) { const distEl = container.querySelector("#gs-dist"); const spdEl = container.querySelector("#gs-speed"); @@ -926,11 +1068,31 @@ function showMapDownloadModal() { // --------------------------------------------------------------------------- async function showSessionDetail(sessionId) { + try { + console.log("[SD] loading session", sessionId); const session = await DB.getGpsSession(sessionId); - if (!session) return; + if (!session) { console.error("[SD] session not found"); return; } + console.log("[SD] loading points"); const rawPoints = await DB.getGpsPoints(sessionId); + console.log("[SD] got", rawPoints.length, "raw points"); const points = GPS.filterNoise(rawPoints, session.activity_type); - const stats = GPS.computeStats(points, state.latestWeight?.weight_kg ?? 80); + console.log("[SD] filtered to", points.length, "points"); + // Use stored session stats if available (computed correctly at import time), + // fallback to recomputing from points + const storedStats = session.total_distance_m != null; + const stats = storedStats ? { + distance: Math.round(session.total_distance_m || 0), + avgSpeed: session.avg_speed_kmh || 0, + maxSpeed: session.max_speed_kmh || 0, + kcal: Math.round(session.total_kcal || 0), + elevGain: Math.round(session.elevation_gain_m || 0), + elevLoss: Math.round(session.elevation_loss_m || 0), + duration: 0, + } : GPS.computeStats(points, state.latestWeight?.weight_kg ?? 80); + // Compute duration from points regardless + if (points.length >= 2) { + stats.duration = Math.round((new Date(points[points.length-1].ts) - new Date(points[0].ts)) / 1000); + } // Speed: prefer device-reported speed (Doppler, smooth), fallback to position-based const rawSpeeds = []; @@ -944,22 +1106,26 @@ async function showSessionDetail(sessionId) { rawSpeeds.push(dt > 0 ? (d / dt) * 3.6 : 0); } } - // 9-point moving average on top + // 31-point moving average — kills GPS jitter from phone-in-pocket + const HALF_WIN = 15; const smoothed = rawSpeeds.map((_, i) => { let sum = 0, count = 0; - for (let j = Math.max(0, i - 4); j <= Math.min(rawSpeeds.length - 1, i + 4); j++) { + for (let j = Math.max(0, i - HALF_WIN); j <= Math.min(rawSpeeds.length - 1, i + HALF_WIN); j++) { sum += rawSpeeds[j]; count++; } return Math.round(sum / count * 10) / 10; }); + // maxSpeed from smoothed graph (not raw) + const graphMaxSpeed = smoothed.length ? Math.max(...smoothed) : 0; + // Elevation data const elevations = points.filter(p => p.altitude_gps_m != null).map(p => p.altitude_gps_m); const minElev = elevations.length ? Math.round(Math.min(...elevations)) : null; const maxElev = elevations.length ? Math.round(Math.max(...elevations)) : null; // Time labels (minutes from start) - const t0 = new Date(points[0]?.ts).getTime(); + const t0 = points.length ? new Date(points[0].ts).getTime() : 0; const timeLabels = points.slice(1).map(p => ((new Date(p.ts).getTime() - t0) / 60000).toFixed(1)); const actNames = { bike: "Велосипед", walk: "Ходьба", run: "Бег", other: "Другое" }; @@ -1000,7 +1166,7 @@ async function showSessionDetail(sessionId) {
Скорость
${stats.avgSpeed} км/чСредняя
-
${stats.maxSpeed} км/чМакс.
+
${stats.maxSpeed} км/чМакс.
@@ -1036,7 +1202,11 @@ async function showSessionDetail(sessionId) { if (!mapEl || !points.length) return; L.Icon.Default.imagePath = "lib/"; - const map = L.map(mapEl, { attributionControl: false, zoomControl: false }); + const map = L.map(mapEl, { attributionControl: false, zoomControl: false, scrollWheelZoom: false }); + // Ctrl+wheel zoom on desktop + mapEl.addEventListener("wheel", (e) => { + if (e.ctrlKey) { e.preventDefault(); map.setZoom(map.getZoom() + (e.deltaY < 0 ? 1 : -1)); } + }, { passive: false }); addZoomDisplay(map); const CachedLayer = TileCache.createLayer(); new CachedLayer({ maxZoom: 19 }).addTo(map); @@ -1126,7 +1296,7 @@ async function showSessionDetail(sessionId) { map.setView(ll, 15); L.circleMarker(ll, { radius: 8, color: "#4285f4", fillColor: "#4285f4", fillOpacity: 0.8, weight: 2 }).addTo(map); }, - err => alert("GPS: " + err.message), + err => console.error("GPS error:", err.message), { enableHighAccuracy: true, timeout: 10000 } ); }; @@ -1144,6 +1314,9 @@ async function showSessionDetail(sessionId) { setTimeout(() => { const ctx = document.querySelector("#sd-speed-chart"); if (!ctx || !smoothed.length) return; + // Update maxSpeed display to match smoothed graph + const maxEl = document.querySelector("#sd-max-speed"); + if (maxEl) maxEl.textContent = graphMaxSpeed + " км/ч"; new Chart(ctx, { type: "line", data: { @@ -1159,7 +1332,14 @@ async function showSessionDetail(sessionId) { }, options: { responsive: true, - plugins: { legend: { display: false } }, + plugins: { + legend: { display: false }, + zoom: { + pan: { enabled: true, mode: "x" }, + zoom: { wheel: { enabled: true, modifierKey: "ctrl" }, pinch: { enabled: true }, drag: { enabled: false }, mode: "x" }, + pan: { enabled: false }, + }, + }, scales: { x: { display: true, title: { display: true, text: "мин", color: "#888" }, ticks: { color: "#888", maxTicksLimit: 8 }, grid: { color: "#333" } }, @@ -1192,7 +1372,14 @@ async function showSessionDetail(sessionId) { }, options: { responsive: true, - plugins: { legend: { display: false } }, + plugins: { + legend: { display: false }, + zoom: { + pan: { enabled: true, mode: "x" }, + zoom: { wheel: { enabled: true, modifierKey: "ctrl" }, pinch: { enabled: true }, drag: { enabled: false }, mode: "x" }, + pan: { enabled: false }, + }, + }, scales: { x: { display: true, title: { display: true, text: "мин", color: "#888" }, ticks: { color: "#888", maxTicksLimit: 8 }, grid: { color: "#333" } }, @@ -1207,13 +1394,13 @@ async function showSessionDetail(sessionId) { setTimeout(() => { const del = document.querySelector("#sd-delete"); if (del) del.onclick = async () => { - if (!confirm("Удалить тренировку?")) return; await DB.deleteGpsSession(sessionId); document.querySelector(".modal-overlay")?.remove(); history.back(); renderScreen("gps"); }; }, 100); + } catch(e) { console.error("[showSessionDetail] error:", e, JSON.stringify(e)); } } // --------------------------------------------------------------------------- @@ -1409,84 +1596,149 @@ function showEditSupplementModal(existing) { async function renderSettings(container) { const p = state.profile; + const profileSummary = [ + p.height_cm ? `${p.height_cm} см` : null, + p.birth_year ? `${p.birth_year} г.р.` : null, + p.sex === "f" ? "Ж" : "М", + ].filter(Boolean).join(", "); container.innerHTML = `
-

Профиль

-
- - -
-
- - -
-
- -
- - +

Настройки

+ +
+
+
+
Профиль
+
${profileSummary || "Не заполнен"}
+
+ ›
-
- - -
-
- - -
-
- -
- - + +
+
Карты
+
+
+
Оффлайн режим
+
Тайлы только из кэша
+
+ +
+
+ +
+
Разрешения
+
+
+
Геолокация (GPS)
+
Проверка...
+
+ +
+
+ +
+
О приложении
+
+
+
BalDav
+
Версия ${document.querySelector("meta[name=version]")?.content || "dev"}
+
- Оффлайн: тайлы только из кэша, интернет не используется
-
`; + // Profile button → open profile modal + container.querySelector("#s-open-profile").onclick = () => showProfileModal(); + + // Offline toggle + const offToggle = container.querySelector("#s-offline-toggle"); + offToggle.onclick = () => { + const newVal = !TileCache.offlineMode; + TileCache.setOfflineMode(newVal); + offToggle.classList.toggle("on", newVal); + }; + + // GPS permission check + const gpsStatus = container.querySelector("#s-gps-status"); + const gpsToggle = container.querySelector("#s-gps-toggle"); + if (navigator.permissions && navigator.permissions.query) { + try { + const result = await navigator.permissions.query({ name: "geolocation" }); + gpsStatus.textContent = result.state === "granted" ? "Разрешено" : result.state === "denied" ? "Запрещено" : "Не запрошено"; + gpsToggle.classList.toggle("on", result.state === "granted"); + } catch { gpsStatus.textContent = "Недоступно"; } + } + gpsToggle.onclick = () => { + navigator.geolocation.getCurrentPosition( + () => { gpsStatus.textContent = "Разрешено"; gpsToggle.classList.add("on"); }, + () => { gpsStatus.textContent = "Запрещено"; gpsToggle.classList.remove("on"); }, + { enableHighAccuracy: true, timeout: 5000 } + ); + }; +} + +function showProfileModal() { + const p = state.profile; let selectedSex = p.sex ?? "m"; - container.querySelectorAll("[data-sex]").forEach(btn => { - btn.onclick = () => { - selectedSex = btn.dataset.sex; - container.querySelectorAll("[data-sex]").forEach(b => b.classList.remove("selected")); - btn.classList.add("selected"); - }; - }); - container.querySelectorAll("[data-map]").forEach(btn => { - btn.onclick = () => { - container.querySelectorAll("[data-map]").forEach(b => b.classList.remove("selected")); - btn.classList.add("selected"); - TileCache.setOfflineMode(btn.dataset.map === "offline"); - }; - }); + showModal(` +

Профиль

+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
`, + async overlay => { + const height = parseInt(overlay.querySelector("#m-height").value); + const year = parseInt(overlay.querySelector("#m-year").value); + const bFormula = parseInt(overlay.querySelector("#m-bmr-formula").value) || null; + const bEst = parseInt(overlay.querySelector("#m-bmr-est").value) || null; + if (isNaN(height) || isNaN(year)) return false; - container.querySelector("#save-profile").onclick = async () => { - const height = parseInt(container.querySelector("#s-height").value); - const year = parseInt(container.querySelector("#s-year").value); - const bFormula = parseInt(container.querySelector("#s-bmr-formula").value) || null; - const bEst = parseInt(container.querySelector("#s-bmr-est").value) || null; - if (isNaN(height) || isNaN(year)) { alert("Введите рост и год рождения"); return; } - - await DB.setProfileValue("height_cm", height); + await DB.setProfileValue("height_cm", height); await DB.setProfileValue("birth_year", year); - await DB.setProfileValue("sex", selectedSex); - if (bFormula) await DB.setProfileValue("bmr_formula", bFormula); - if (bEst) await DB.setProfileValue("bmr_estimated", bEst); + await DB.setProfileValue("sex", selectedSex); + if (bFormula) await DB.setProfileValue("bmr_formula", bFormula); + if (bEst) await DB.setProfileValue("bmr_estimated", bEst); state.profile = { ...state.profile, height_cm: String(height), birth_year: String(year), sex: selectedSex, bmr_formula: bFormula ? String(bFormula) : null, bmr_estimated: bEst ? String(bEst) : null }; - renderScreen("today"); - }; + }); + + // Sex chip handler inside modal + setTimeout(() => { + const overlay = document.querySelector(".modal-overlay"); + if (!overlay) return; + overlay.querySelectorAll("[data-sex]").forEach(btn => { + btn.onclick = () => { + selectedSex = btn.dataset.sex; + overlay.querySelectorAll("[data-sex]").forEach(b => b.classList.remove("selected")); + btn.classList.add("selected"); + }; + }); + }, 100); } // --------------------------------------------------------------------------- @@ -1588,12 +1840,18 @@ function addZoomDisplay(map) { } function pickFile(accept, callback, asBinary) { + console.log("[pickFile] accept:", accept, "asBinary:", asBinary); const input = document.createElement("input"); input.type = "file"; - input.accept = accept; - input.onchange = async () => { + if (accept) input.accept = accept; + input.style.display = "none"; + document.body.appendChild(input); + input.addEventListener("change", async () => { + console.log("[pickFile] change event, files:", input.files.length); const file = input.files[0]; + document.body.removeChild(input); if (!file) return; + console.log("[pickFile] file:", file.name, file.size); try { if (asBinary) { const buf = await file.arrayBuffer(); @@ -1603,9 +1861,9 @@ function pickFile(accept, callback, asBinary) { await callback(text, file); } } catch (e) { - alert("Import error: " + e.message); + console.error("[pickFile] error:", e); } - }; + }); input.click(); } diff --git a/frontend/lib/chartjs-plugin-zoom.min.js b/frontend/lib/chartjs-plugin-zoom.min.js new file mode 100644 index 0000000..a6c5577 --- /dev/null +++ b/frontend/lib/chartjs-plugin-zoom.min.js @@ -0,0 +1,7 @@ +/*! +* chartjs-plugin-zoom v2.0.1 +* undefined + * (c) 2016-2023 chartjs-plugin-zoom Contributors + * Released under the MIT License + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("chart.js"),require("hammerjs"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","hammerjs","chart.js/helpers"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).ChartZoom=t(e.Chart,e.Hammer,e.Chart.helpers)}(this,(function(e,t,n){"use strict";function o(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var a=o(t);const i=e=>e&&e.enabled&&e.modifierKey,c=(e,t)=>e&&t[e+"Key"],r=(e,t)=>e&&!t[e+"Key"];function s(e,t,n){return void 0===e||("string"==typeof e?-1!==e.indexOf(t):"function"==typeof e&&-1!==e({chart:n}).indexOf(t))}function l(e,t){return"function"==typeof e&&(e=e({chart:t})),"string"==typeof e?{x:-1!==e.indexOf("x"),y:-1!==e.indexOf("y")}:{x:!1,y:!1}}function u(e,t,o){const{mode:a="xy",scaleMode:i,overScaleMode:c}=e||{},r=function({x:e,y:t},n){const o=n.scales,a=Object.keys(o);for(let n=0;n=i.top&&t<=i.bottom&&e>=i.left&&e<=i.right)return i}return null}(t,o),s=l(a,o),u=l(i,o);if(c){const e=l(c,o);for(const t of["x","y"])e[t]&&(u[t]=s[t],s[t]=!1)}if(r&&u[r.axis])return[r];const m=[];return n.each(o.scales,(function(e){s[e.axis]&&m.push(e)})),m}const m=new WeakMap;function d(e){let t=m.get(e);return t||(t={originalScaleLimits:{},updatedScaleLimits:{},handlers:{},panDelta:{}},m.set(e,t)),t}function f(e,t,n){const o=e.max-e.min,a=o*(t-1),i=e.isHorizontal()?n.x:n.y,c=Math.max(0,Math.min(1,(e.getValueForPixel(i)-e.min)/o||0));return{min:a*c,max:a*(1-c)}}function p(e,t,o,a,i){let c=o[a];if("original"===c){const o=e.originalScaleLimits[t.id][a];c=n.valueOrDefault(o.options,o.scale)}return n.valueOrDefault(c,i)}function h(e,{min:t,max:n},o,a=!1){const i=d(e.chart),{id:c,axis:r,options:s}=e,l=o&&(o[c]||o[r])||{},{minRange:u=0}=l,m=p(i,e,l,"min",-1/0),f=p(i,e,l,"max",1/0),h=a?Math.max(n-t,u):e.max-e.min,x=(h-n+t)/2;return n+=x,(t-=x)f&&(n=f,t=Math.max(f-h,m)),s.min=t,s.max=n,i.updatedScaleLimits[e.id]={min:t,max:n},e.parse(t)!==e.min||e.parse(n)!==e.max}const x=e=>0===e||isNaN(e)?0:e<0?Math.min(Math.round(e),-1):Math.max(Math.round(e),1);const g={second:500,minute:3e4,hour:18e5,day:432e5,week:3024e5,month:1296e6,quarter:5184e6,year:157248e5};function y(e,t,n,o=!1){const{min:a,max:i,options:c}=e,r=c.time&&c.time.round,s=g[r]||0,l=e.getValueForPixel(e.getPixelForValue(a+s)-t),u=e.getValueForPixel(e.getPixelForValue(i+s)-t),{min:m=-1/0,max:d=1/0}=o&&n&&n[e.axis]||{};return!!(isNaN(l)||isNaN(u)||ld)||h(e,{min:l,max:u},n,o)}function b(e,t,n){return y(e,t,n,!0)}const v={category:function(e,t,n,o){const a=f(e,t,n);return e.min===e.max&&t<1&&function(e){const t=e.getLabels().length-1;e.min>0&&(e.min-=1),e.maxr&&(a=Math.max(0,a-s),i=1===c?a:a+c,l=0===a),h(e,{min:a,max:i},n)||l},default:y,logarithmic:b,timeseries:b};function M(e,t){n.each(e,((n,o)=>{t[o]||delete e[o]}))}function k(e,t){const{scales:o}=e,{originalScaleLimits:a,updatedScaleLimits:i}=t;return n.each(o,(function(e){(function(e,t,n){const{id:o,options:{min:a,max:i}}=e;if(!t[o]||!n[o])return!0;const c=n[o];return c.min!==a||c.max!==i})(e,a,i)&&(a[e.id]={min:{scale:e.min,options:e.options.min},max:{scale:e.max,options:e.options.max}})})),M(a,o),M(i,o),a}function S(e,t,o,a){const i=v[e.type]||v.default;n.callback(i,[e,t,o,a])}function P(e,t,o,a,i){const c=w[e.type]||w.default;n.callback(c,[e,t,o,a,i])}function D(e){const t=e.chartArea;return{x:(t.left+t.right)/2,y:(t.top+t.bottom)/2}}function j(e,t,o="none"){const{x:a=1,y:i=1,focalPoint:c=D(e)}="number"==typeof t?{x:t,y:t}:t,r=d(e),{options:{limits:s,zoom:l}}=r;k(e,r);const m=1!==a,f=1!==i,p=u(l,c,e);n.each(p||e.scales,(function(e){e.isHorizontal()&&m?S(e,a,c,s):!e.isHorizontal()&&f&&S(e,i,c,s)})),e.update(o),n.callback(l.onZoom,[{chart:e}])}function O(e,t,o,a="none"){const i=d(e),{options:{limits:c,zoom:r}}=i,{mode:l="xy"}=r;k(e,i);const u=s(l,"x",e),m=s(l,"y",e);n.each(e.scales,(function(e){e.isHorizontal()&&u?P(e,t.x,o.x,c):!e.isHorizontal()&&m&&P(e,t.y,o.y,c)})),e.update(a),n.callback(r.onZoom,[{chart:e}])}function C(e){const t=d(e);let o=1,a=1;return n.each(e.scales,(function(e){const i=function(e,t){const o=e.originalScaleLimits[t];if(!o)return;const{min:a,max:i}=o;return n.valueOrDefault(i.options,i.scale)-n.valueOrDefault(a.options,a.scale)}(t,e.id);if(i){const t=Math.round(i/(e.max-e.min)*100)/100;o=Math.min(o,t),a=Math.max(a,t)}})),o<1?o:a}function R(e,t,o,a){const{panDelta:i}=a,c=i[e.id]||0;n.sign(c)===n.sign(t)&&(t+=c);const r=z[e.type]||z.default;n.callback(r,[e,t,o])?i[e.id]=0:i[e.id]=t}function Z(e,t,o,a="none"){const{x:i=0,y:c=0}="number"==typeof t?{x:t,y:t}:t,r=d(e),{options:{pan:s,limits:l}}=r,{onPan:u}=s||{};k(e,r);const m=0!==i,f=0!==c;n.each(o||e.scales,(function(e){e.isHorizontal()&&m?R(e,i,l,r):!e.isHorizontal()&&f&&R(e,c,l,r)})),e.update(a),n.callback(u,[{chart:e}])}function T(e){const t=d(e);k(e,t);const n={};for(const o of Object.keys(e.scales)){const{min:e,max:a}=t.originalScaleLimits[o]||{min:{},max:{}};n[o]={min:e.scale,max:a.scale}}return n}function L(e,t){const{handlers:n}=d(e),o=n[t];o&&o.target&&(o.target.removeEventListener(t,o),delete n[t])}function E(e,t,n,o){const{handlers:a,options:i}=d(e),c=a[n];c&&c.target===t||(L(e,n),a[n]=t=>o(e,t,i),a[n].target=t,t.addEventListener(n,a[n]))}function F(e,t){const n=d(e);n.dragStart&&(n.dragging=!0,n.dragEnd=t,e.update("none"))}function H(e,t){const n=d(e);n.dragStart&&"Escape"===t.key&&(L(e,"keydown"),n.dragging=!1,n.dragStart=n.dragEnd=null,e.update("none"))}function Y(e,t,o){const{onZoomStart:a,onZoomRejected:i}=o;if(a){const o=n.getRelativePosition(t,e);if(!1===n.callback(a,[{chart:e,event:t,point:o}]))return n.callback(i,[{chart:e,event:t}]),!1}}function V(e,t){const o=d(e),{pan:a,zoom:s={}}=o.options;if(0!==t.button||c(i(a),t)||r(i(s.drag),t))return n.callback(s.onZoomRejected,[{chart:e,event:t}]);!1!==Y(e,t,s)&&(o.dragStart=t,E(e,e.canvas,"mousemove",F),E(e,window.document,"keydown",H))}function K(e,t,o,a){const i=s(t,"x",e),c=s(t,"y",e);let{top:r,left:l,right:u,bottom:m,width:d,height:f}=e.chartArea;const p=n.getRelativePosition(o,e),h=n.getRelativePosition(a,e);i&&(l=Math.min(p.x,h.x),u=Math.max(p.x,h.x)),c&&(r=Math.min(p.y,h.y),m=Math.max(p.y,h.y));const x=u-l,g=m-r;return{left:l,top:r,right:u,bottom:m,width:x,height:g,zoomX:i&&x?1+(d-x)/d:1,zoomY:c&&g?1+(f-g)/f:1}}function N(e,t){const o=d(e);if(!o.dragStart)return;L(e,"mousemove");const{mode:a,onZoomComplete:i,drag:{threshold:c=0}}=o.options.zoom,r=K(e,a,o.dragStart,t),l=s(a,"x",e)?r.width:0,u=s(a,"y",e)?r.height:0,m=Math.sqrt(l*l+u*u);if(o.dragStart=o.dragEnd=null,m<=c)return o.dragging=!1,void e.update("none");O(e,{x:r.left,y:r.top},{x:r.right,y:r.bottom},"zoom"),setTimeout((()=>o.dragging=!1),500),n.callback(i,[{chart:e}])}function X(e,t){const{handlers:{onZoomComplete:o},options:{zoom:a}}=d(e);if(!function(e,t,o){if(r(i(o.wheel),t))n.callback(o.onZoomRejected,[{chart:e,event:t}]);else if(!1!==Y(e,t,o)&&(t.cancelable&&t.preventDefault(),void 0!==t.deltaY))return!0}(e,t,a))return;const c=t.target.getBoundingClientRect(),s=1+(t.deltaY>=0?-a.wheel.speed:a.wheel.speed);j(e,{x:s,y:s,focalPoint:{x:t.clientX-c.left,y:t.clientY-c.top}}),o&&o()}function q(e,t,o,a){o&&(d(e).handlers[t]=function(e,t){let n;return function(){return clearTimeout(n),n=setTimeout(e,t),t}}((()=>n.callback(o,[{chart:e}])),a))}function W(e,t){return function(o,a){const{pan:s,zoom:l={}}=t.options;if(!s||!s.enabled)return!1;const u=a&&a.srcEvent;return!u||(!(!t.panning&&"mouse"===a.pointerType&&(r(i(s),u)||c(i(l.drag),u)))||(n.callback(s.onPanRejected,[{chart:e,event:a}]),!1))}}function B(e,t,n){if(t.scale){const{center:o,pointers:a}=n,i=1/t.scale*n.scale,c=n.target.getBoundingClientRect(),r=function(e,t){const n=Math.abs(e.clientX-t.clientX),o=Math.abs(e.clientY-t.clientY),a=n/o;let i,c;return a>.3&&a<1.7?i=c=!0:n>o?i=!0:c=!0,{x:i,y:c}}(a[0],a[1]),l=t.options.zoom.mode;j(e,{x:r.x&&s(l,"x",e)?i:1,y:r.y&&s(l,"y",e)?i:1,focalPoint:{x:o.x-c.left,y:o.y-c.top}}),t.scale=n.scale}}function A(e,t,n){const o=t.delta;o&&(t.panning=!0,Z(e,{x:n.deltaX-o.x,y:n.deltaY-o.y},t.panScales),t.delta={x:n.deltaX,y:n.deltaY})}const I=new WeakMap;function U(e,t){const o=d(e),i=e.canvas,{pan:c,zoom:r}=t,s=new a.default.Manager(i);r&&r.pinch.enabled&&(s.add(new a.default.Pinch),s.on("pinchstart",(()=>function(e,t){t.options.zoom.pinch.enabled&&(t.scale=1)}(0,o))),s.on("pinch",(t=>B(e,o,t))),s.on("pinchend",(t=>function(e,t,o){t.scale&&(B(e,t,o),t.scale=null,n.callback(t.options.zoom.onZoomComplete,[{chart:e}]))}(e,o,t)))),c&&c.enabled&&(s.add(new a.default.Pan({threshold:c.threshold,enable:W(e,o)})),s.on("panstart",(t=>function(e,t,o){const{enabled:a,onPanStart:i,onPanRejected:c}=t.options.pan;if(!a)return;const r=o.target.getBoundingClientRect(),s={x:o.center.x-r.left,y:o.center.y-r.top};if(!1===n.callback(i,[{chart:e,event:o,point:s}]))return n.callback(c,[{chart:e,event:o}]);t.panScales=u(t.options.pan,s,e),t.delta={x:0,y:0},clearTimeout(t.panEndTimeout),A(e,t,o)}(e,o,t))),s.on("panmove",(t=>A(e,o,t))),s.on("panend",(()=>function(e,t){t.delta=null,t.panning&&(t.panEndTimeout=setTimeout((()=>t.panning=!1),500),n.callback(t.options.pan.onPanComplete,[{chart:e}]))}(e,o)))),I.set(e,s)}function G(e,t,n){const o=n.zoom.drag,{dragStart:a,dragEnd:i}=d(e);if(o.drawTime!==t||!i)return;const{left:c,top:r,width:s,height:l}=K(e,n.zoom.mode,a,i),u=e.ctx;u.save(),u.beginPath(),u.fillStyle=o.backgroundColor||"rgba(225,225,225,0.3)",u.fillRect(c,r,s,l),o.borderWidth>0&&(u.lineWidth=o.borderWidth,u.strokeStyle=o.borderColor||"rgba(225,225,225)",u.strokeRect(c,r,s,l)),u.restore()}var J={id:"zoom",version:"2.0.1",defaults:{pan:{enabled:!1,mode:"xy",threshold:10,modifierKey:null},zoom:{wheel:{enabled:!1,speed:.1,modifierKey:null},drag:{enabled:!1,drawTime:"beforeDatasetsDraw",modifierKey:null},pinch:{enabled:!1},mode:"xy"}},start:function(e,t,o){d(e).options=o,Object.prototype.hasOwnProperty.call(o.zoom,"enabled")&&console.warn("The option `zoom.enabled` is no longer supported. Please use `zoom.wheel.enabled`, `zoom.drag.enabled`, or `zoom.pinch.enabled`."),(Object.prototype.hasOwnProperty.call(o.zoom,"overScaleMode")||Object.prototype.hasOwnProperty.call(o.pan,"overScaleMode"))&&console.warn("The option `overScaleMode` is deprecated. Please use `scaleMode` instead (and update `mode` as desired)."),a.default&&U(e,o),e.pan=(t,n,o)=>Z(e,t,n,o),e.zoom=(t,n)=>j(e,t,n),e.zoomRect=(t,n,o)=>O(e,t,n,o),e.zoomScale=(t,n,o)=>function(e,t,n,o="none"){k(e,d(e)),h(e.scales[t],n,void 0,!0),e.update(o)}(e,t,n,o),e.resetZoom=t=>function(e,t="default"){const o=d(e),a=k(e,o);n.each(e.scales,(function(e){const t=e.options;a[e.id]?(t.min=a[e.id].min.options,t.max=a[e.id].max.options):(delete t.min,delete t.max)})),e.update(t),n.callback(o.options.zoom.onZoomComplete,[{chart:e}])}(e,t),e.getZoomLevel=()=>C(e),e.getInitialScaleBounds=()=>T(e),e.isZoomedOrPanned=()=>function(e){const t=T(e);for(const n of Object.keys(e.scales)){const{min:o,max:a}=t[n];if(void 0!==o&&e.scales[n].min!==o)return!0;if(void 0!==a&&e.scales[n].max!==a)return!0}return!1}(e)},beforeEvent(e){const t=d(e);if(t.panning||t.dragging)return!1},beforeUpdate:function(e,t,n){d(e).options=n,function(e,t){const n=e.canvas,{wheel:o,drag:a,onZoomComplete:i}=t.zoom;o.enabled?(E(e,n,"wheel",X),q(e,"onZoomComplete",i,250)):L(e,"wheel"),a.enabled?(E(e,n,"mousedown",V),E(e,n.ownerDocument,"mouseup",N)):(L(e,"mousedown"),L(e,"mousemove"),L(e,"mouseup"),L(e,"keydown"))}(e,n)},beforeDatasetsDraw(e,t,n){G(e,"beforeDatasetsDraw",n)},afterDatasetsDraw(e,t,n){G(e,"afterDatasetsDraw",n)},beforeDraw(e,t,n){G(e,"beforeDraw",n)},afterDraw(e,t,n){G(e,"afterDraw",n)},stop:function(e){!function(e){L(e,"mousedown"),L(e,"mousemove"),L(e,"mouseup"),L(e,"wheel"),L(e,"click"),L(e,"keydown")}(e),a.default&&function(e){const t=I.get(e);t&&(t.remove("pinchstart"),t.remove("pinch"),t.remove("pinchend"),t.remove("panstart"),t.remove("pan"),t.remove("panend"),t.destroy(),I.delete(e))}(e),function(e){m.delete(e)}(e)},panFunctions:z,zoomFunctions:v,zoomRectFunctions:w};return e.Chart.register(J),J})); diff --git a/frontend/lib/hammer.min.js b/frontend/lib/hammer.min.js new file mode 100644 index 0000000..34a8c86 --- /dev/null +++ b/frontend/lib/hammer.min.js @@ -0,0 +1,7 @@ +/*! Hammer.JS - v2.0.7 - 2016-04-22 + * http://hammerjs.github.io/ + * + * Copyright (c) 2016 Jorik Tangelder; + * Licensed under the MIT license */ +!function(a,b,c,d){"use strict";function e(a,b,c){return setTimeout(j(a,c),b)}function f(a,b,c){return Array.isArray(a)?(g(a,c[b],c),!0):!1}function g(a,b,c){var e;if(a)if(a.forEach)a.forEach(b,c);else if(a.length!==d)for(e=0;e\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",f=a.console&&(a.console.warn||a.console.log);return f&&f.call(a.console,e,d),b.apply(this,arguments)}}function i(a,b,c){var d,e=b.prototype;d=a.prototype=Object.create(e),d.constructor=a,d._super=e,c&&la(d,c)}function j(a,b){return function(){return a.apply(b,arguments)}}function k(a,b){return typeof a==oa?a.apply(b?b[0]||d:d,b):a}function l(a,b){return a===d?b:a}function m(a,b,c){g(q(b),function(b){a.addEventListener(b,c,!1)})}function n(a,b,c){g(q(b),function(b){a.removeEventListener(b,c,!1)})}function o(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1}function p(a,b){return a.indexOf(b)>-1}function q(a){return a.trim().split(/\s+/g)}function r(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;dc[b]}):d.sort()),d}function u(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g1&&!c.firstMultiple?c.firstMultiple=D(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=E(d);b.timeStamp=ra(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=I(h,i),b.distance=H(h,i),B(c,b),b.offsetDirection=G(b.deltaX,b.deltaY);var j=F(b.deltaTime,b.deltaX,b.deltaY);b.overallVelocityX=j.x,b.overallVelocityY=j.y,b.overallVelocity=qa(j.x)>qa(j.y)?j.x:j.y,b.scale=g?K(g.pointers,d):1,b.rotation=g?J(g.pointers,d):0,b.maxPointers=c.prevInput?b.pointers.length>c.prevInput.maxPointers?b.pointers.length:c.prevInput.maxPointers:b.pointers.length,C(c,b);var k=a.element;o(b.srcEvent.target,k)&&(k=b.srcEvent.target),b.target=k}function B(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};b.eventType!==Ea&&f.eventType!==Ga||(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function C(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Ha&&(i>Da||h.velocity===d)){var j=b.deltaX-h.deltaX,k=b.deltaY-h.deltaY,l=F(i,j,k);e=l.x,f=l.y,c=qa(l.x)>qa(l.y)?l.x:l.y,g=G(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function D(a){for(var b=[],c=0;ce;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:pa(c/b),y:pa(d/b)}}function F(a,b,c){return{x:b/a||0,y:c/a||0}}function G(a,b){return a===b?Ia:qa(a)>=qa(b)?0>a?Ja:Ka:0>b?La:Ma}function H(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function I(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function J(a,b){return I(b[1],b[0],Ra)+I(a[1],a[0],Ra)}function K(a,b){return H(b[0],b[1],Ra)/H(a[0],a[1],Ra)}function L(){this.evEl=Ta,this.evWin=Ua,this.pressed=!1,x.apply(this,arguments)}function M(){this.evEl=Xa,this.evWin=Ya,x.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function N(){this.evTarget=$a,this.evWin=_a,this.started=!1,x.apply(this,arguments)}function O(a,b){var c=s(a.touches),d=s(a.changedTouches);return b&(Ga|Ha)&&(c=t(c.concat(d),"identifier",!0)),[c,d]}function P(){this.evTarget=bb,this.targetIds={},x.apply(this,arguments)}function Q(a,b){var c=s(a.touches),d=this.targetIds;if(b&(Ea|Fa)&&1===c.length)return d[c[0].identifier]=!0,[c,c];var e,f,g=s(a.changedTouches),h=[],i=this.target;if(f=c.filter(function(a){return o(a.target,i)}),b===Ea)for(e=0;e-1&&d.splice(a,1)};setTimeout(e,cb)}}function U(a){for(var b=a.srcEvent.clientX,c=a.srcEvent.clientY,d=0;d=f&&db>=g)return!0}return!1}function V(a,b){this.manager=a,this.set(b)}function W(a){if(p(a,jb))return jb;var b=p(a,kb),c=p(a,lb);return b&&c?jb:b||c?b?kb:lb:p(a,ib)?ib:hb}function X(){if(!fb)return!1;var b={},c=a.CSS&&a.CSS.supports;return["auto","manipulation","pan-y","pan-x","pan-x pan-y","none"].forEach(function(d){b[d]=c?a.CSS.supports("touch-action",d):!0}),b}function Y(a){this.options=la({},this.defaults,a||{}),this.id=v(),this.manager=null,this.options.enable=l(this.options.enable,!0),this.state=nb,this.simultaneous={},this.requireFail=[]}function Z(a){return a&sb?"cancel":a&qb?"end":a&pb?"move":a&ob?"start":""}function $(a){return a==Ma?"down":a==La?"up":a==Ja?"left":a==Ka?"right":""}function _(a,b){var c=b.manager;return c?c.get(a):a}function aa(){Y.apply(this,arguments)}function ba(){aa.apply(this,arguments),this.pX=null,this.pY=null}function ca(){aa.apply(this,arguments)}function da(){Y.apply(this,arguments),this._timer=null,this._input=null}function ea(){aa.apply(this,arguments)}function fa(){aa.apply(this,arguments)}function ga(){Y.apply(this,arguments),this.pTime=!1,this.pCenter=!1,this._timer=null,this._input=null,this.count=0}function ha(a,b){return b=b||{},b.recognizers=l(b.recognizers,ha.defaults.preset),new ia(a,b)}function ia(a,b){this.options=la({},ha.defaults,b||{}),this.options.inputTarget=this.options.inputTarget||a,this.handlers={},this.session={},this.recognizers=[],this.oldCssProps={},this.element=a,this.input=y(this),this.touchAction=new V(this,this.options.touchAction),ja(this,!0),g(this.options.recognizers,function(a){var b=this.add(new a[0](a[1]));a[2]&&b.recognizeWith(a[2]),a[3]&&b.requireFailure(a[3])},this)}function ja(a,b){var c=a.element;if(c.style){var d;g(a.options.cssProps,function(e,f){d=u(c.style,f),b?(a.oldCssProps[d]=c.style[d],c.style[d]=e):c.style[d]=a.oldCssProps[d]||""}),b||(a.oldCssProps={})}}function ka(a,c){var d=b.createEvent("Event");d.initEvent(a,!0,!0),d.gesture=c,c.target.dispatchEvent(d)}var la,ma=["","webkit","Moz","MS","ms","o"],na=b.createElement("div"),oa="function",pa=Math.round,qa=Math.abs,ra=Date.now;la="function"!=typeof Object.assign?function(a){if(a===d||null===a)throw new TypeError("Cannot convert undefined or null to object");for(var b=Object(a),c=1;ch&&(b.push(a),h=b.length-1):e&(Ga|Ha)&&(c=!0),0>h||(b[h]=a,this.callback(this.manager,e,{pointers:b,changedPointers:[a],pointerType:f,srcEvent:a}),c&&b.splice(h,1))}});var Za={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},$a="touchstart",_a="touchstart touchmove touchend touchcancel";i(N,x,{handler:function(a){var b=Za[a.type];if(b===Ea&&(this.started=!0),this.started){var c=O.call(this,a,b);b&(Ga|Ha)&&c[0].length-c[1].length===0&&(this.started=!1),this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}}});var ab={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},bb="touchstart touchmove touchend touchcancel";i(P,x,{handler:function(a){var b=ab[a.type],c=Q.call(this,a,b);c&&this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}});var cb=2500,db=25;i(R,x,{handler:function(a,b,c){var d=c.pointerType==za,e=c.pointerType==Ba;if(!(e&&c.sourceCapabilities&&c.sourceCapabilities.firesTouchEvents)){if(d)S.call(this,b,c);else if(e&&U.call(this,c))return;this.callback(a,b,c)}},destroy:function(){this.touch.destroy(),this.mouse.destroy()}});var eb=u(na.style,"touchAction"),fb=eb!==d,gb="compute",hb="auto",ib="manipulation",jb="none",kb="pan-x",lb="pan-y",mb=X();V.prototype={set:function(a){a==gb&&(a=this.compute()),fb&&this.manager.element.style&&mb[a]&&(this.manager.element.style[eb]=a),this.actions=a.toLowerCase().trim()},update:function(){this.set(this.manager.options.touchAction)},compute:function(){var a=[];return g(this.manager.recognizers,function(b){k(b.options.enable,[b])&&(a=a.concat(b.getTouchAction()))}),W(a.join(" "))},preventDefaults:function(a){var b=a.srcEvent,c=a.offsetDirection;if(this.manager.session.prevented)return void b.preventDefault();var d=this.actions,e=p(d,jb)&&!mb[jb],f=p(d,lb)&&!mb[lb],g=p(d,kb)&&!mb[kb];if(e){var h=1===a.pointers.length,i=a.distance<2,j=a.deltaTime<250;if(h&&i&&j)return}return g&&f?void 0:e||f&&c&Na||g&&c&Oa?this.preventSrc(b):void 0},preventSrc:function(a){this.manager.session.prevented=!0,a.preventDefault()}};var nb=1,ob=2,pb=4,qb=8,rb=qb,sb=16,tb=32;Y.prototype={defaults:{},set:function(a){return la(this.options,a),this.manager&&this.manager.touchAction.update(),this},recognizeWith:function(a){if(f(a,"recognizeWith",this))return this;var b=this.simultaneous;return a=_(a,this),b[a.id]||(b[a.id]=a,a.recognizeWith(this)),this},dropRecognizeWith:function(a){return f(a,"dropRecognizeWith",this)?this:(a=_(a,this),delete this.simultaneous[a.id],this)},requireFailure:function(a){if(f(a,"requireFailure",this))return this;var b=this.requireFail;return a=_(a,this),-1===r(b,a)&&(b.push(a),a.requireFailure(this)),this},dropRequireFailure:function(a){if(f(a,"dropRequireFailure",this))return this;a=_(a,this);var b=r(this.requireFail,a);return b>-1&&this.requireFail.splice(b,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(a){return!!this.simultaneous[a.id]},emit:function(a){function b(b){c.manager.emit(b,a)}var c=this,d=this.state;qb>d&&b(c.options.event+Z(d)),b(c.options.event),a.additionalEvent&&b(a.additionalEvent),d>=qb&&b(c.options.event+Z(d))},tryEmit:function(a){return this.canEmit()?this.emit(a):void(this.state=tb)},canEmit:function(){for(var a=0;af?Ja:Ka,c=f!=this.pX,d=Math.abs(a.deltaX)):(e=0===g?Ia:0>g?La:Ma,c=g!=this.pY,d=Math.abs(a.deltaY))),a.direction=e,c&&d>b.threshold&&e&b.direction},attrTest:function(a){return aa.prototype.attrTest.call(this,a)&&(this.state&ob||!(this.state&ob)&&this.directionTest(a))},emit:function(a){this.pX=a.deltaX,this.pY=a.deltaY;var b=$(a.direction);b&&(a.additionalEvent=this.options.event+b),this._super.emit.call(this,a)}}),i(ca,aa,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.scale-1)>this.options.threshold||this.state&ob)},emit:function(a){if(1!==a.scale){var b=a.scale<1?"in":"out";a.additionalEvent=this.options.event+b}this._super.emit.call(this,a)}}),i(da,Y,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[hb]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distanceb.time;if(this._input=a,!d||!c||a.eventType&(Ga|Ha)&&!f)this.reset();else if(a.eventType&Ea)this.reset(),this._timer=e(function(){this.state=rb,this.tryEmit()},b.time,this);else if(a.eventType&Ga)return rb;return tb},reset:function(){clearTimeout(this._timer)},emit:function(a){this.state===rb&&(a&&a.eventType&Ga?this.manager.emit(this.options.event+"up",a):(this._input.timeStamp=ra(),this.manager.emit(this.options.event,this._input)))}}),i(ea,aa,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.rotation)>this.options.threshold||this.state&ob)}}),i(fa,aa,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:Na|Oa,pointers:1},getTouchAction:function(){return ba.prototype.getTouchAction.call(this)},attrTest:function(a){var b,c=this.options.direction;return c&(Na|Oa)?b=a.overallVelocity:c&Na?b=a.overallVelocityX:c&Oa&&(b=a.overallVelocityY),this._super.attrTest.call(this,a)&&c&a.offsetDirection&&a.distance>this.options.threshold&&a.maxPointers==this.options.pointers&&qa(b)>this.options.velocity&&a.eventType&Ga},emit:function(a){var b=$(a.offsetDirection);b&&this.manager.emit(this.options.event+b,a),this.manager.emit(this.options.event,a)}}),i(ga,Y,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[ib]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance SafeInsets { + let data_dir = app.path().app_data_dir().unwrap_or_default(); + let insets_file = data_dir.join("safe_insets.json"); + for _ in 0..10 { + if let Ok(content) = std::fs::read_to_string(&insets_file) { + if let Ok(insets) = serde_json::from_str::(&content) { + return SafeInsets { + top: insets["top"].as_i64().unwrap_or(0) as i32, + bottom: insets["bottom"].as_i64().unwrap_or(0) as i32, + left: insets["left"].as_i64().unwrap_or(0) as i32, + right: insets["right"].as_i64().unwrap_or(0) as i32, + }; + } + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + SafeInsets { top: 0, bottom: 0, left: 0, right: 0 } +} + +#[cfg(not(target_os = "android"))] +#[tauri::command] +fn get_safe_insets() -> SafeInsets { + SafeInsets { top: 0, bottom: 0, left: 0, right: 0 } +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let migrations = vec![ + let migrations: Vec = vec![ Migration { version: 1, description: "full schema v1", @@ -183,6 +222,12 @@ pub fn run() { sql: "ALTER TABLE products ADD COLUMN composition TEXT;", kind: MigrationKind::Up, }, + Migration { + version: 3, + description: "reserved", + sql: "SELECT 1;", + kind: MigrationKind::Up, + }, ]; tauri::Builder::default() @@ -191,6 +236,7 @@ pub fn run() { .add_migrations("sqlite:baldav.db", migrations) .build(), ) + .invoke_handler(tauri::generate_handler![get_safe_insets]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }