diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt index 74eae8cf..4e929576 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt @@ -36,6 +36,7 @@ import com.rtbishop.look4sat.core.data.repository.SelectionRepo import com.rtbishop.look4sat.core.data.repository.SensorsRepo import com.rtbishop.look4sat.core.data.repository.SettingsRepo import com.rtbishop.look4sat.core.data.repository.UpdateRepository +import com.rtbishop.look4sat.core.data.repository.WavelogRepository import com.rtbishop.look4sat.core.data.source.LocalSource import com.rtbishop.look4sat.core.data.source.RemoteSource import com.rtbishop.look4sat.core.data.usecase.AddToCalendar @@ -52,6 +53,7 @@ import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo +import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository import com.rtbishop.look4sat.core.domain.repository.MutualPassData import com.rtbishop.look4sat.core.domain.source.ILocalSource import com.rtbishop.look4sat.core.domain.source.IRemoteSource @@ -81,6 +83,7 @@ class MainContainer(private val context: Context) : IMainContainer { override val databaseRepo = provideDatabaseRepo() override val amSatRepo by lazy { AmSatRepository(remoteSource, appScope) } override val updateRepo by lazy { UpdateRepository(remoteSource) } + override val wavelogRepo: IWavelogRepository by lazy { WavelogRepository() } override val radioTrackingService: IRadioTrackingService by lazy { val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager RadioTrackingService(appScope, manager, satelliteRepo, settingsRepo) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index da83ca0a..fb59a705 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -29,6 +29,7 @@ import com.rtbishop.look4sat.core.domain.model.OtherSettings import com.rtbishop.look4sat.core.domain.model.PassesSettings import com.rtbishop.look4sat.core.domain.model.RCSettings import com.rtbishop.look4sat.core.domain.model.RadioControlSettings +import com.rtbishop.look4sat.core.domain.model.WavelogSettings import com.rtbishop.look4sat.core.domain.predict.GeoPos import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo import com.rtbishop.look4sat.core.domain.utility.positionToQth @@ -81,6 +82,7 @@ class SettingsRepo( private val keyStateOfUtc = "stateOfUtc" private val keyStateOfLightTheme = "stateOfLightTheme" private val keyStateOfNightMode = "stateOfNightMode" + private val keyStateOfMapGrid = "stateOfMapGrid" private val keyStationAltitude = "stationAltitude" private val keyStationLatitude = "stationLatitude" private val keyStationLongitude = "stationLongitude" @@ -137,7 +139,48 @@ class SettingsRepo( } //endregion - //region # Passes filter settings + //region # Wavelog worked-grids settings + private val keyWavelogUrl = "wavelogUrl" + private val keyWavelogToken = "wavelogToken" + private val keyWorkedGrids = "workedGrids" + + private val _wavelogSettings = MutableStateFlow(getWavelogSettings()) + override val wavelogSettings: StateFlow = _wavelogSettings + + override fun updateWavelogSettings(settings: WavelogSettings) { + preferences.edit { + putString(keyWavelogUrl, settings.url.trim()) + putString(keyWavelogToken, settings.token.trim()) + } + _wavelogSettings.value = settings.copy(url = settings.url.trim(), token = settings.token.trim()) + } + + private fun getWavelogSettings(): WavelogSettings = WavelogSettings( + url = preferences.getString(keyWavelogUrl, null).orEmpty(), + token = preferences.getString(keyWavelogToken, null).orEmpty() + ) + + override fun getWorkedGrids(): Set { + val json = preferences.getString(keyWorkedGrids, null).orEmpty() + if (json.isBlank()) return emptySet() + return try { + val array = org.json.JSONArray(json) + (0 until array.length()).mapNotNull { i -> + array.optString(i).takeIf { it.isNotBlank() } + }.toSet() + } catch (_: Exception) { + emptySet() + } + } + + override fun setWorkedGrids(grids: Set) { + val array = org.json.JSONArray() + grids.sorted().forEach { array.put(it) } + preferences.edit { putString(keyWorkedGrids, array.toString()) } + } + //endregion + + //region # Transceivers settings private val _passesSettings = MutableStateFlow(getPassesSettings()) override val passesSettings: StateFlow = _passesSettings @@ -371,6 +414,7 @@ class SettingsRepo( putBoolean(keyStateOfUtc, new.stateOfUtc) putBoolean(keyStateOfLightTheme, new.stateOfLightTheme) putBoolean(keyStateOfNightMode, new.stateOfNightMode) + putBoolean(keyStateOfMapGrid, new.stateOfMapGrid) putBoolean(keyShouldSeeWarning, new.shouldSeeWarning) putBoolean(keyShouldSeeWhatsNew, new.shouldSeeWhatsNew) putString(keySstvMode, new.sstvMode) @@ -388,6 +432,7 @@ class SettingsRepo( stateOfUtc = preferences.getBoolean(keyStateOfUtc, false), stateOfLightTheme = preferences.getBoolean(keyStateOfLightTheme, false), stateOfNightMode = preferences.getBoolean(keyStateOfNightMode, false), + stateOfMapGrid = preferences.getBoolean(keyStateOfMapGrid, false), shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true), shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true), sstvMode = preferences.getString(keySstvMode, null) ?: "Auto", diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/WavelogRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/WavelogRepository.kt new file mode 100644 index 00000000..01647d04 --- /dev/null +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/WavelogRepository.kt @@ -0,0 +1,93 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.rtbishop.look4sat.core.data.repository + +import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject + +/** + * Fetches the list of worked gridsquares from a self-hosted Wavelog instance. + * + * Uses the Wavelog API v2 endpoint (Wavelog >= 3.1.0): + * GET {base}/index.php/api/v2/lookup?grid=all + * Authorization: Bearer wl2_... + * Response: {"data": {"grids": ["JN47", "JO30"], "count": 2}, ...} + * + * Uses a plain HttpURLConnection (bearer auth header) — IRemoteSource does not + * support custom headers, and no feature module touches this class directly. + */ +class WavelogRepository : IWavelogRepository { + + /** + * Fetch worked grids. Returns the grid set, or null on any failure + * (network error, non-200, malformed response, bad token). + */ + override suspend fun fetchWorkedGrids(url: String, token: String): Set? = withContext(Dispatchers.IO) { + if (url.isBlank() || token.isBlank()) return@withContext null + val base = url.trim().trimEnd('/') + val requestUrl = "$base/index.php/api/v2/lookup?grid=all" + try { + // IRemoteSource.getNetworkStream does not support custom headers, so + // run the bearer-token request directly here via java.net (simple GET, + // no OkHttp dependency added to this module's public API). + val connection = java.net.URL(requestUrl).openConnection() as java.net.HttpURLConnection + connectionTimeouts(connection) + connection.setRequestProperty("Authorization", "Bearer ${token.trim()}") + connection.requestMethod = "GET" + val code = connection.responseCode + if (code != 200) { + connection.disconnect() + return@withContext null + } + val body = connection.inputStream.bufferedReader().use { it.readText() } + connection.disconnect() + parseWorkedGrids(body) + } catch (e: Exception) { + println("WavelogRepository fetch failure: $e") + null + } + } + + private fun connectionTimeouts(connection: java.net.HttpURLConnection) { + connection.connectTimeout = 10_000 + connection.readTimeout = 15_000 + } + + internal fun parseWorkedGrids(body: String): Set? { + return try { + val obj = JSONObject(body) + val data = obj.optJSONObject("data") ?: return null + val grids = data.optJSONArray("grids") ?: return null + val result = mutableSetOf() + for (i in 0 until grids.length()) { + val grid = grids.optString(i, "").trim().uppercase() + // Keep only well-formed 4-character Maidenhead squares (e.g. OL62) + if (grid.length == 4 && grid[0] in 'A'..'R' && grid[1] in 'A'..'R' + && grid[2] in '0'..'9' && grid[3] in '0'..'9' + ) { + result.add(grid) + } + } + result + } catch (_: Exception) { + null + } + } +} diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt index a5458d1e..09101905 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt @@ -59,6 +59,7 @@ data class OtherSettings( val stateOfUtc: Boolean, val stateOfLightTheme: Boolean, val stateOfNightMode: Boolean = false, + val stateOfMapGrid: Boolean = false, val shouldSeeWarning: Boolean, val shouldSeeWhatsNew: Boolean, val sstvMode: String = "Auto", @@ -76,6 +77,13 @@ data class DataSourcesSettings( fun isTransceiverEnabled(index: Int): Boolean = transceiversEnabled.getOrElse(index) { true } } +data class WavelogSettings( + val url: String = "", + val token: String = "" +) { + val isConfigured: Boolean get() = url.isNotBlank() && token.isNotBlank() +} + data class RadioControlSettings( val enabled: Boolean, val radioModel: String, diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt index 346c7f3b..84544030 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt @@ -32,6 +32,7 @@ interface IMainContainer { val databaseRepo: IDatabaseRepo val amSatRepo: IAmSatRepository val updateRepo: IUpdateRepository + val wavelogRepo: IWavelogRepository val radioTrackingService: IRadioTrackingService val mutualPassData: StateFlow fun setMutualPassData(data: MutualPassData) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt index 69abea0a..fc475e22 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt @@ -23,6 +23,7 @@ import com.rtbishop.look4sat.core.domain.model.OtherSettings import com.rtbishop.look4sat.core.domain.model.PassesSettings import com.rtbishop.look4sat.core.domain.model.RCSettings import com.rtbishop.look4sat.core.domain.model.RadioControlSettings +import com.rtbishop.look4sat.core.domain.model.WavelogSettings import com.rtbishop.look4sat.core.domain.predict.GeoPos import kotlinx.coroutines.flow.StateFlow @@ -92,4 +93,11 @@ interface ISettingsRepo { fun getAmSatCallsign(): String fun setAmSatCallsign(callsign: String) //endregion + + //region # Wavelog worked-grids settings + val wavelogSettings: StateFlow + fun updateWavelogSettings(settings: WavelogSettings) + fun getWorkedGrids(): Set + fun setWorkedGrids(grids: Set) + //endregion } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IWavelogRepository.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IWavelogRepository.kt new file mode 100644 index 00000000..e1059675 --- /dev/null +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IWavelogRepository.kt @@ -0,0 +1,28 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.rtbishop.look4sat.core.domain.repository + +/** Fetches worked gridsquares from a self-hosted Wavelog instance (API v2). */ +interface IWavelogRepository { + + /** + * Fetch worked grids for the configured Wavelog URL/token. + * Returns the grid set, or null on any failure. + */ + suspend fun fetchWorkedGrids(url: String, token: String): Set? +} diff --git a/core/presentation/src/main/res/values-es/strings.xml b/core/presentation/src/main/res/values-es/strings.xml index 0f7df61f..4f8a0939 100644 --- a/core/presentation/src/main/res/values-es/strings.xml +++ b/core/presentation/src/main/res/values-es/strings.xml @@ -150,6 +150,7 @@ Habilitar animaciones radar Usar sensores para rotar vista de radar Activar filtro nocturno rojo + Mostrar cuadrícula Maidenhead en el mapa Resaltado de elevación Baja < %1$d° diff --git a/core/presentation/src/main/res/values-ru/strings.xml b/core/presentation/src/main/res/values-ru/strings.xml index 95a4dd7b..9093231a 100644 --- a/core/presentation/src/main/res/values-ru/strings.xml +++ b/core/presentation/src/main/res/values-ru/strings.xml @@ -149,6 +149,7 @@ Показывать анимацию радара Использовать сенсоры устройства Включить красный ночной фильтр + Показывать сетку Maidenhead на карте Подсветка высоты Низко < %1$d° diff --git a/core/presentation/src/main/res/values-si/strings.xml b/core/presentation/src/main/res/values-si/strings.xml index 441576b5..ff1fe5dc 100644 --- a/core/presentation/src/main/res/values-si/strings.xml +++ b/core/presentation/src/main/res/values-si/strings.xml @@ -150,6 +150,7 @@ radar sweep සජීවිකරණය සබල කරන්න Radar දර්ශනය කරකැවීමට සංවේදක භාවිතා කරන්න රතු රාත්‍රී පෙරහන සබල කරන්න + සිතියමේ Maidenhead ජාලකය පෙන්වන්න උස් උද්දීපනය අඩු < %1$d° diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index 14f01cca..dab528c7 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -192,6 +192,7 @@ Radar taramasını etkinleştir Radar görünümünü döndürmek için sensörleri kullan Kırmızı gece filtresini etkinleştir + Haritada Maidenhead ızgarasını göster Elevasyon vurgusu Düşük < %1$d° diff --git a/core/presentation/src/main/res/values-uk/strings.xml b/core/presentation/src/main/res/values-uk/strings.xml index 6993c71e..73fc6efa 100644 --- a/core/presentation/src/main/res/values-uk/strings.xml +++ b/core/presentation/src/main/res/values-uk/strings.xml @@ -150,6 +150,7 @@ Показувати анімацію радара Використовувати сенсори пристрою Увімкнути червоний нічний фільтр + Показувати сітку Maidenhead на мапі Підсвічування висоти Низько < %1$d° diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 2ee04103..448f6ff6 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -139,6 +139,15 @@ 输入站点的纬度 输入站点的经度 QTH 网格 + Wavelog 已通联网格 + 配置 + 已从 Wavelog 同步 — 通联 %1$d 个网格 + 未配置 — 连接你的 Wavelog 日志以在地图上高亮已通联网格 + Wavelog 地址 + API 令牌 + 已同步网格数:%1$d。请先保存,再点同步。 + 立即同步 + 同步中… 使用梅登黑德网格设置站点位置 卫星数据更新 @@ -202,6 +211,7 @@ 启用雷达扫描动画 使用传感器旋转雷达视图 启用红色夜间模式 + 地图显示 Maidenhead 网格 仰角高亮 低 < %1$d° diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 540683df..edb5fbc7 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -174,6 +174,15 @@ Set your ground station\'s latitude Set your ground station\'s longitude QTH locator settings + Wavelog worked grids + Configure + Synced from Wavelog — %1$d grids worked + Not configured — connect your Wavelog logbook to highlight worked grids on the map + Wavelog URL + API token + Worked grids synced: %1$d. Save first, then tap Sync. + Sync now + Syncing… Set station\'s position using locator Satellite data @@ -236,6 +245,7 @@ Enable radar sweep animation Use sensors to rotate radar view Enable red night mode filter + Show Maidenhead grid on map Elevation highlight Low < %1$d° diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt new file mode 100644 index 00000000..fc153a06 --- /dev/null +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt @@ -0,0 +1,204 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.rtbishop.look4sat.feature.map + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import org.osmdroid.views.MapView +import org.osmdroid.views.Projection +import org.osmdroid.views.overlay.Overlay +import kotlin.math.ceil +import kotlin.math.floor + +/** + * Maidenhead locator grid overlay, drawn directly on the map canvas. + * + * Zoom levels (map maxZoom = 7): + * - zoom < GRID_ZOOM_SUB: 10° x 20° fields with 2-character labels (e.g. "PM") + * - zoom >= GRID_ZOOM_SUB: 1° x 2° squares with 4-character labels (e.g. "PM95") + * + * All geometry is computed per-frame from the projection, so the overlay + * stays correct while panning/zooming. Grid lines that cross the antimeridian + * are drawn in segments clamped to the visible bounding box. + */ +class MaidenheadGridOverlay : Overlay() { + + private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + strokeWidth = 1.5f + style = Paint.Style.STROKE + color = Color.argb(160, 255, 224, 130) + } + private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + textSize = 26f + style = android.graphics.Paint.Style.FILL + color = Color.argb(220, 255, 224, 130) + setShadowLayer(3f, 2f, 2f, Color.BLACK) + } + private val workedPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = android.graphics.Paint.Style.FILL + color = Color.argb(90, 76, 217, 100) + } + + /** Worked gridsquares (4-char, uppercase) to highlight, e.g. {"OL62", "PM95"}. */ + var workedGrids: Set = emptySet() + + /** The station's own 4-char gridsquare, drawn with a distinct outline. */ + var ownGrid: String? = null + + override fun draw(canvas: Canvas, mapView: MapView, shadow: Boolean) { + if (shadow || !isEnabled) return + val projection = mapView.projection + val zoom = mapView.zoomLevelDouble + val cellLat = if (zoom >= GRID_ZOOM_SUB) SUB_SQUARE_LAT else FIELD_LAT + val cellLon = if (zoom >= GRID_ZOOM_SUB) SUB_SQUARE_LON else FIELD_LON + + // Visible bounding box in geographic coordinates + val north = projection.fromPixels(0, 0) + val south = projection.fromPixels(canvas.width, canvas.height) + val topLat = north.latitude.coerceIn(-90.0, 90.0) + val bottomLat = south.latitude.coerceIn(-90.0, 90.0) + val leftLon = south.longitude + val rightLon = north.longitude + + val firstRow = floor(bottomLat / cellLat).toInt() + val lastRow = ceil(topLat / cellLat).toInt() + // Longitude cell indices may exceed the [-180, 180) range when the view + // crosses the antimeridian; index arithmetic still works (PMxx at 360° == same grid) + val firstCol = floor(leftLon / cellLon).toInt() + val lastCol = ceil(rightLon / cellLon).toInt() + + // Worked-grid highlight fills (only meaningful at sub-square zoom) + if (workedGrids.isNotEmpty() && zoom >= GRID_ZOOM_SUB) { + for (row in firstRow..lastRow) { + val lat = row * cellLat + if (lat < -90.0 || lat >= 90.0) continue + val topLatCell = lat + cellLat + if (topLatCell > 90.0) continue + val yTop = projectionToY(projection, topLatCell) + val yBottom = projectionToY(projection, lat) + if (yTop == null || yBottom == null) continue + for (col in firstCol..lastCol) { + val lon = col * cellLon + val xLeft = projectionToX(projection, lon) ?: continue + val xRight = projectionToX(projection, lon + cellLon) ?: continue + if (xRight < 0f || xLeft > canvas.width) continue + if (cellLabel(lat, lon, zoom) in workedGrids) { + canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint) + } + } + } + } + + // Vertical lines (meridians) + for (col in firstCol..lastCol) { + val lon = col * cellLon + val x = projectionToX(projection, lon) + if (x == null) continue + canvas.drawLine(x, 0f, x, canvas.height.toFloat(), linePaint) + } + // Horizontal lines (parallels) + for (row in firstRow..lastRow) { + val lat = row * cellLat + if (lat <= -90.0 || lat >= 90.0) continue + val y = projectionToY(projection, lat) + if (y == null) continue + canvas.drawLine(0f, y, canvas.width.toFloat(), y, linePaint) + } + + // Labels: place at the top-left corner of each cell, only when the cell + // is large enough on screen to hold a label (avoid clutter at low zoom) + val showLabels = when { + zoom >= GRID_ZOOM_SUB -> true + zoom >= FIELD_ZOOM_LABELS -> true + else -> false + } + if (!showLabels) return + // Estimate on-screen cell height to avoid clutter at low zoom: + // project two points 1° apart in latitude and measure the pixel distance. + val y1 = projectionToY(projection, 0.0) + val y2 = projectionToY(projection, 1.0) + if (y1 == null || y2 == null) return + val pixelsPerDegree = Math.abs(y2 - y1) + if (pixelsPerDegree * cellLat < MIN_LABEL_CELL_PX) return + + val labelOffset = 8f + for (row in firstRow..lastRow) { + val lat = row * cellLat + if (lat < -90.0 || lat >= 90.0) continue + val topLatCell = lat + cellLat + if (topLatCell > 90.0) continue + val y = projectionToY(projection, topLatCell) ?: continue + if (y < -labelPaint.textSize || y > canvas.height) continue + for (col in firstCol..lastCol) { + val lon = col * cellLon + val x = projectionToX(projection, lon) ?: continue + if (x < -200f || x > canvas.width) continue + val label = cellLabel(lat, lon, zoom) + canvas.drawText(label, x + labelOffset, y + labelPaint.textSize + labelOffset, labelPaint) + } + } + } + + /** X pixel for a longitude (meridians are vertical in Web Mercator). */ + private fun projectionToX(projection: Projection, lon: Double): Float? { + val geo = org.osmdroid.util.GeoPoint(0.0, lon) + val p = projection.toPixels(geo, null) + return if (p.x in -MAX_OVERSHOOT_PX..MAX_OVERSHOOT_PX + 4096) p.x.toFloat() else null + } + + /** Y pixel for a latitude, or null when outside the viewport. */ + private fun projectionToY(projection: Projection, lat: Double): Float? { + val geo = org.osmdroid.util.GeoPoint(lat, 0.0) + val p = projection.toPixels(geo, null) + return p.y.toFloat() + } + + private fun cellLabel(lat: Double, lon: Double, zoom: Double): String { + // Maidenhead field: longitude 20° fields A-R starting at -180, + // latitude 10° fields A-R starting at -90. + val lonNorm = normalizeLon(lon) + val latNorm = lat.coerceIn(-90.0, 89.999) + val fieldLon = ((lonNorm + 180.0) / FIELD_LON).toInt() + val fieldLat = ((latNorm + 90.0) / FIELD_LAT).toInt() + val field = "${'A' + fieldLon}${'A' + fieldLat}" + if (zoom < GRID_ZOOM_SUB) return field + // Square: 2° x 1° digits + val squareLon = (((lonNorm + 180.0) % FIELD_LON) / SUB_SQUARE_LON).toInt() + val squareLat = (((latNorm + 90.0) % FIELD_LAT) / SUB_SQUARE_LAT).toInt() + return "$field$squareLon$squareLat" + } + + private fun normalizeLon(lon: Double): Double { + var l = lon % 360.0 + if (l >= 180.0) l -= 360.0 + if (l < -180.0) l += 360.0 + return l + } + + private companion object { + const val FIELD_LAT = 10.0 + const val FIELD_LON = 20.0 + const val SUB_SQUARE_LAT = 1.0 + const val SUB_SQUARE_LON = 2.0 + const val GRID_ZOOM_SUB = 5.0 + const val FIELD_ZOOM_LABELS = 3.0 + const val MIN_LABEL_CELL_PX = 48f + const val MAX_OVERSHOOT_PX = 64 + } +} diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt index 3ed234d4..518acbef 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt @@ -80,14 +80,15 @@ import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Polyline // Overlay indices -private const val OVERLAY_STATION = 0 -private const val OVERLAY_TRACK = 1 -private const val OVERLAY_FOOTPRINT = 2 -private const val OVERLAY_POSITIONS = 3 -private const val OVERLAY_TERMINATOR = 4 -private const val OVERLAY_SUN = 5 -private const val OVERLAY_MOON = 6 -private const val OVERLAY_COUNT = 7 +private const val OVERLAY_GRID = 0 +private const val OVERLAY_STATION = 1 +private const val OVERLAY_TRACK = 2 +private const val OVERLAY_FOOTPRINT = 3 +private const val OVERLAY_POSITIONS = 4 +private const val OVERLAY_TERMINATOR = 5 +private const val OVERLAY_SUN = 6 +private const val OVERLAY_MOON = 7 +private const val OVERLAY_COUNT = 8 private val minLat = MapView.getTileSystem().minLatitude private val maxLat = MapView.getTileSystem().maxLatitude @@ -159,13 +160,16 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView: ElevatedCard(modifier = Modifier.weight(1f)) { Box(contentAlignment = Alignment.BottomCenter) { AndroidView({ mapView }) { view -> - uiState.stationPosition?.let { setStationPosition(it, view) } - uiState.track?.let { setSatelliteTrack(it, view) } - uiState.footprint?.let { setFootprint(it, view) } - uiState.positions?.let { setPositions(it, view) { item -> onAction(MapAction.SelectItem(item)) } } - setTerminator(uiState.sunLatDeg, uiState.sunLonDeg, view) - setSubSolarPoint(uiState.sunLatDeg, uiState.sunLonDeg, view) - setMoonPosition(uiState.moonLatDeg, uiState.moonLonDeg, view) + setGridMode(uiState.isGridMode, uiState.workedGrids, view) + if (!uiState.isGridMode) { + uiState.stationPosition?.let { setStationPosition(it, view) } + uiState.track?.let { setSatelliteTrack(it, view) } + uiState.footprint?.let { setFootprint(it, view) } + uiState.positions?.let { setPositions(it, view) { item -> onAction(MapAction.SelectItem(item)) } } + setTerminator(uiState.sunLatDeg, uiState.sunLonDeg, view) + setSubSolarPoint(uiState.sunLatDeg, uiState.sunLonDeg, view) + setMoonPosition(uiState.moonLatDeg, uiState.moonLonDeg, view) + } view.invalidate() } uiState.mapData?.let { mapData -> @@ -261,6 +265,30 @@ private fun MapDataCards(data: MapData) { // endregion // region Map overlay helpers + +/** Toggle the grid-mode layer visibility on/off without recreating any overlay. */ +private fun setGridMode(gridMode: Boolean, workedGrids: Set, mapView: MapView) { + try { + val gridOverlay = mapView.overlays[OVERLAY_GRID] + if (gridOverlay is MaidenheadGridOverlay) { + gridOverlay.isEnabled = gridMode + gridOverlay.workedGrids = workedGrids + } else { + mapView.overlays[OVERLAY_GRID] = MaidenheadGridOverlay().apply { + isEnabled = gridMode + this.workedGrids = workedGrids + } + } + // Satellite-related layers are hidden in grid mode; the grid overlay + // itself is controlled by its own enabled flag. + for (index in OVERLAY_STATION..OVERLAY_MOON) { + mapView.overlays.getOrNull(index)?.isEnabled = !gridMode + } + } catch (e: Exception) { + println(e) + } +} + private fun setStationPosition(stationPos: GeoPos, mapView: MapView) { try { val overlay = mapView.overlays[OVERLAY_STATION] @@ -500,6 +528,7 @@ private fun rememberMapViewWithLifecycle(): MapView { overlayManager.tilesOverlay.setColorFilter(createColorFilter()) setScrollableAreaLimitLatitude(maxLat, minLat, 0) overlays.addAll(Array(OVERLAY_COUNT) { FolderOverlay() }) + overlays[OVERLAY_GRID] = MaidenheadGridOverlay() } } val lifecycleObserver = rememberMapViewLifecycleObserver(mapView) diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt index e1274618..a9293fb3 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt @@ -26,6 +26,8 @@ data class MapState( val mapData: MapData? = null, val isLightUi: Boolean = false, val isUtc: Boolean = false, + val isGridMode: Boolean = false, + val workedGrids: Set = emptySet(), val stationPosition: GeoPos? = null, val orbitalPass: OrbitalPass, val track: List>? = null, diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt index 118a089d..dba1dd67 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt @@ -74,7 +74,12 @@ class MapViewModel( init { viewModelScope.launch { settingsRepo.otherSettings.collectLatest { settings -> - _uiState.update { it.copy(isUtc = settings.stateOfUtc) } + _uiState.update { it.copy(isUtc = settings.stateOfUtc, isGridMode = settings.stateOfMapGrid) } + } + } + viewModelScope.launch { + settingsRepo.wavelogSettings.collectLatest { _ -> + _uiState.update { it.copy(workedGrids = settingsRepo.getWorkedGrids()) } } } val (selectedCatNum, _) = satelliteRepo.selectedPass.value diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt index 5afc574c..53b724a3 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsDialog.kt @@ -51,6 +51,7 @@ import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -155,6 +156,69 @@ fun LocatorDialog(qthLocator: String, dismiss: () -> Unit, save: (String) -> Uni } } +@Composable +fun WavelogDialog( + initialSettings: com.rtbishop.look4sat.core.domain.model.WavelogSettings, + workedGridsCount: Int, + isSyncing: Boolean, + message: String?, + dismiss: () -> Unit, + onSave: (com.rtbishop.look4sat.core.domain.model.WavelogSettings) -> Unit, + onSync: () -> Unit +) { + val url = rememberSaveable { mutableStateOf(initialSettings.url) } + val token = rememberSaveable { mutableStateOf(initialSettings.token) } + SharedDialog( + title = stringResource(R.string.prefs_wavelog_title), + onCancel = dismiss, + onAccept = { + onSave(com.rtbishop.look4sat.core.domain.model.WavelogSettings(url.value, token.value)) + dismiss() + } + ) { + OutlinedTextField( + value = url.value, + onValueChange = { url.value = it }, + label = { Text(text = stringResource(id = R.string.prefs_wavelog_url)) }, + placeholder = { Text(text = "http://192.168.1.10") }, + singleLine = true, + modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large), + ) + OutlinedTextField( + value = token.value, + onValueChange = { token.value = it }, + label = { Text(text = stringResource(id = R.string.prefs_wavelog_token)) }, + placeholder = { Text(text = "wl2_...") }, + singleLine = true, + modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large), + ) + Text( + text = stringResource(R.string.prefs_wavelog_hint, workedGridsCount), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = LocalSpacing.current.large) + ) + if (message != null) { + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = LocalSpacing.current.large) + ) + } + // Sync button row (inside dialog content so token must be saved first) + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large) + ) { + TextButton(onClick = onSync, enabled = !isSyncing) { + Text(text = if (isSyncing) stringResource(R.string.prefs_wavelog_syncing) + else stringResource(R.string.prefs_wavelog_sync)) + } + } + Spacer(modifier = Modifier.height(0.dp)) + } +} + @Preview(showBackground = true) @Composable private fun TransceiversDialogPreview() { diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index 1427d562..30f2efe5 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -63,6 +63,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings import com.rtbishop.look4sat.core.domain.model.OtherSettings +import com.rtbishop.look4sat.core.domain.model.RadioControlSettings +import com.rtbishop.look4sat.core.domain.model.WavelogSettings import com.rtbishop.look4sat.core.domain.predict.GeoPos import com.rtbishop.look4sat.core.domain.repository.IContainerProvider import com.rtbishop.look4sat.core.presentation.CardButton @@ -201,6 +203,17 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> onSave = { onAction(SettingsAction.UpdateRadioControl(it)) } ) } + if (dialogs.wavelog) { + WavelogDialog( + initialSettings = uiState.wavelogSettings, + workedGridsCount = uiState.workedGridsCount, + isSyncing = uiState.wavelogSyncing, + message = uiState.wavelogMessage, + dismiss = { dialogs.wavelog = false }, + onSave = { onAction(SettingsAction.UpdateWavelog(it)) }, + onSync = { onAction(SettingsAction.SyncWorkedGrids) } + ) + } // URLs for top bar val uriHandler = LocalUriHandler.current @@ -305,6 +318,13 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> onRadioControlClick = { dialogs.radioControl = true } ) } + item { + WavelogCard( + settings = uiState.wavelogSettings, + workedGridsCount = uiState.workedGridsCount, + showWavelogDialog = { dialogs.wavelog = true } + ) + } item { OtherCard(uiState.otherSettings, onAction) } item { CardCredits() } item(span = { GridItemSpan(maxLineSpan) }) { @@ -489,6 +509,7 @@ private fun OtherCardPreview() = MainTheme { stateOfUtc = false, stateOfLightTheme = false, stateOfNightMode = false, + stateOfMapGrid = false, shouldSeeWarning = false, shouldSeeWhatsNew = false ) @@ -500,7 +521,7 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni ElevatedCard( modifier = Modifier .fillMaxWidth() - .height(268.dp) + .height(312.dp) ) { Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { Text( @@ -522,6 +543,9 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni SwitchRow(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) { onAction(SettingsAction.ToggleNightMode(it)) } + SwitchRow(R.string.prefs_other_switch_map_grid, settings.stateOfMapGrid) { + onAction(SettingsAction.ToggleMapGrid(it)) + } } } } @@ -538,6 +562,44 @@ private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boole } } +@Preview(showBackground = true) +@Composable +private fun WavelogCardPreview() = MainTheme { + WavelogCard(settings = WavelogSettings("http://192.168.1.10", "wl2_demo"), workedGridsCount = 42) {} +} + +@Composable +private fun WavelogCard( + settings: WavelogSettings, + workedGridsCount: Int, + showWavelogDialog: () -> Unit +) { + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { + Text( + text = stringResource(id = R.string.prefs_wavelog_title), + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = if (settings.isConfigured) { + stringResource(R.string.prefs_wavelog_configured, workedGridsCount) + } else { + stringResource(R.string.prefs_wavelog_not_configured) + }, + style = MaterialTheme.typography.bodySmall, + maxLines = 2 + ) + Spacer(modifier = Modifier.height(4.dp)) + CardButton( + onClick = showWavelogDialog, + text = stringResource(id = R.string.prefs_wavelog_configure), + modifier = Modifier.fillMaxWidth() + ) + } + } +} + @Composable private fun formatUpdateTime(updateTime: Long): String { val timePattern = stringResource(id = R.string.prefs_updated_time) @@ -657,6 +719,7 @@ private class DialogVisibility { var network by mutableStateOf(false) var bluetooth by mutableStateOf(false) var radioControl by mutableStateOf(false) + var wavelog by mutableStateOf(false) } @Composable @@ -664,12 +727,12 @@ private fun rememberDialogVisibility(): DialogVisibility { return rememberSaveable(saver = run { androidx.compose.runtime.saveable.Saver( save = { - listOf(it.position, it.locator, it.dataSources, it.network, it.bluetooth, it.radioControl) + listOf(it.position, it.locator, it.dataSources, it.network, it.bluetooth, it.radioControl, it.wavelog) }, restore = { DialogVisibility().apply { position = it[0]; locator = it[1]; dataSources = it[2] - network = it[3]; bluetooth = it[4]; radioControl = it[5] + network = it[3]; bluetooth = it[4]; radioControl = it[5]; wavelog = it[6] } } ) diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt index b391fdce..8a04fbd9 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt @@ -22,6 +22,7 @@ import com.rtbishop.look4sat.core.domain.model.LatestRelease import com.rtbishop.look4sat.core.domain.model.OtherSettings import com.rtbishop.look4sat.core.domain.model.RCSettings import com.rtbishop.look4sat.core.domain.model.RadioControlSettings +import com.rtbishop.look4sat.core.domain.model.WavelogSettings import com.rtbishop.look4sat.core.domain.predict.GeoPos import java.io.File @@ -54,6 +55,10 @@ data class SettingsState( val radioControlSettings: RadioControlSettings, val dataSourcesSettings: DataSourcesSettings, val dataSourcesStatus: Map = emptyMap(), + val wavelogSettings: WavelogSettings = WavelogSettings(), + val workedGridsCount: Int = 0, + val wavelogSyncing: Boolean = false, + val wavelogMessage: String? = null, val updateChecker: UpdateCheckerState = UpdateCheckerState() ) @@ -77,6 +82,7 @@ sealed interface SettingsAction { data class ToggleSensor(val value: Boolean) : SettingsAction data class ToggleLightTheme(val value: Boolean) : SettingsAction data class ToggleNightMode(val value: Boolean) : SettingsAction + data class ToggleMapGrid(val value: Boolean) : SettingsAction // Remote control data class UpdateRC(val settings: RCSettings) : SettingsAction @@ -85,6 +91,10 @@ sealed interface SettingsAction { // Data sources data class UpdateDataSources(val settings: DataSourcesSettings) : SettingsAction + // Wavelog worked grids + data class UpdateWavelog(val settings: WavelogSettings) : SettingsAction + data object SyncWorkedGrids : SettingsAction + // Update checker data object CheckForUpdate : SettingsAction data object DownloadUpdate : SettingsAction diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt index bc4fc7ce..ae538799 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt @@ -26,6 +26,7 @@ import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo import com.rtbishop.look4sat.core.domain.repository.IMainContainer import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo import com.rtbishop.look4sat.core.domain.repository.IUpdateRepository +import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository import com.rtbishop.look4sat.core.domain.usecase.IShowToast import com.rtbishop.look4sat.core.domain.utility.VersionComparator import com.rtbishop.look4sat.core.presentation.R @@ -39,6 +40,7 @@ class SettingsViewModel( private val databaseRepo: IDatabaseRepo, private val settingsRepo: ISettingsRepo, private val updateRepo: IUpdateRepository, + private val wavelogRepo: IWavelogRepository, private val apkFile: File, private val showToast: IShowToast ) : ViewModel() { @@ -54,7 +56,9 @@ class SettingsViewModel( rcSettings = settingsRepo.rcSettings.value, radioControlSettings = settingsRepo.radioControlSettings.value, dataSourcesSettings = settingsRepo.dataSourcesSettings.value, - dataSourcesStatus = settingsRepo.dataSourcesStatus.value + dataSourcesStatus = settingsRepo.dataSourcesStatus.value, + wavelogSettings = settingsRepo.wavelogSettings.value, + workedGridsCount = settingsRepo.getWorkedGrids().size ) ) @@ -107,6 +111,11 @@ class SettingsViewModel( _uiState.update { it.copy(radioControlSettings = settings) } } } + viewModelScope.launch { + settingsRepo.wavelogSettings.collect { settings -> + _uiState.update { it.copy(wavelogSettings = settings) } + } + } } @@ -133,10 +142,14 @@ class SettingsViewModel( is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) } is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) } is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) } + is SettingsAction.ToggleMapGrid -> settingsRepo.updateOtherSettings { it.copy(stateOfMapGrid = action.value) } // Remote control & data sources is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings) is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings) is SettingsAction.UpdateDataSources -> settingsRepo.updateDataSourcesSettings(action.settings) + // Wavelog worked grids + is SettingsAction.UpdateWavelog -> settingsRepo.updateWavelogSettings(action.settings) + SettingsAction.SyncWorkedGrids -> syncWorkedGrids() // Update checker SettingsAction.CheckForUpdate -> checkForUpdate() SettingsAction.DownloadUpdate -> downloadUpdate() @@ -146,6 +159,30 @@ class SettingsViewModel( } } + // region Wavelog worked grids + + private fun syncWorkedGrids() { + val settings = _uiState.value.wavelogSettings + if (!settings.isConfigured) { + _uiState.update { it.copy(wavelogMessage = "Wavelog URL/token not configured") } + return + } + _uiState.update { it.copy(wavelogSyncing = true, wavelogMessage = null) } + viewModelScope.launch { + val grids = wavelogRepo.fetchWorkedGrids(settings.url, settings.token) + _uiState.update { + if (grids != null) { + settingsRepo.setWorkedGrids(grids) + it.copy(wavelogSyncing = false, workedGridsCount = grids.size, wavelogMessage = null) + } else { + it.copy(wavelogSyncing = false, wavelogMessage = "Sync failed — check URL/token/network") + } + } + } + } + + // endregion + // region Update checker private fun checkForUpdate() { @@ -267,6 +304,7 @@ class SettingsViewModel( databaseRepo = container.databaseRepo, settingsRepo = container.settingsRepo, updateRepo = container.updateRepo, + wavelogRepo = container.wavelogRepo, apkFile = File(context.cacheDir, "look4sat-update.apk"), showToast = container.provideShowToast() )