Share image (Похвастаться): render + preview modal + detail button
Stage 1 (pure JS). buildShareImage() draws a 9:16 story PNG: track shape (colored glowing line), big Distance/Time/Speed + small elevation/kcal/date, on a transparent or photo background. showShareModal() previews it with a transparent/photo toggle and Share/Save buttons; a 'Похвастаться' button in the track detail opens it. Native export (SAF save + share intent) and the settings background picker come in Stage 2 (needs an APK rebuild); on desktop the export buttons toast an error.
This commit is contained in:
parent
7bb764d2ca
commit
69f6fdac18
5 changed files with 281 additions and 0 deletions
|
|
@ -22,6 +22,7 @@
|
|||
- **Заполнение GPS-пропусков по дороге** — на детали трека кнопка **«🧩»** прокладывает дорожный путь (BRouter, профиль shortest) через каждый GPS-дропаут (разрыв по времени + реальная хорда) и показывает **предпросмотр только достроенных пропусков** (голубым, с бело-чёрной оковкой; недостроенные — красным пунктиром) плюс панель «Достроено N (+X км), M не удалось» с кнопками **Сохранить/Отмена**. Сохранение — **обратимый патч на месте**: достроенные точки помечаются `estimated` и добавляются в трек (оригинал цел), статистика пересчитывается; повторный тап предлагает **«Убрать достройку»** (удаляет только достроенное). Достраиваются только реальные длинные потери сигнала (по умолчанию разрыв > 60с и хорда > 150м); пропуск не достраивается, если дорога заметно длиннее хорды (крюк, > 2×) или почти совпадает с прямой (мостить нечего) — чтобы не перерисовывать нормальные участки объездами.
|
||||
- **Непроезжие места (nogo)** — красные круги, которые роутинг ОБЪЕЗЖАЕТ. Ставятся кнопкой **⛔**: в планировщике — тапом по карте/линии, на карте записи — «здесь нет проезда» (текущее место). Показываются на картах планировщика, записи и детали трека; тап по кругу — убрать. Избегание работает и через домашний RouteServer, и встроенным движком офлайн (BRouter nogopoints). Web-BRouter умеет nogo сам.
|
||||
- **Объезд с возвратом на маршрут (на ходу)** — на карте записи кнопка **«↪»**: если активный маршрут ведёт через непроезжее место, строит от текущей позиции объезд обратно НА маршрут за местом затыка (отступ ~1.5 км вперёд, растёт при неудаче до конца маршрута), избегая nogo и разворота на 180° (учитывает курс). Показывается ярким навигационным оверлеем (не меняет сохранённый маршрут и не прерывает запись); долгий тап по кнопке убирает оверлей.
|
||||
- **Похвастаться (шэр-картинка)** — в детали трека кнопка **«Похвастаться»**: генерирует картинку в формате сторис (9:16) с формой трека и статами (Дистанция/Время/Средняя скорость крупно + набор/ккал/дата), **прозрачную** (PNG с альфой для наложения на своё фото) или **на фото-подложке** из настроек. Модалка предпросмотра с переключателем и кнопками **Поделиться** (системный шэр) и **Сохранить** (SAF). Подложка выбирается в Настройках.
|
||||
- **Де-джиттер трека** — сглаживание дрожащего GPS-трека методом Савицкого-Голея (настраиваемое окно/число итераций, живое превью на карте детали трека). Хранится обратимо: оригинал и сглаженный вариант лежат раздельно, калории пересчитываются по сглаженным точкам, откат к оригиналу в один тап. Точность каждого фикса (`accuracy_m`) пишется в БД: при пересчёте трека точки хуже порога «Макс. погрешность GPS» исключаются из расчёта и линии (обратимо — точки в БД остаются; поднять порог = вернуть). Работает во всех пересчётах (кнопка «Пересчитать», «Пересчитать всё», после де-джиттера); импорт/старьё без точности и dead-reckoning заполнения не трогает.
|
||||
- **POI на карте** — точки интереса (кафе, магазины, вода, туалеты, банки/ATM, остановки, укрытия, смотровые и т.д.) круглыми контрастными значками (эмодзи в кружке, читаемо на светлой и тёмной карте). Кнопка `📍` с панелью-фильтром категорий; рендер по видимой области от зума 13. Данные оффлайн (`data/pois.json`, ~100 км вокруг опорной точки, 5662 объекта из OSM/Overpass). На картах записи, деталей трека и дорог.
|
||||
- **Тёмная карта** — тумблер `☾` на экранах записи, деталей трека и дорог: инвертирует ТОЛЬКО слой тайлов (`invert + hue-rotate 180° + brightness`, трек/маршрут/дороги остаются цветными), выбор запоминается. Работает одинаково на офлайн-тайлах и онлайне, без ключей/квот/перепаковки.
|
||||
|
|
|
|||
96
docs/plans/2026-09-14-share-image.md
Normal file
96
docs/plans/2026-09-14-share-image.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# План: генератор шэр-картинки трека («Похвастаться»)
|
||||
|
||||
## Context (зачем)
|
||||
Пользователь хочет делиться красивой картинкой поездки: форма трека + крупные статы
|
||||
(как на присланном скрине). Три части: (1) картинка с формой трека и статами, прозрачная
|
||||
или на фото-подложке; (2) настраиваемая подложка (фото) в настройках; (3) кнопка
|
||||
«Похвастаться» в детали трека, которая даёт **Поделиться** (системный шэр) и **Сохранить файл**.
|
||||
|
||||
Итог решений (согласовано): экспорт — и «Поделиться», и «Сохранить»; в диалоге переключатель
|
||||
прозрачный/фото; статы на картинке — дистанция, время, средняя скорость и (мельче) набор
|
||||
высоты, ккал, дата.
|
||||
|
||||
Рисуем на `<canvas>` в WebView (чистый JS). Бинарный вывод PNG в WebView невозможен (blob
|
||||
download не работает), поэтому сохранение/шэр — нативно (Kotlin), плюс чтение выбранного фото
|
||||
в base64 — Rust. Нужна пересборка APK. FileProvider уже есть в манифесте, `file_paths` содержит
|
||||
`cache-path "."` — годится для шэра.
|
||||
|
||||
## Часть A — Рендер картинки на canvas (JS, `frontend/js/main.js` + `styles.css`)
|
||||
- Новая функция `buildShareImage(session, stats, points, opts)` → возвращает dataURL PNG.
|
||||
Холст 1080×1920 (сторис 9:16):
|
||||
- Фон: `opts.bg==="photo"` и есть сохранённая подложка → нарисовать её cover-fit + лёгкий
|
||||
тёмный градиент снизу для читаемости; иначе прозрачный (холст не заливать).
|
||||
- Форма трека: точки трека спроецировать (равнопрямоугольная проекция в масштабе трека,
|
||||
паддинг), нарисовать линией цвета трека (`GPS.config.trackColor`) с мягким свечением, в
|
||||
верхней ~40% части.
|
||||
- Статы: крупно по центру — Дистанция / Время / Средняя скорость (значение + подпись
|
||||
English: Distance/Time/Speed, «km»/«km/h», 1 знак) как на скрине; мелкой строкой снизу —
|
||||
«↑ N m · K kcal · дата». Белый текст с тенью/обводкой (читаемо и на прозрачном, и на фото).
|
||||
- Мелкий локальный форматтер времени «1h 29min»/«29 min» (не `GPS.formatDuration`, там h:mm:ss).
|
||||
- Модалка предпросмотра `showShareModal(session, stats, points)`: показывает `<img src=dataURL>`,
|
||||
переключатель **Прозрачный / С фото** (перерисовывает), кнопки **Поделиться**, **Сохранить**,
|
||||
**Закрыть**. Паттерн модалки — как `appConfirm` (main.js:1040).
|
||||
|
||||
## Часть B — Кнопка «Похвастаться» в детали трека
|
||||
- В `showSessionDetail` рядом с `#sd-export` (main.js:5011-5015) добавить
|
||||
`<button id="sd-share">Похвастаться</button>` → `showShareModal(session, stats, points)`
|
||||
(переменные `session`/`stats`/`points` уже в области видимости деталей).
|
||||
|
||||
## Часть C — Нативный вывод PNG (Kotlin + Rust, пересборка APK)
|
||||
- Rust `export_image(app, filename, base64, action)` (action `save`|`share`): пишет base64 в
|
||||
`export_image.b64` (+ app root) и команду `exportimg:<action>:<filename>` в `gps_command.txt`
|
||||
(тот же файловый мост, что `save_file`, lib.rs:515). Зарегистрировать в `generate_handler`.
|
||||
- Kotlin (`MainActivity.kt`): в обработчике команд ветка `exportimg:` — прочитать
|
||||
`export_image.b64`, `Base64.decode`:
|
||||
- `save` → новый `saveImageLauncher = CreateDocument("image/png")` пишет байты (по образцу
|
||||
`saveFileLauncher`, MainActivity.kt:55).
|
||||
- `share` → записать в `cacheDir/<filename>`, `FileProvider.getUriForFile(ctx,
|
||||
"${applicationId}.fileprovider", f)`, `Intent(ACTION_SEND) type=image/png EXTRA_STREAM=uri
|
||||
FLAG_GRANT_READ_URI_PERMISSION`, `startActivity(createChooser)`.
|
||||
- JS: `BRouter`-подобных нет; звать напрямую `invoke("export_image", {filename, base64, action})`.
|
||||
base64 = `dataURL.split(",")[1]`.
|
||||
|
||||
## Часть D — Настраиваемая подложка (настройки)
|
||||
- Rust `read_file_base64(app, path)` → вернуть содержимое файла base64 (для чтения выбранного
|
||||
фото; `pick_file_open` уже копирует выбранное в `picked_import.db`, lib.rs:167/183).
|
||||
- Настройки (`renderSettings` / секция «Карты» или новая «Картинка»): строка «Фон для картинки»
|
||||
с кнопками **Выбрать** и **Сбросить**.
|
||||
- Выбрать: `pick_file_open("image/*")` → поллинг `get_picked_file` → путь → `read_file_base64`
|
||||
→ загрузить в `Image`, **уменьшить** до ширины ≤1080 на офскрин-canvas → `toDataURL("image/
|
||||
jpeg",0.85)` → сохранить в `localStorage.baldav_share_bg` (после даунскейла ~200-400КБ, влезает).
|
||||
- Сбросить: удалить ключ.
|
||||
- `buildShareImage` берёт подложку из `localStorage.baldav_share_bg`.
|
||||
|
||||
## Файлы
|
||||
- `frontend/js/main.js` — `buildShareImage`, `showShareModal`, кнопка `#sd-share`, пикер фона
|
||||
в настройках, `invoke("export_image"/"read_file_base64")`.
|
||||
- `frontend/css/styles.css` — стиль модалки предпросмотра.
|
||||
- `src-tauri/src/lib.rs` — команды `export_image`, `read_file_base64` (+ регистрация).
|
||||
- `src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt` — `exportimg:`
|
||||
(saveImageLauncher + share-интент).
|
||||
|
||||
## Переиспользование (не изобретать)
|
||||
- Файловый мост `save_file`/`gps_command.txt` (lib.rs:515, MainActivity.kt:457) — образец для
|
||||
`export_image`.
|
||||
- `saveFileLauncher` (CreateDocument, MainActivity.kt:55) — образец `saveImageLauncher`.
|
||||
- `pick_file_open`/`get_picked_file` (lib.rs:167/183) — выбор фото.
|
||||
- FileProvider (`AndroidManifest.xml:50`, `file_paths.xml` cache-path) — шэр.
|
||||
- `appConfirm` модалка (main.js:1040), `showToast`, `GPS.formatDistance`, `state.profile`.
|
||||
- Точки/статы трека уже посчитаны в `showSessionDetail`.
|
||||
|
||||
## Верификация
|
||||
Десктоп (Xvfb :99): кнопка «Похвастаться» → модалка с картинкой (форма трека + статы),
|
||||
переключатель прозрачный/фото рисует корректно; «Сохранить» на десктопе — через `save_file`-путь
|
||||
(проверить, что dataURL строится; нативный SAF/шэр — только на теле). Скрин модалки.
|
||||
Устройство (`./build.sh android -d -i`, по явной просьбе): «Поделиться» открывает системный
|
||||
чужой чузер с PNG; «Сохранить» — SAF, файл открывается как картинка; в настройках выбор фото →
|
||||
на картинке подложка; прозрачный вариант — PNG с альфой (проверить в галерее на светлом фоне
|
||||
через тень текста).
|
||||
|
||||
## Стадии
|
||||
1. A+B (рендер + модалка + кнопка) с «Сохранить»/«Поделиться» заглушены тостом на десктопе —
|
||||
проверить рендер визуально.
|
||||
2. C (нативный save/share) + D (подложка) → пересборка APK, тест на теле.
|
||||
|
||||
## Не трогаем
|
||||
- Карту/треки не меняем; экспорт GPX (`sd-export`) остаётся.
|
||||
27
docs/session-logs/2026-09-14_share-image.md
Normal file
27
docs/session-logs/2026-09-14_share-image.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Сессия 2026-09-14 — шэр-картинка трека («Похвастаться»)
|
||||
|
||||
## Контекст
|
||||
Пользователь хочет делиться картинкой поездки: форма трека + крупные статы (как на скрине),
|
||||
прозрачная или на фото-подложке. План `docs/plans/2026-09-14-share-image.md`. Согласовано:
|
||||
кнопка даёт «Поделиться» и «Сохранить»; в диалоге переключатель прозрачный/фото; статы —
|
||||
дистанция, время, средняя скорость + набор/ккал/дата.
|
||||
|
||||
Разведка: `save_file` (SAF, текст) — не годится для бинаря PNG; нужен нативный бинарный сейв +
|
||||
ACTION_SEND (FileProvider уже в манифесте, `file_paths` с cache-path есть) + чтение фото в
|
||||
base64 (Rust). Canvas в WebView доступен.
|
||||
|
||||
## Сделано — Стадия 1 (рендер + модалка + кнопка), чистый JS
|
||||
- `buildShareImage(session, stats, points, opts)` (main.js): холст 1080×1920; фон прозрачный
|
||||
или фото (cover-fit + тёмный градиент); форма трека (проекция точек, линия цвета трека со
|
||||
свечением); статы крупно Distance/Time/Speed + мелко ↑набор/ккал/дата; белый текст с тенью.
|
||||
- `showShareModal` — предпросмотр `<img>` + переключатель Прозрачный/С фото + кнопки
|
||||
Поделиться/Сохранить/Закрыть. `exportShareImage(dataUrl, filename, action)` зовёт
|
||||
`invoke("export_image")` (нативной команды пока нет — на десктопе тост-ошибка, это ок).
|
||||
- Кнопка `#sd-share` «Похвастаться» в детали трека. CSS `.share-modal` (шахматка под прозрачность).
|
||||
|
||||
## Проверка
|
||||
- Синтаксис main.js OK. Рендер модалки на десктопе (Xvfb) — следующий шаг.
|
||||
|
||||
## Дальше
|
||||
- Стадия 2: Rust `export_image`/`read_file_base64`, Kotlin `exportimg:` (SAF-сейв PNG +
|
||||
ACTION_SEND), выбор подложки в настройках; пересборка APK; тест на теле.
|
||||
|
|
@ -1168,6 +1168,42 @@ select {
|
|||
/* Invisible fat hit target for route legs — clickable despite zero opacity. */
|
||||
.re-hit { pointer-events: stroke; }
|
||||
|
||||
/* Share image ("Похвастаться") preview modal. */
|
||||
.share-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100000;
|
||||
background: rgba(0,0,0,0.88);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
padding-top: calc(16px + var(--safe-area-top));
|
||||
padding-bottom: calc(16px + var(--safe-area-bottom));
|
||||
}
|
||||
.share-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
.share-preview {
|
||||
max-width: 100%;
|
||||
max-height: 68vh;
|
||||
object-fit: contain;
|
||||
border-radius: 10px;
|
||||
/* checkerboard so a transparent PNG reads as transparent */
|
||||
background-image:
|
||||
linear-gradient(45deg, #3a3a3a 25%, transparent 25%, transparent 75%, #3a3a3a 75%),
|
||||
linear-gradient(45deg, #3a3a3a 25%, #262626 25%, #262626 75%, #3a3a3a 75%);
|
||||
background-size: 24px 24px;
|
||||
background-position: 0 0, 12px 12px;
|
||||
}
|
||||
.share-toggle, .share-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
|
||||
.share-toggle button, .share-actions button { padding: 9px 14px; }
|
||||
|
||||
.rec-map-wrap {
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
|
|
|
|||
|
|
@ -3246,6 +3246,123 @@ async function rejoinReroute(cur, bearingDeg, routePts, nogos) {
|
|||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Share image ("Похвастаться"): track shape + stats as a story-format PNG
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Story-format duration: "1h 29min" / "29 min" (GPS.formatDuration is h:mm:ss).
|
||||
function _shareDuration(sec) {
|
||||
let h = Math.floor(sec / 3600), m = Math.round((sec % 3600) / 60);
|
||||
if (m === 60) { h += 1; m = 0; }
|
||||
return h > 0 ? (m > 0 ? h + "h " + m + "min" : h + "h") : m + " min";
|
||||
}
|
||||
// Project track points into a WxH box (equirectangular at track scale, padded, Y down).
|
||||
function _projectTrack(points, w, h, pad) {
|
||||
let lats = points.map(p => p.lat), lons = points.map(p => p.lon);
|
||||
let minLa = Math.min(...lats), maxLa = Math.max(...lats), minLo = Math.min(...lons), maxLo = Math.max(...lons);
|
||||
let kx = Math.cos((minLa + maxLa) / 2 * Math.PI / 180); // lon → same metric scale as lat
|
||||
let spanX = ((maxLo - minLo) * kx) || 1e-6, spanY = (maxLa - minLa) || 1e-6;
|
||||
let scale = Math.min((w - 2 * pad) / spanX, (h - 2 * pad) / spanY);
|
||||
let ox = (w - spanX * scale) / 2, oy = (h - spanY * scale) / 2;
|
||||
return points.map(p => ({ x: ox + (p.lon - minLo) * kx * scale, y: oy + (maxLa - p.lat) * scale }));
|
||||
}
|
||||
// Build the share PNG dataURL. opts.bg: "transparent" | "photo".
|
||||
async function buildShareImage(session, stats, points, opts) {
|
||||
opts = opts || {};
|
||||
let W = 1080, H = 1920;
|
||||
let cv = document.createElement("canvas"); cv.width = W; cv.height = H;
|
||||
let ctx = cv.getContext("2d");
|
||||
if (opts.bg === "photo") {
|
||||
let bg = localStorage.getItem("baldav_share_bg");
|
||||
if (bg) {
|
||||
await new Promise((res) => {
|
||||
let img = new Image();
|
||||
img.onload = () => { let s = Math.max(W / img.width, H / img.height); let dw = img.width * s, dh = img.height * s; ctx.drawImage(img, (W - dw) / 2, (H - dh) / 2, dw, dh); res(); };
|
||||
img.onerror = () => res();
|
||||
img.src = bg;
|
||||
});
|
||||
}
|
||||
let g = ctx.createLinearGradient(0, H * 0.4, 0, H);
|
||||
g.addColorStop(0, "rgba(0,0,0,0)"); g.addColorStop(1, "rgba(0,0,0,0.55)");
|
||||
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
||||
}
|
||||
// Track shape (top ~42%).
|
||||
if (points && points.length >= 2) {
|
||||
let boxTop = 90, boxH = Math.round(H * 0.42);
|
||||
let pts = _projectTrack(points.map(p => ({ lat: p.lat, lon: p.lon })), W, boxH, 130);
|
||||
let col = (window.GPS && GPS.config && GPS.config.trackColor) || "#2f6bff";
|
||||
ctx.lineJoin = "round"; ctx.lineCap = "round";
|
||||
ctx.shadowColor = col; ctx.shadowBlur = 30; ctx.strokeStyle = col; ctx.lineWidth = 12;
|
||||
ctx.beginPath(); pts.forEach((p, i) => i ? ctx.lineTo(p.x, p.y + boxTop) : ctx.moveTo(p.x, p.y + boxTop));
|
||||
ctx.stroke(); ctx.shadowBlur = 0;
|
||||
}
|
||||
// Stats.
|
||||
let cx = W / 2;
|
||||
function bigStat(y, value, label) {
|
||||
ctx.textAlign = "center";
|
||||
ctx.shadowColor = "rgba(0,0,0,0.6)"; ctx.shadowBlur = 10; ctx.shadowOffsetY = 3;
|
||||
ctx.fillStyle = "#fff"; ctx.font = "700 100px system-ui, -apple-system, sans-serif"; ctx.fillText(value, cx, y);
|
||||
ctx.shadowBlur = 5; ctx.fillStyle = "rgba(255,255,255,0.85)"; ctx.font = "400 42px system-ui, -apple-system, sans-serif"; ctx.fillText(label, cx, y + 56);
|
||||
ctx.shadowBlur = 0; ctx.shadowOffsetY = 0;
|
||||
}
|
||||
let y0 = Math.round(H * 0.55);
|
||||
bigStat(y0, (stats.distance / 1000).toFixed(1) + " km", "Distance");
|
||||
bigStat(y0 + 250, _shareDuration(stats.duration || 0), "Time");
|
||||
bigStat(y0 + 500, (stats.avgSpeed || 0).toFixed(1) + " km/h", "Speed");
|
||||
let footer = [];
|
||||
if (stats.elevGain != null) footer.push("↑ " + Math.round(stats.elevGain) + " m");
|
||||
if (stats.kcal != null) footer.push(Math.round(stats.kcal) + " kcal");
|
||||
if (session && session.date) footer.push(session.date);
|
||||
if (footer.length) {
|
||||
ctx.textAlign = "center"; ctx.shadowColor = "rgba(0,0,0,0.6)"; ctx.shadowBlur = 6;
|
||||
ctx.fillStyle = "rgba(255,255,255,0.9)"; ctx.font = "400 36px system-ui, -apple-system, sans-serif";
|
||||
ctx.fillText(footer.join(" · "), cx, y0 + 650); ctx.shadowBlur = 0;
|
||||
}
|
||||
return cv.toDataURL("image/png");
|
||||
}
|
||||
// Send the generated PNG to the native layer: action "share" (system chooser) or "save" (SAF).
|
||||
async function exportShareImage(dataUrl, filename, action) {
|
||||
if (!dataUrl) return;
|
||||
let base64 = dataUrl.split(",")[1];
|
||||
try {
|
||||
await window.__TAURI__.core.invoke("export_image", { filename, base64, action });
|
||||
showToast(action === "share" ? "Открываю «Поделиться»…" : "Сохранение…");
|
||||
} catch (e) { showToast("Экспорт картинки: " + (e.message || e)); }
|
||||
}
|
||||
// Preview modal with a transparent/photo toggle + Share/Save.
|
||||
function showShareModal(session, stats, points) {
|
||||
let ov = document.createElement("div");
|
||||
ov.className = "share-modal";
|
||||
ov.innerHTML = `
|
||||
<div class="share-box">
|
||||
<img id="share-img" class="share-preview" alt="">
|
||||
<div class="share-toggle">
|
||||
<button id="share-bg-t" class="btn-secondary active">Прозрачный</button>
|
||||
<button id="share-bg-p" class="btn-secondary">С фото</button>
|
||||
</div>
|
||||
<div class="share-actions">
|
||||
<button id="share-do" class="btn-primary">Поделиться</button>
|
||||
<button id="share-save" class="btn-secondary">Сохранить</button>
|
||||
<button id="share-close" class="btn-secondary" style="color:#cf6679">Закрыть</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(ov);
|
||||
let bg = "transparent", dataUrl = null;
|
||||
let img = ov.querySelector("#share-img");
|
||||
let bt = ov.querySelector("#share-bg-t"), bp = ov.querySelector("#share-bg-p");
|
||||
async function render() { dataUrl = await buildShareImage(session, stats, points, { bg }); img.src = dataUrl; }
|
||||
bt.onclick = () => { bg = "transparent"; bt.classList.add("active"); bp.classList.remove("active"); render(); };
|
||||
bp.onclick = () => {
|
||||
if (!localStorage.getItem("baldav_share_bg")) { showToast("Подложка не выбрана — задайте в Настройках"); return; }
|
||||
bg = "photo"; bp.classList.add("active"); bt.classList.remove("active"); render();
|
||||
};
|
||||
let filename = () => "baldav-" + ((session && session.date) || "track") + ".png";
|
||||
ov.querySelector("#share-do").onclick = () => exportShareImage(dataUrl, filename(), "share");
|
||||
ov.querySelector("#share-save").onclick = () => exportShareImage(dataUrl, filename(), "save");
|
||||
ov.querySelector("#share-close").onclick = () => ov.remove();
|
||||
render();
|
||||
}
|
||||
|
||||
// Slope tint by absolute grade in %, in 1%-wide discrete bands (hard boundaries
|
||||
// kept — one colour per segment): green -> yellow -> red -> brown -> black.
|
||||
// <1%=0 .. 9–10%=9, >=10% = black. Both up- and downhill coloured by magnitude.
|
||||
|
|
@ -5012,6 +5129,7 @@ async function showSessionDetail(sessionId) {
|
|||
<button id="sd-recalc" class="btn-secondary full-width" style="margin-top:8px">Пересчитать</button>
|
||||
<button id="sd-refine" class="btn-secondary full-width" style="margin-top:8px">Уточнить высоты (онлайн)</button>
|
||||
<button id="sd-export" class="btn-secondary full-width" style="margin-top:8px">Экспорт GPX</button>
|
||||
<button id="sd-share" class="btn-secondary full-width" style="margin-top:8px">Похвастаться</button>
|
||||
<button id="sd-delete" class="btn-secondary full-width" style="margin-top:8px;color:#cf6679">Удалить тренировку</button>
|
||||
`, () => {});
|
||||
|
||||
|
|
@ -5601,6 +5719,9 @@ async function showSessionDetail(sessionId) {
|
|||
let exp = document.querySelector("#sd-export");
|
||||
if (exp) exp.onclick = () => GPS.exportGPX(sessionId);
|
||||
|
||||
let shareBtn = document.querySelector("#sd-share");
|
||||
if (shareBtn) shareBtn.onclick = () => showShareModal(session, stats, points);
|
||||
|
||||
// Track ID — click to copy to clipboard
|
||||
let idEl = document.querySelector("#sd-track-id");
|
||||
if (idEl) idEl.onclick = () => copyToClipboard(String(sessionId), "ID скопирован: " + sessionId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue