Очистить продукты без магазина
@@ -6913,6 +6923,40 @@ async function renderSettings(container) {
}
};
+ // Share-image background: pick a photo, downscale, store as a data URL in localStorage.
+ let sbgBtn = container.querySelector("#s-share-bg");
+ if (sbgBtn) sbgBtn.onclick = async () => {
+ let prev = sbgBtn.textContent, path;
+ if (/Android/i.test(navigator.userAgent)) {
+ sbgBtn.disabled = true; sbgBtn.textContent = "Выбор...";
+ try { path = await pickLocalFile("image/*"); } catch (e) { showToast("Ошибка выбора: " + (e.message || e)); }
+ sbgBtn.disabled = false; sbgBtn.textContent = prev;
+ } else {
+ path = await appPrompt("Путь к фото-подложке:", "");
+ }
+ if (!path) return;
+ sbgBtn.disabled = true; sbgBtn.textContent = "Загрузка...";
+ try {
+ let b64 = await window.__TAURI__.core.invoke("read_file_base64", { path });
+ let mime = /\.png$/i.test(path) ? "image/png" : "image/jpeg";
+ await new Promise((res, rej) => {
+ let img = new Image();
+ img.onload = () => {
+ let s = Math.min(1, 1080 / img.width);
+ let c = document.createElement("canvas"); c.width = Math.round(img.width * s); c.height = Math.round(img.height * s);
+ c.getContext("2d").drawImage(img, 0, 0, c.width, c.height);
+ try { localStorage.setItem("baldav_share_bg", c.toDataURL("image/jpeg", 0.85)); res(); } catch (e) { rej(e); }
+ };
+ img.onerror = () => rej(new Error("не удалось прочитать изображение"));
+ img.src = "data:" + mime + ";base64," + b64;
+ });
+ showToast("Подложка сохранена");
+ } catch (e) { showToast("Подложка: " + (e.message || e)); }
+ sbgBtn.disabled = false; sbgBtn.textContent = prev;
+ };
+ let sbgClear = container.querySelector("#s-share-bg-clear");
+ if (sbgClear) sbgClear.onclick = () => { localStorage.removeItem("baldav_share_bg"); showToast("Подложка сброшена"); };
+
// 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 () => {
diff --git a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt
index 21b769b..f69328f 100644
--- a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt
+++ b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt
@@ -24,6 +24,7 @@ import javax.net.ssl.HttpsURLConnection
class MainActivity : TauriActivity() {
private var commandObserver: FileObserver? = null
private var pendingSaveContent: String? = null
+ private var pendingImageBytes: ByteArray? = null
private var backWebView: WebView? = null
// Disable WryActivity's generated back handler: its mWebView.goBack() does NOT
@@ -66,6 +67,33 @@ class MainActivity : TauriActivity() {
pendingSaveContent = null
}
+ // SAF "create document" for the share PNG (bytes, image/png mime).
+ private val saveImageLauncher = registerForActivityResult(
+ ActivityResultContracts.CreateDocument("image/png")
+ ) { uri: Uri? ->
+ if (uri != null && pendingImageBytes != null) {
+ try { contentResolver.openOutputStream(uri)?.use { it.write(pendingImageBytes!!) } }
+ catch (e: Exception) { Log.e("BalDav", "Failed to save image", e) }
+ }
+ pendingImageBytes = null
+ }
+
+ // Share a PNG via the system chooser (write to cache, FileProvider content:// URI, ACTION_SEND).
+ private fun shareImage(bytes: ByteArray, filename: String) {
+ try {
+ val dir = File(cacheDir, "shared"); dir.mkdirs()
+ val f = File(dir, if (filename.isBlank()) "baldav.png" else filename)
+ f.writeBytes(bytes)
+ val uri = androidx.core.content.FileProvider.getUriForFile(this, "$packageName.fileprovider", f)
+ val send = Intent(Intent.ACTION_SEND).apply {
+ type = "image/png"
+ putExtra(Intent.EXTRA_STREAM, uri)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ startActivity(Intent.createChooser(send, "Поделиться").addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION))
+ } catch (e: Exception) { Log.e("BalDav", "share image failed", e) }
+ }
+
// SAF "open document" picker. A content:// URI has no filesystem path, so the
// picked file is copied into the app dir and its path is written to
// open_file_result.txt, which the JS side polls via the Rust get_picked_file.
@@ -472,6 +500,21 @@ class MainActivity : TauriActivity() {
Log.d("BalDav", "Opening file picker for mime=$mime")
openFileLauncher.launch(arrayOf(mime))
}
+ cmd.startsWith("exportimg:") -> {
+ // exportimg:
: — decode export_image.b64, then SAF-save or share.
+ val rest = cmd.substringAfter("exportimg:")
+ val action = rest.substringBefore(":")
+ val filename = rest.substringAfter(":")
+ val b64File = File(appDataDir, "export_image.b64")
+ if (b64File.exists()) {
+ val bytes = try { android.util.Base64.decode(b64File.readText().trim(), android.util.Base64.DEFAULT) } catch (e: Exception) { null }
+ b64File.delete()
+ if (bytes != null) {
+ if (action == "save") { pendingImageBytes = bytes; saveImageLauncher.launch(filename) }
+ else shareImage(bytes, filename)
+ }
+ }
+ }
}
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 8aa0ab3..c2602a5 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -533,6 +533,30 @@ fn save_file(app: tauri::AppHandle, filename: String, content: String) -> Result
Ok("save_requested".to_string())
}
+// Export a PNG (base64) via the file bridge → Kotlin decodes + SAF-saves ("save") or shares it
+// via a chooser ("share"). Mirrors save_file but carries binary as base64.
+#[tauri::command]
+fn export_image(app: tauri::AppHandle, filename: String, base64: String, action: String) -> Result {
+ let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
+ std::fs::create_dir_all(&data_dir).ok();
+ let root = data_dir.parent().and_then(|p| p.parent()).map(|p| p.to_path_buf());
+ let write_both = |name: &str, content: &str| {
+ let _ = std::fs::write(data_dir.join(name), content);
+ if let Some(r) = &root { let _ = std::fs::write(r.join(name), content); }
+ };
+ write_both("export_image.b64", &base64);
+ write_both("gps_command.txt", &format!("exportimg:{}:{}", action, filename));
+ Ok("export_requested".to_string())
+}
+
+// Read any file and return its bytes base64-encoded (for loading a picked image into the WebView).
+#[tauri::command]
+fn read_file_base64(path: String) -> Result {
+ use base64::Engine;
+ let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
+ Ok(base64::engine::general_purpose::STANDARD.encode(&bytes))
+}
+
// 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 {
@@ -997,7 +1021,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_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, 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_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])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}