GPX export + GPS background recording fixes
GPX export: button in session detail, standard GPX 1.1 format. Background GPS fixes: - Native service: batched writes (10s flush) instead of per-point appendText — eliminates race condition with reader - JS reader: skip corrupt lines, sort by timestamp, track by ts instead of line count — handles partial writes and reordering - Recovery: idempotent import using max DB timestamp - Live recording: seq numbers on every point - FileObserver watches correct path (app root, not files/dev.baldav.app) - Removed ACCESS_BACKGROUND_LOCATION (causes throttling) Nav: "Трекинг" button on GPS screen when recording active.
This commit is contained in:
parent
f717204ac8
commit
daf71333ff
10 changed files with 5911 additions and 52 deletions
|
|
@ -1158,6 +1158,7 @@ body {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
touch-action: pan-y pinch-zoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sd-map-full {
|
.sd-map-full {
|
||||||
|
|
|
||||||
|
|
@ -375,11 +375,11 @@ async function createGpsSession(date, startTime, activityType) {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addGpsPoint(sessionId, ts, lat, lon, altGps, speedMs) {
|
async function addGpsPoint(sessionId, ts, lat, lon, altGps, speedMs, seq) {
|
||||||
await execute(
|
await execute(
|
||||||
`INSERT INTO gps_points (session_id, ts, lat, lon, altitude_gps_m, speed_ms)
|
`INSERT INTO gps_points (session_id, ts, lat, lon, altitude_gps_m, speed_ms, seq)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)`,
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`,
|
||||||
[sessionId, ts, lat, lon, altGps ?? null, speedMs ?? null]
|
[sessionId, ts, lat, lon, altGps ?? null, speedMs ?? null, seq ?? null]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,14 +158,22 @@ GPS._readNativePoints = async function() {
|
||||||
try {
|
try {
|
||||||
const raw = await window.__TAURI__.core.invoke("gps_service_read_points");
|
const raw = await window.__TAURI__.core.invoke("gps_service_read_points");
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
return raw.trim().split("\n").filter(Boolean).map(line => {
|
const points = [];
|
||||||
const d = JSON.parse(line);
|
for (const line of raw.trim().split("\n")) {
|
||||||
return {
|
if (!line) continue;
|
||||||
lat: d.lat, lon: d.lon, altitude: d.alt,
|
try {
|
||||||
speed_ms: d.speed, accuracy: d.acc,
|
const d = JSON.parse(line);
|
||||||
ts: new Date(d.ts).toISOString(),
|
if (d.lat == null || d.lon == null) continue; // skip marker/invalid lines
|
||||||
};
|
points.push({
|
||||||
});
|
lat: d.lat, lon: d.lon, altitude: d.alt,
|
||||||
|
speed_ms: d.speed, accuracy: d.acc,
|
||||||
|
ts: new Date(d.ts).toISOString(),
|
||||||
|
});
|
||||||
|
} catch { /* skip partial/corrupt lines */ }
|
||||||
|
}
|
||||||
|
// Sort by timestamp to fix any ordering issues
|
||||||
|
points.sort((a, b) => new Date(a.ts) - new Date(b.ts));
|
||||||
|
return points;
|
||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -231,11 +239,12 @@ GPS._processIncomingPoint = async function(p) {
|
||||||
if (GPS.recording.onStateChange) GPS.recording.onStateChange("resumed", p);
|
if (GPS.recording.onStateChange) GPS.recording.onStateChange("resumed", p);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save moving point
|
// Save moving point with seq
|
||||||
GPS.recording.lastMovingPoint = p;
|
GPS.recording.lastMovingPoint = p;
|
||||||
GPS.recording.points.push(p);
|
GPS.recording.points.push(p);
|
||||||
|
GPS.recording._pointSeq = (GPS.recording._pointSeq || 0) + 1;
|
||||||
if (GPS.recording.sessionId) {
|
if (GPS.recording.sessionId) {
|
||||||
await DB.addGpsPoint(GPS.recording.sessionId, p.ts, p.lat, p.lon, p.altitude, p.speed_ms);
|
await DB.addGpsPoint(GPS.recording.sessionId, p.ts, p.lat, p.lon, p.altitude, p.speed_ms, GPS.recording._pointSeq);
|
||||||
}
|
}
|
||||||
if (GPS.recording.onPoint) GPS.recording.onPoint(p);
|
if (GPS.recording.onPoint) GPS.recording.onPoint(p);
|
||||||
};
|
};
|
||||||
|
|
@ -298,13 +307,17 @@ GPS.recoverSession = async function() {
|
||||||
try {
|
try {
|
||||||
const s = JSON.parse(saved);
|
const s = JSON.parse(saved);
|
||||||
if (!s.sessionId) { GPS._clearPersistedState(); return; }
|
if (!s.sessionId) { GPS._clearPersistedState(); return; }
|
||||||
// Import any remaining native points
|
// Import remaining native points not yet in DB (idempotent by timestamp)
|
||||||
|
const existingPts = await DB.getGpsPoints(s.sessionId);
|
||||||
|
const maxDbTs = existingPts.length ? Math.max(...existingPts.map(p => new Date(p.ts).getTime())) : 0;
|
||||||
const nativePoints = await GPS._readNativePoints();
|
const nativePoints = await GPS._readNativePoints();
|
||||||
if (nativePoints.length > (s.pointCount || 0)) {
|
let imported = 0;
|
||||||
for (let i = s.pointCount || 0; i < nativePoints.length; i++) {
|
let seq = existingPts.length;
|
||||||
const p = nativePoints[i];
|
for (const p of nativePoints) {
|
||||||
await DB.addGpsPoint(s.sessionId, p.ts, p.lat, p.lon, p.altitude, p.speed_ms);
|
if (new Date(p.ts).getTime() <= maxDbTs) continue;
|
||||||
}
|
seq++;
|
||||||
|
await DB.addGpsPoint(s.sessionId, p.ts, p.lat, p.lon, p.altitude, p.speed_ms, seq);
|
||||||
|
imported++;
|
||||||
}
|
}
|
||||||
// Finalize the session
|
// Finalize the session
|
||||||
const pts = await DB.getGpsPoints(s.sessionId);
|
const pts = await DB.getGpsPoints(s.sessionId);
|
||||||
|
|
@ -341,7 +354,8 @@ GPS.startRecording = async function(activityType, weightKg) {
|
||||||
GPS.recording._currentPause = null;
|
GPS.recording._currentPause = null;
|
||||||
GPS.recording.nativeService = false;
|
GPS.recording.nativeService = false;
|
||||||
GPS.recording._pollTimer = null;
|
GPS.recording._pollTimer = null;
|
||||||
GPS.recording._lastNativeCount = 0;
|
GPS.recording._lastProcessedTs = 0;
|
||||||
|
GPS.recording._pointSeq = 0;
|
||||||
|
|
||||||
// Start native foreground service (Android)
|
// Start native foreground service (Android)
|
||||||
await GPS._startNativeService();
|
await GPS._startNativeService();
|
||||||
|
|
@ -351,10 +365,13 @@ GPS.startRecording = async function(activityType, weightKg) {
|
||||||
GPS.recording._pollTimer = setInterval(async () => {
|
GPS.recording._pollTimer = setInterval(async () => {
|
||||||
if (!GPS.recording.active) { clearInterval(GPS.recording._pollTimer); return; }
|
if (!GPS.recording.active) { clearInterval(GPS.recording._pollTimer); return; }
|
||||||
const nativePoints = await GPS._readNativePoints();
|
const nativePoints = await GPS._readNativePoints();
|
||||||
for (let i = GPS.recording._lastNativeCount; i < nativePoints.length; i++) {
|
// Process only points newer than last processed (by timestamp)
|
||||||
await GPS._processIncomingPoint(nativePoints[i]);
|
for (const p of nativePoints) {
|
||||||
|
const pTs = new Date(p.ts).getTime();
|
||||||
|
if (pTs <= GPS.recording._lastProcessedTs) continue;
|
||||||
|
await GPS._processIncomingPoint(p);
|
||||||
|
GPS.recording._lastProcessedTs = pTs;
|
||||||
}
|
}
|
||||||
GPS.recording._lastNativeCount = nativePoints.length;
|
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -660,5 +677,51 @@ GPS.formatDistance = function(meters) {
|
||||||
return meters >= 1000 ? (meters / 1000).toFixed(2) + " км" : meters + " м";
|
return meters >= 1000 ? (meters / 1000).toFixed(2) + " км" : meters + " м";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// GPX Export
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
GPS.exportGPX = async function(sessionId) {
|
||||||
|
const session = await DB.getGpsSession(sessionId);
|
||||||
|
if (!session) return;
|
||||||
|
const points = await DB.getGpsPoints(sessionId);
|
||||||
|
if (!points.length) return;
|
||||||
|
|
||||||
|
const esc = s => String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||||
|
const name = esc(session.activity_type || "Track") + " " + esc(session.date);
|
||||||
|
|
||||||
|
let gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gpx creator="BalDav" version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
|
||||||
|
<metadata><name>${name}</name><time>${points[0].ts}</time></metadata>
|
||||||
|
<trk>
|
||||||
|
<name>${name}</name>
|
||||||
|
<type>${esc(session.activity_type || "other")}</type>
|
||||||
|
<trkseg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (const p of points) {
|
||||||
|
gpx += ` <trkpt lat="${p.lat}" lon="${p.lon}">
|
||||||
|
`;
|
||||||
|
if (p.altitude_gps_m != null) gpx += ` <ele>${p.altitude_gps_m}</ele>\n`;
|
||||||
|
if (p.ts) gpx += ` <time>${p.ts}</time>\n`;
|
||||||
|
if (p.speed_ms != null) gpx += ` <extensions><speed>${p.speed_ms}</speed></extensions>\n`;
|
||||||
|
gpx += ` </trkpt>\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
gpx += ` </trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>`;
|
||||||
|
|
||||||
|
// Download
|
||||||
|
const blob = new Blob([gpx], { type: "application/gpx+xml" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `baldav-${session.date}-${session.activity_type || "track"}.gpx`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
window.GPS = GPS;
|
window.GPS = GPS;
|
||||||
console.log("[gps.js] window.GPS ready");
|
console.log("[gps.js] window.GPS ready");
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,12 @@ async function init() {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function renderScreen(name) {
|
function renderScreen(name) {
|
||||||
|
const prevScreen = state.screen;
|
||||||
state.screen = name;
|
state.screen = name;
|
||||||
|
// Push history so "back" navigates between screens instead of exiting app
|
||||||
|
if (prevScreen && prevScreen !== name) {
|
||||||
|
history.pushState({ screen: name }, "");
|
||||||
|
}
|
||||||
const screens = {
|
const screens = {
|
||||||
today: renderToday,
|
today: renderToday,
|
||||||
products: renderProducts,
|
products: renderProducts,
|
||||||
|
|
@ -112,7 +117,7 @@ function buildNav(active) {
|
||||||
[
|
[
|
||||||
{ id: "today", label: "Сегодня" },
|
{ id: "today", label: "Сегодня" },
|
||||||
{ id: "products", label: "Продукты" },
|
{ id: "products", label: "Продукты" },
|
||||||
{ id: GPS.recording.active ? "recording" : "gps", label: GPS.recording.active ? "Запись" : "Трек" },
|
{ id: "gps", label: "Трек" },
|
||||||
{ id: "supplements", label: "Добавки" },
|
{ id: "supplements", label: "Добавки" },
|
||||||
{ id: "settings", label: "Настройки" },
|
{ id: "settings", label: "Настройки" },
|
||||||
].forEach(item => {
|
].forEach(item => {
|
||||||
|
|
@ -302,13 +307,28 @@ function showModal(html, onSubmit) {
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
history.pushState({ modal: true }, "");
|
history.pushState({ modal: true }, "");
|
||||||
const popHandler = () => closeModal(false);
|
const popHandler = () => {
|
||||||
|
// If fullscreen map is open inside this modal, collapse it instead of closing modal
|
||||||
|
const fsMap = overlay.querySelector(".sd-map-full");
|
||||||
|
if (fsMap) {
|
||||||
|
fsMap.classList.remove("sd-map-full");
|
||||||
|
const expandBtn = fsMap.querySelector(".sd-map-expand-btn");
|
||||||
|
if (expandBtn) expandBtn.innerHTML = "⛶";
|
||||||
|
try { map.dragging.disable(); } catch {}
|
||||||
|
history.pushState({ modal: true }, "");
|
||||||
|
window.addEventListener("popstate", popHandler, { once: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeModal(false);
|
||||||
|
};
|
||||||
window.addEventListener("popstate", popHandler, { once: true });
|
window.addEventListener("popstate", popHandler, { once: true });
|
||||||
|
|
||||||
function closeModal(goBack) {
|
function closeModal(goBack) {
|
||||||
overlay.remove();
|
overlay.remove();
|
||||||
window.removeEventListener("popstate", popHandler);
|
window.removeEventListener("popstate", popHandler);
|
||||||
if (goBack) history.back();
|
if (goBack) history.back();
|
||||||
|
// Ensure history isn't empty after modal close (prevents app exit on next back)
|
||||||
|
history.replaceState({ screen: state.screen }, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay.querySelector("#modal-back").onclick = () => closeModal(true);
|
overlay.querySelector("#modal-back").onclick = () => closeModal(true);
|
||||||
|
|
@ -733,7 +753,7 @@ async function renderGpsScreen(container) {
|
||||||
<option value="run">Бег</option>
|
<option value="run">Бег</option>
|
||||||
</select>
|
</select>
|
||||||
<button id="gps-rec-btn" class="btn-primary gps-rec ${isRec ? "recording" : ""}">
|
<button id="gps-rec-btn" class="btn-primary gps-rec ${isRec ? "recording" : ""}">
|
||||||
${isRec ? "Стоп" : "Старт"}
|
${isRec ? "Трекинг" : "Старт"}
|
||||||
</button>
|
</button>
|
||||||
<div class="gps-menu-wrap">
|
<div class="gps-menu-wrap">
|
||||||
<button id="gps-menu-btn" class="gps-menu-btn">⋮</button>
|
<button id="gps-menu-btn" class="gps-menu-btn">⋮</button>
|
||||||
|
|
@ -1224,7 +1244,7 @@ async function showSessionDetail(sessionId) {
|
||||||
<div class="sd-stat"><span class="sd-val">${stats.avgSpeed} км/ч</span><span class="sd-lbl">Средняя</span></div>
|
<div class="sd-stat"><span class="sd-val">${stats.avgSpeed} км/ч</span><span class="sd-lbl">Средняя</span></div>
|
||||||
<div class="sd-stat"><span class="sd-val" id="sd-max-speed">${stats.maxSpeed} км/ч</span><span class="sd-lbl">Макс.</span></div>
|
<div class="sd-stat"><span class="sd-val" id="sd-max-speed">${stats.maxSpeed} км/ч</span><span class="sd-lbl">Макс.</span></div>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="sd-speed-chart" height="150"></canvas>
|
<canvas id="sd-speed-chart" height="150" style="touch-action:pan-y"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sd-section">
|
<div class="sd-section">
|
||||||
|
|
@ -1246,10 +1266,11 @@ async function showSessionDetail(sessionId) {
|
||||||
<div class="sd-stat"><span class="sd-val">${stats.elevGain} м</span><span class="sd-lbl">Подъём</span></div>
|
<div class="sd-stat"><span class="sd-val">${stats.elevGain} м</span><span class="sd-lbl">Подъём</span></div>
|
||||||
<div class="sd-stat"><span class="sd-val">${stats.elevLoss} м</span><span class="sd-lbl">Спуск</span></div>
|
<div class="sd-stat"><span class="sd-val">${stats.elevLoss} м</span><span class="sd-lbl">Спуск</span></div>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="sd-elev-chart" height="150"></canvas>
|
<canvas id="sd-elev-chart" height="150" style="touch-action:pan-y"></canvas>
|
||||||
</div>` : ""}
|
</div>` : ""}
|
||||||
|
|
||||||
<button id="sd-delete" class="btn-secondary full-width" style="margin-top:16px;color:#cf6679">Удалить тренировку</button>
|
<button id="sd-export" class="btn-secondary full-width" style="margin-top:16px">Экспорт GPX</button>
|
||||||
|
<button id="sd-delete" class="btn-secondary full-width" style="margin-top:8px;color:#cf6679">Удалить тренировку</button>
|
||||||
`, () => {});
|
`, () => {});
|
||||||
|
|
||||||
// Render map
|
// Render map
|
||||||
|
|
@ -1258,7 +1279,7 @@ async function showSessionDetail(sessionId) {
|
||||||
if (!mapEl || !points.length) return;
|
if (!mapEl || !points.length) return;
|
||||||
|
|
||||||
L.Icon.Default.imagePath = "lib/";
|
L.Icon.Default.imagePath = "lib/";
|
||||||
const map = L.map(mapEl, { attributionControl: false, zoomControl: false, scrollWheelZoom: false });
|
const map = L.map(mapEl, { attributionControl: false, zoomControl: false, scrollWheelZoom: false, dragging: false, tap: false, touchZoom: true });
|
||||||
// Ctrl+wheel zoom on desktop
|
// Ctrl+wheel zoom on desktop
|
||||||
mapEl.addEventListener("wheel", (e) => {
|
mapEl.addEventListener("wheel", (e) => {
|
||||||
if (e.ctrlKey) { e.preventDefault(); map.setZoom(map.getZoom() + (e.deltaY < 0 ? 1 : -1)); }
|
if (e.ctrlKey) { e.preventDefault(); map.setZoom(map.getZoom() + (e.deltaY < 0 ? 1 : -1)); }
|
||||||
|
|
@ -1309,29 +1330,33 @@ async function showSessionDetail(sessionId) {
|
||||||
|
|
||||||
// Fullscreen toggle button on map
|
// Fullscreen toggle button on map
|
||||||
// Fullscreen map toggle
|
// Fullscreen map toggle
|
||||||
let mapFsPopHandler = null;
|
let mapIsFullscreen = false;
|
||||||
function toggleMapFs() {
|
|
||||||
const isFull = mapEl.classList.toggle("sd-map-full");
|
function collapseMap() {
|
||||||
fsDiv.innerHTML = isFull ? "✕" : "⛶";
|
if (!mapIsFullscreen) return;
|
||||||
if (isFull) {
|
mapIsFullscreen = false;
|
||||||
history.pushState({ mapFs: true }, "");
|
mapEl.classList.remove("sd-map-full");
|
||||||
mapFsPopHandler = () => {
|
fsDiv.innerHTML = "⛶";
|
||||||
mapEl.classList.remove("sd-map-full");
|
|
||||||
fsDiv.innerHTML = "⛶";
|
|
||||||
setTimeout(() => { map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05)); }, 100);
|
|
||||||
};
|
|
||||||
window.addEventListener("popstate", mapFsPopHandler, { once: true });
|
|
||||||
} else {
|
|
||||||
if (mapFsPopHandler) { window.removeEventListener("popstate", mapFsPopHandler); mapFsPopHandler = null; }
|
|
||||||
history.back();
|
|
||||||
}
|
|
||||||
setTimeout(() => { map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05)); }, 100);
|
setTimeout(() => { map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05)); }, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleMapFs() {
|
||||||
|
if (mapIsFullscreen) {
|
||||||
|
collapseMap();
|
||||||
|
map.dragging.disable();
|
||||||
|
} else {
|
||||||
|
mapIsFullscreen = true;
|
||||||
|
mapEl.classList.add("sd-map-full");
|
||||||
|
fsDiv.innerHTML = "✕";
|
||||||
|
map.dragging.enable();
|
||||||
|
setTimeout(() => { map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05)); }, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fsBtn = L.control({ position: "topright" });
|
const fsBtn = L.control({ position: "topright" });
|
||||||
let fsDiv;
|
let fsDiv;
|
||||||
fsBtn.onAdd = () => {
|
fsBtn.onAdd = () => {
|
||||||
fsDiv = L.DomUtil.create("div", "sd-map-fs-btn");
|
fsDiv = L.DomUtil.create("div", "sd-map-fs-btn sd-map-expand-btn");
|
||||||
fsDiv.innerHTML = "⛶";
|
fsDiv.innerHTML = "⛶";
|
||||||
fsDiv.onclick = (e) => { e.stopPropagation(); toggleMapFs(); };
|
fsDiv.onclick = (e) => { e.stopPropagation(); toggleMapFs(); };
|
||||||
return fsDiv;
|
return fsDiv;
|
||||||
|
|
@ -1391,7 +1416,6 @@ async function showSessionDetail(sessionId) {
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
zoom: {
|
zoom: {
|
||||||
pan: { enabled: true, mode: "x" },
|
|
||||||
zoom: { wheel: { enabled: true, modifierKey: "ctrl" }, pinch: { enabled: true }, drag: { enabled: false }, mode: "x" },
|
zoom: { wheel: { enabled: true, modifierKey: "ctrl" }, pinch: { enabled: true }, drag: { enabled: false }, mode: "x" },
|
||||||
pan: { enabled: false },
|
pan: { enabled: false },
|
||||||
},
|
},
|
||||||
|
|
@ -1431,7 +1455,6 @@ async function showSessionDetail(sessionId) {
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
zoom: {
|
zoom: {
|
||||||
pan: { enabled: true, mode: "x" },
|
|
||||||
zoom: { wheel: { enabled: true, modifierKey: "ctrl" }, pinch: { enabled: true }, drag: { enabled: false }, mode: "x" },
|
zoom: { wheel: { enabled: true, modifierKey: "ctrl" }, pinch: { enabled: true }, drag: { enabled: false }, mode: "x" },
|
||||||
pan: { enabled: false },
|
pan: { enabled: false },
|
||||||
},
|
},
|
||||||
|
|
@ -1448,6 +1471,9 @@ async function showSessionDetail(sessionId) {
|
||||||
|
|
||||||
// Delete button
|
// Delete button
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
const exp = document.querySelector("#sd-export");
|
||||||
|
if (exp) exp.onclick = () => GPS.exportGPX(sessionId);
|
||||||
|
|
||||||
const del = document.querySelector("#sd-delete");
|
const del = document.querySelector("#sd-delete");
|
||||||
if (del) del.onclick = async () => {
|
if (del) del.onclick = async () => {
|
||||||
await DB.deleteGpsSession(sessionId);
|
await DB.deleteGpsSession(sessionId);
|
||||||
|
|
@ -1992,6 +2018,16 @@ function esc(str) {
|
||||||
// Start
|
// Start
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Global back navigation between screens
|
||||||
|
window.addEventListener("popstate", (e) => {
|
||||||
|
// If a modal is open, don't handle here (modal has its own handler)
|
||||||
|
if (document.querySelector(".modal-overlay")) return;
|
||||||
|
// Navigate to previous screen or stay on today
|
||||||
|
if (e.state && e.state.screen) {
|
||||||
|
renderScreen(e.state.screen);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
init().catch(err => {
|
init().catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
app.innerHTML = `<div class="loading" style="color:#cf6679">Ошибка: ${esc(String(err))}</div>`;
|
app.innerHTML = `<div class="loading" style="color:#cf6679">Ошибка: ${esc(String(err))}</div>`;
|
||||||
|
|
|
||||||
5733
src-tauri/Cargo.lock
generated
Normal file
5733
src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "baldav"
|
name = "baldav"
|
||||||
version = "0.1.39"
|
version = "0.1.59"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
|
||||||
12
src-tauri/capabilities/default.json
Normal file
12
src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/nicegui/tauri-v2/refs/heads/main/core/tauri-config-schema/schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "BalDav default capabilities",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"sql:default",
|
||||||
|
"sql:allow-execute",
|
||||||
|
"sql:allow-select"
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -25,8 +25,14 @@ fn get_app_info() -> AppInfo {
|
||||||
fn gps_service_command(app: tauri::AppHandle, cmd: String) -> Result<String, String> {
|
fn gps_service_command(app: tauri::AppHandle, cmd: String) -> Result<String, String> {
|
||||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||||
std::fs::create_dir_all(&data_dir).ok();
|
std::fs::create_dir_all(&data_dir).ok();
|
||||||
|
// Write to app_data_dir
|
||||||
let cmd_path = data_dir.join("gps_command.txt");
|
let cmd_path = data_dir.join("gps_command.txt");
|
||||||
std::fs::write(&cmd_path, &cmd).map_err(|e| e.to_string())?;
|
std::fs::write(&cmd_path, &cmd).map_err(|e| e.to_string())?;
|
||||||
|
// Also write to app root dir (Android: Kotlin watches there)
|
||||||
|
if let Some(root) = data_dir.parent().and_then(|p| p.parent()) {
|
||||||
|
let alt = root.join("gps_command.txt");
|
||||||
|
let _ = std::fs::write(&alt, &cmd);
|
||||||
|
}
|
||||||
Ok(format!("sent:{}", cmd))
|
Ok(format!("sent:{}", cmd))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,9 +40,17 @@ fn gps_service_command(app: tauri::AppHandle, cmd: String) -> Result<String, Str
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn gps_service_read_points(app: tauri::AppHandle) -> Result<String, String> {
|
fn gps_service_read_points(app: tauri::AppHandle) -> Result<String, String> {
|
||||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||||
|
// Also check parent dir (Android: app_data_dir may be files/dev.baldav.app but service writes to app root)
|
||||||
let pts_path = data_dir.join("gps_live_points.jsonl");
|
let pts_path = data_dir.join("gps_live_points.jsonl");
|
||||||
|
let alt_path = data_dir.parent().and_then(|p| p.parent()).map(|p| p.join("gps_live_points.jsonl"));
|
||||||
if pts_path.exists() {
|
if pts_path.exists() {
|
||||||
std::fs::read_to_string(&pts_path).map_err(|e| e.to_string())
|
std::fs::read_to_string(&pts_path).map_err(|e| e.to_string())
|
||||||
|
} else if let Some(ref alt) = alt_path {
|
||||||
|
if alt.exists() {
|
||||||
|
std::fs::read_to_string(alt).map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(String::new())
|
Ok(String::new())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicegui/tauri-v2/refs/heads/main/tooling/cli/schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicegui/tauri-v2/refs/heads/main/tooling/cli/schema.json",
|
||||||
"productName": "BalDav",
|
"productName": "BalDav",
|
||||||
"version": "0.1.39",
|
"version": "0.1.59",
|
||||||
"identifier": "dev.baldav.app",
|
"identifier": "dev.baldav.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend"
|
"frontendDist": "../frontend"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue