Full DB backup: export all data to a file and restore it (swap DB on next launch)
This commit is contained in:
parent
75eca0e501
commit
682bee6e9a
6 changed files with 205 additions and 1 deletions
|
|
@ -182,6 +182,9 @@
|
|||
| `ble_save_device` | Сохранение привязанного устройства |
|
||||
| `import_tiles` | Чтение тайлового SQLite батчами (base64) |
|
||||
| `download_file` | Скачивание бинарного файла по URL (без лимита размера) |
|
||||
| `export_db` | Бэкап всей БД: `VACUUM INTO` чистой копии в app-root -> команда `exportdb:` -> Kotlin SAF-сохранение (`saveDbLauncher`, стрим файла) |
|
||||
| `import_db` | Восстановление: валидирует `picked_import.db` (`quick_check` + таблицы BalDav), кладёт в `pending_restore.db`; Kotlin `onCreate` свопит на `baldav.db` до открытия БД |
|
||||
| `restart_app` | Команда `restartapp` -> Kotlin перезапускает процесс (применить восстановление) |
|
||||
| `rebuild_roads` | Перестройка road_points из всех треков (2м дедуп; пеший point (`walk=1`) → atype 'walk') |
|
||||
| `get_road_points` | Чтение road_points батчами (обход лимита SQL plugin) |
|
||||
| `get_ride_graph` | Граф проезженных дорог для роутинга по своим трекам: узлы на сетке ~20м (исключая `walk=1`), рёбра между последовательными точками сессии (вес = между узлами). `{nodes,edges}` |
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
- **Роутинг сначала по проезженному** - публичные данные BRouter часто не совпадают с землёй (роутит по несуществующим дорогам). Поэтому планировщик для каждой дорожной ноги сначала пробует проложить путь **по дорогам, где ты реально ездил** (граф из своих треков: узлы на сетке ~20м, исключая пешие куски), и только если проезженного рядом нет или путь выходит сильно кривой - обращается к BRouter. Ридден-нога рисуется тонкой линией (без высот), BRouter-нога - оковкой с раскраской. Граф строится из `gps_points` командой `get_ride_graph`, кэшируется.
|
||||
- **Объединение поездок** - долгий тап по треку в истории включает выделение, тап отмечает ещё; когда выбрано 2+, кнопка **«Объединить»** склеивает их в одну тренировку (точки всех сессий переносятся в самую раннюю по времени, порядок по таймстампу, стата пересчитывается). Полезно, когда одна тренировка записалась несколькими кусками (например, отдельный заезд оказался в своей сессии - после склейки он попадёт в «Разбор повторов»). Разрыв между треками больше 5 минут - предупреждает, но не мешает. **Разные велосипеды склеить нельзя** - показывает предупреждение (передача/вес отличаются, мощность и каденс посчитались бы неверно); если это один велосипед, задай его трекам одинаково и склеивай. Обратимо: у каждой точки хранится исходная сессия (`gps_points.orig_session_id`).
|
||||
- **Разрезание трека** - в детали трека кнопка **«Разрезать трек»** включает режим: тап по точке на карте -> в её окошке кнопка **«✂ Разрезать здесь»** -> подтверждение -> второй кусок становится отдельной тренировкой (с тем же велосипедом; порядок точек по времени). Зачем: если часть трека проехана на другом велосипеде или другой передаче - разрежь в точке смены, назначь каждому куску свой велосипед (в детали: «Велосипед» + «Пересчитать»), и мощность/каденс посчитаются по правильной передаче на каждом куске.
|
||||
- **Резервная копия всех данных** - в Настройках **Экспорт** сливает всю базу (`baldav.db`) в файл через SAF (`export_db` -> `VACUUM INTO` чистой сжатой копии, ~на треть меньше), **Импорт** выбирает файл, проверяет его (`PRAGMA quick_check` + наличие таблиц BalDav) и восстанавливает с ЗАМЕНОЙ: файл кладётся в `pending_restore.db`, приложение перезапускается, и Kotlin в `onCreate` (до открытия БД) свопит его на `baldav.db`. Секретов в базе нет. Зачем: переносить наработки между сборками (**debug <-> release** - разные подписи, переустановка стирает данные) без сторонних бэкаперов. Внутриприложенческой авто-установки APK намеренно нет - только экспорт/импорт данных.
|
||||
- **Разбор повторов (интервалы)** - катаешь один подъём несколько раз за поездку (один трек, без стоп/старт на каждом заезде) - деталь трека сама находит повторяемый участок по самоподобию (сетка ~10м, повторно посещённые ячейки, самый длинный эталонный проход) и в сворачиваемой секции «Разбор повторов» показывает таблицу заездов (номер, время, скорость, каденс, мощность) и главное число - **последний / первый**, с вердиктом: >=100% зелёный (можно завтра), 90-99% жёлтый (тяжёлая норма), <90% красный (два дня спокойно). Каждая точка проецируется на опорную линию (снос вбок в пределах ~30м, например при возне с седлом, не режет покрытие); время заезда - только секунды в движении (остановки вырезаются, скорость нормируется на длину участка). Найденный участок подсвечивается на карте. Только вело. `GPS.detectRepeats`; повторов нет (обычная поездка A-B) - так и пишет.
|
||||
- **Похвастаться (шэр-картинка)** - в детали трека кнопка **«Похвастаться»**: генерирует картинку в формате сторис (9:16) с формой трека и статами (Дистанция/Время/Средняя скорость крупно + набор/ккал/дата), **прозрачную** (PNG с альфой для наложения на своё фото) или **на фото-подложке** из настроек. В модалке предпросмотра галочками выбирается, что класть на картинку (форма трека, дистанция, время, средняя/макс. скорость, набор высоты, калории, дата; выбор запоминается), переключатель прозрачный/фото и кнопки **Поделиться** (системный шэр) и **Сохранить** (SAF). Подложка выбирается в Настройках.
|
||||
- **Сегменты - сворачиваемо** - в детали трека список сегментов высот (climb/descent, до десятков штук) свёрнут в аккордеон (`<details>`, свёрнут по умолчанию, счётчик в заголовке); тап разворачивает/сворачивает. Кнопки внизу (Похвастаться, Экспорт, Пересчитать, Удалить) остаются доступны без долгого скролла.
|
||||
|
|
|
|||
42
docs/session-logs/2026-09-21_db-backup.md
Normal file
42
docs/session-logs/2026-09-21_db-backup.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Сессия 2026-09-21 (2) - экспорт/импорт всей БД (резервная копия)
|
||||
|
||||
## Контекст
|
||||
Смена сборки debug<->release стирает данные (разные подписи -> переустановка). Решили:
|
||||
НЕ делать внутриприложенческую авто-установку APK, а сделать полный бэкап/восстановление БД.
|
||||
Записано в память: `project_no_self_install`. Секретов в базе нет.
|
||||
|
||||
## Сделано
|
||||
- Rust (`lib.rs`):
|
||||
- `db_and_root(app)` - резолв пути `baldav.db` + app-root (как в rebuild_roads).
|
||||
- `export_db(filename)` - `VACUUM INTO` чистой копии в `app-root/export_db.tmp` (консистентно
|
||||
при живом соединении, сжимает ~на треть), команда `exportdb:<filename>` в gps_command.txt.
|
||||
- `import_db()` - валидирует `picked_import.db` (`PRAGMA quick_check`=='ok' + есть хоть одна
|
||||
таблица из gps_sessions/products/profile), копирует в `pending_restore.db`, удаляет picked.
|
||||
- `restart_app()` - команда `restartapp`.
|
||||
- Все три в generate_handler.
|
||||
- Kotlin (`MainActivity.kt`):
|
||||
- `pendingDbFile` + `saveDbLauncher` (CreateDocument octet-stream, СТРИМ файла, не в память -
|
||||
файл десятки МБ).
|
||||
- Ветка `exportdb:` -> SAF-сохранение `export_db.tmp`.
|
||||
- Ветка `restartapp` -> relaunch (getLaunchIntentForPackage + FLAG_ACTIVITY_NEW_TASK) +
|
||||
`Runtime.exit(0)`.
|
||||
- `onCreate` (сразу после appDataDir, ДО открытия БД): если есть `pending_restore.db` -
|
||||
скопировать на `baldav.db`, удалить journal/wal/shm и pending. Тот же паттерн, что копия сида.
|
||||
- JS (`main.js`): в Настройках блок «Резервная копия всех данных» с кнопками Экспорт/Импорт.
|
||||
Экспорт -> `export_db`. Импорт -> confirm (заменит всё) -> `pickLocalFile` -> `import_db` ->
|
||||
confirm перезапуска -> `restart_app`. Импорт только на Android.
|
||||
|
||||
## Проверка
|
||||
- JS синтаксис ок. Автономный тест `VACUUM INTO` на десктоп-БД: 53МБ -> 36МБ, quick_check ok,
|
||||
сессии на месте (валидная сжатая копия). Ядро экспорта подтверждено.
|
||||
- SAF-сохранение, своп при onCreate и рестарт - только на устройстве (Kotlin), десктоп не
|
||||
воспроизводит. Проверка на телефоне: экспорт -> файл-бэкап валиден; импорт другого бэкапа ->
|
||||
данные заменились после рестарта. (Сборка bump до 0.1.465.)
|
||||
|
||||
## Замечания
|
||||
- Сжатие gzip не делал: VACUUM INTO уже компактит; flate2 в зависимостях нет. Добавлю, если
|
||||
нужно меньше.
|
||||
- Схема-совместимость: миграции в db.js идемпотентны (ALTER ... IF NOT EXISTS), старый бэкап
|
||||
подтянется вверх, новый в старое приложение - лишние колонки безвредны.
|
||||
|
||||
## На роутере всё ещё 0.1.464 (build 464). Новый бэкап-билд (465) на роутер не выкатывал.
|
||||
|
|
@ -6861,6 +6861,16 @@ async function renderSettings(container) {
|
|||
<button class="btn-secondary" id="s-share-bg-clear" style="padding:6px 12px;font-size:13px;color:#cf6679">Сброс</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-left">
|
||||
<div class="settings-item-title">Резервная копия всех данных</div>
|
||||
<div class="settings-item-desc">Экспорт всей базы в файл и восстановление из него - чтобы переносить наработки между версиями (debug/release) без потерь. Импорт заменяет ВСЁ и перезапускает приложение.</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn-secondary" id="s-export-db" style="padding:6px 12px;font-size:13px;">Экспорт</button>
|
||||
<button class="btn-secondary" id="s-import-db" style="padding:6px 12px;font-size:13px;">Импорт</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-left">
|
||||
<div class="settings-item-title">Очистить продукты без магазина</div>
|
||||
|
|
@ -7266,6 +7276,39 @@ async function renderSettings(container) {
|
|||
let sbgClear = container.querySelector("#s-share-bg-clear");
|
||||
if (sbgClear) sbgClear.onclick = () => { localStorage.removeItem("baldav_share_bg"); showToast("Подложка сброшена"); };
|
||||
|
||||
// Full-DB backup: export the whole database to a file (SAF save), and restore from one.
|
||||
let expDbBtn = container.querySelector("#s-export-db");
|
||||
if (expDbBtn) expDbBtn.onclick = async () => {
|
||||
let prev = expDbBtn.textContent;
|
||||
expDbBtn.disabled = true; expDbBtn.textContent = "Готовлю...";
|
||||
try {
|
||||
let filename = "baldav-backup-" + new Date().toISOString().slice(0, 10) + ".db";
|
||||
let res = await window.__TAURI__.core.invoke("export_db", { filename });
|
||||
let size = 0; try { size = JSON.parse(res).size || 0; } catch {}
|
||||
showToast("Копия готова (" + (size / 1048576).toFixed(1) + " МБ) - выберите, куда сохранить");
|
||||
} catch (e) { appAlert("Экспорт не удался: " + (e.message || e)); }
|
||||
expDbBtn.disabled = false; expDbBtn.textContent = prev;
|
||||
};
|
||||
let impDbBtn = container.querySelector("#s-import-db");
|
||||
if (impDbBtn) impDbBtn.onclick = async () => {
|
||||
if (!/Android/i.test(navigator.userAgent)) { appAlert("Импорт базы доступен только на устройстве."); return; }
|
||||
if (!(await appConfirm("Импорт ЗАМЕНИТ все текущие данные (треки, питание, профиль, велосипеды) данными из файла. Несохранённое пропадёт.\n\nСделайте экспорт текущей базы, если она нужна. Продолжить?", "Выбрать файл"))) return;
|
||||
let prev = impDbBtn.textContent;
|
||||
impDbBtn.disabled = true; impDbBtn.textContent = "Выбор...";
|
||||
try {
|
||||
let path = await pickLocalFile("*/*");
|
||||
if (!path) { showToast("Файл не выбран"); return; }
|
||||
impDbBtn.textContent = "Проверка...";
|
||||
await window.__TAURI__.core.invoke("import_db"); // validates picked_import.db + stages it
|
||||
if (await appConfirm("База проверена и готова. Приложение перезапустится, чтобы применить восстановление. Перезапустить сейчас?", "Перезапустить")) {
|
||||
await window.__TAURI__.core.invoke("restart_app");
|
||||
} else {
|
||||
showToast("Восстановление применится при следующем запуске");
|
||||
}
|
||||
} catch (e) { appAlert("Импорт не удался: " + (e.message || e)); }
|
||||
impDbBtn.disabled = false; impDbBtn.textContent = prev;
|
||||
};
|
||||
|
||||
// Download tiles from router — the list comes from the router's directory
|
||||
// listing, so a newly uploaded .db shows up without rebuilding the app.
|
||||
container.querySelector("#s-download-tiles").onclick = async () => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class MainActivity : TauriActivity() {
|
|||
private var commandObserver: FileObserver? = null
|
||||
private var pendingSaveContent: String? = null
|
||||
private var pendingImageBytes: ByteArray? = null
|
||||
private var pendingDbFile: File? = null
|
||||
private var backWebView: WebView? = null
|
||||
|
||||
// Disable WryActivity's generated back handler: its mWebView.goBack() does NOT
|
||||
|
|
@ -78,6 +79,22 @@ class MainActivity : TauriActivity() {
|
|||
pendingImageBytes = null
|
||||
}
|
||||
|
||||
// SAF "create document" for the full DB backup — streams the staged file (can be tens of MB)
|
||||
// straight to the picked location instead of loading it into memory.
|
||||
private val saveDbLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/octet-stream")
|
||||
) { uri: Uri? ->
|
||||
val src = pendingDbFile
|
||||
if (uri != null && src != null && src.exists()) {
|
||||
try {
|
||||
contentResolver.openOutputStream(uri)?.use { out -> src.inputStream().use { it.copyTo(out, 1 shl 20) } }
|
||||
Log.d("BalDav", "DB exported to $uri (${src.length()} bytes)")
|
||||
src.delete()
|
||||
} catch (e: Exception) { Log.e("BalDav", "Failed to export DB", e) }
|
||||
}
|
||||
pendingDbFile = null
|
||||
}
|
||||
|
||||
// Share a PNG via the system chooser (write to cache, FileProvider content:// URI, ACTION_SEND).
|
||||
private fun shareImage(bytes: ByteArray, filename: String) {
|
||||
try {
|
||||
|
|
@ -127,6 +144,22 @@ class MainActivity : TauriActivity() {
|
|||
val appDataDir = filesDir.parentFile ?: filesDir
|
||||
Log.d("BalDav", "App data dir: ${appDataDir.absolutePath}")
|
||||
|
||||
// Restore-on-launch: a full-DB import stages pending_restore.db; swap it onto baldav.db NOW,
|
||||
// before the app opens the DB (tauri-plugin-sql opens it later, when db.js runs). Then clear
|
||||
// any stale journal so the restored file is the single source of truth.
|
||||
try {
|
||||
val pending = File(appDataDir, "pending_restore.db")
|
||||
if (pending.exists() && pending.length() > 0) {
|
||||
val dbFile = File(appDataDir, "baldav.db")
|
||||
pending.inputStream().use { i -> dbFile.outputStream().use { o -> i.copyTo(o, 1 shl 20) } }
|
||||
File(appDataDir, "baldav.db-journal").delete()
|
||||
File(appDataDir, "baldav.db-wal").delete()
|
||||
File(appDataDir, "baldav.db-shm").delete()
|
||||
pending.delete()
|
||||
Log.d("BalDav", "Restored DB from pending_restore.db (${dbFile.length()} bytes)")
|
||||
}
|
||||
} catch (e: Exception) { Log.e("BalDav", "DB restore failed", e) }
|
||||
|
||||
// Create command files if not exist (FileObserver needs them)
|
||||
val cmdFile = File(appDataDir, "gps_command.txt")
|
||||
if (!cmdFile.exists()) cmdFile.createNewFile()
|
||||
|
|
@ -515,6 +548,21 @@ class MainActivity : TauriActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
cmd.startsWith("exportdb:") -> {
|
||||
// exportdb:<filename> — SAF-save the DB copy Rust wrote to export_db.tmp.
|
||||
val filename = cmd.substringAfter("exportdb:").ifBlank { "baldav-backup.db" }
|
||||
val f = File(appDataDir, "export_db.tmp")
|
||||
if (f.exists()) { pendingDbFile = f; saveDbLauncher.launch(filename) }
|
||||
else Log.e("BalDav", "exportdb: export_db.tmp missing")
|
||||
}
|
||||
cmd == "restartapp" -> {
|
||||
// Relaunch the process so the staged DB (pending_restore.db) is swapped in onCreate.
|
||||
Log.d("BalDav", "Restarting app (DB restore)")
|
||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -624,6 +624,73 @@ fn read_file_base64(path: String) -> Result<String, String> {
|
|||
Ok(base64::engine::general_purpose::STANDARD.encode(&bytes))
|
||||
}
|
||||
|
||||
// Resolve the main baldav.db path (same fallbacks as rebuild_roads/get_road_points) and the
|
||||
// app root (where Kotlin's FileObserver watches). Root = data_dir/../.. on Android.
|
||||
fn db_and_root(app: &tauri::AppHandle) -> Result<(std::path::PathBuf, std::path::PathBuf), String> {
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let p1 = data_dir.join("../../baldav.db");
|
||||
let p2 = data_dir.join("baldav.db");
|
||||
let p3 = std::path::PathBuf::from(data_dir.to_str().unwrap_or("").replace("/files/dev.baldav.app", "")).join("baldav.db");
|
||||
let db = if p1.exists() { p1 } else if p2.exists() { p2.clone() } else { p3 };
|
||||
let root = data_dir.parent().and_then(|p| p.parent()).map(|p| p.to_path_buf()).unwrap_or(data_dir.clone());
|
||||
Ok((db, root))
|
||||
}
|
||||
|
||||
// Full backup: VACUUM the live DB into a clean copy in the app root, then tell Kotlin to
|
||||
// SAF-save it as `filename`. VACUUM INTO is consistent even while the app holds the DB open.
|
||||
#[tauri::command]
|
||||
fn export_db(app: tauri::AppHandle, filename: String) -> Result<String, String> {
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let (db_path, root) = db_and_root(&app)?;
|
||||
if !db_path.exists() { return Err("baldav.db не найдена".into()); }
|
||||
let out = root.join("export_db.tmp");
|
||||
let _ = std::fs::remove_file(&out);
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).map_err(|e| format!("open: {e}"))?;
|
||||
conn.execute("VACUUM INTO ?1", rusqlite::params![out.to_str().unwrap_or("")]).map_err(|e| format!("vacuum: {e}"))?;
|
||||
}
|
||||
let size = std::fs::metadata(&out).map(|m| m.len()).unwrap_or(0);
|
||||
let cmd = format!("exportdb:{}", filename);
|
||||
let _ = std::fs::write(data_dir.join("gps_command.txt"), &cmd);
|
||||
let _ = std::fs::write(root.join("gps_command.txt"), &cmd);
|
||||
Ok(format!("{{\"size\":{}}}", size))
|
||||
}
|
||||
|
||||
// Full restore: validate the picked DB (picked_import.db) and stage it as pending_restore.db.
|
||||
// Kotlin swaps it onto baldav.db on the next launch, BEFORE the app opens the DB. Returns ok;
|
||||
// caller then restarts the app.
|
||||
#[tauri::command]
|
||||
fn import_db(app: tauri::AppHandle) -> Result<String, String> {
|
||||
let (_db, root) = db_and_root(&app)?;
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let picked = if root.join("picked_import.db").exists() { root.join("picked_import.db") }
|
||||
else { data_dir.join("picked_import.db") };
|
||||
if !picked.exists() { return Err("файл не выбран".into()); }
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&picked).map_err(|e| format!("не открыть файл: {e}"))?;
|
||||
let ok: String = conn.query_row("PRAGMA quick_check", [], |r| r.get(0)).map_err(|e| format!("проверка: {e}"))?;
|
||||
if ok != "ok" { return Err(format!("база повреждена: {ok}")); }
|
||||
let has: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('gps_sessions','products','profile')",
|
||||
[], |r| r.get(0)).map_err(|e| format!("схема: {e}"))?;
|
||||
if has < 1 { return Err("это не похоже на базу BalDav".into()); }
|
||||
}
|
||||
let pending = root.join("pending_restore.db");
|
||||
std::fs::copy(&picked, &pending).map_err(|e| format!("подготовка: {e}"))?;
|
||||
let _ = std::fs::remove_file(&picked);
|
||||
Ok("staged".into())
|
||||
}
|
||||
|
||||
// Ask Kotlin to restart the app process (used after import_db to load the restored DB).
|
||||
#[tauri::command]
|
||||
fn restart_app(app: tauri::AppHandle) -> Result<String, String> {
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let (_db, root) = db_and_root(&app)?;
|
||||
let _ = std::fs::write(data_dir.join("gps_command.txt"), "restartapp");
|
||||
let _ = std::fs::write(root.join("gps_command.txt"), "restartapp");
|
||||
Ok("ok".into())
|
||||
}
|
||||
|
||||
// Read tiles from a SQLite file in batches for import into IndexedDB
|
||||
#[tauri::command]
|
||||
fn import_tiles(path: String, offset: u32, batch_size: u32) -> Result<String, String> {
|
||||
|
|
@ -1088,7 +1155,7 @@ pub fn run() {
|
|||
.add_migrations("sqlite:baldav.db", migrations)
|
||||
.build(),
|
||||
)
|
||||
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_ride_graph, get_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr])
|
||||
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_ride_graph, get_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, export_db, import_db, restart_app, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue