diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5ace4600..c8ac7976 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,7 @@ updates: directory: "/" schedule: interval: "weekly" + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "never" diff --git a/AGENTS.md b/AGENTS.md index 943bd754..b797b073 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,16 +8,15 @@ All assistant-specific files (`CLAUDE.md`, `.github/copilot-instructions.md`) po ## Project Overview Look4Sat is an open-source, fully offline Android satellite tracker and pass predictor. It tracks 9000+ active -satellites using TLE/OMM data from Celestrak/SatNOGS, calculates orbital positions via SGP4/SDP4 models, and displays -passes relative to the user's location. Features include polar radar visualization, SSTV image decoding, satellite -ground track mapping, and pass predictions up to 10 days ahead. No ads, no tracking, no network required after initial -data download. +satellites using Celestrak/SatNOGS orbital data, calculates positions via SGP4/SDP4, and predicts passes relative to +the user's location. Features include polar radar visualization, SSTV image decoding, and ground track mapping. No ads, +no tracking, no network required after initial data download. -## Architecture +## Architecture & Design **MVI (Model-View-Intent)** with unidirectional data flow: -- `State` data class → exposed via `StateFlow` from ViewModel -- `Action` sealed interface → user intents dispatched to ViewModel's `onAction()` +- `State` data class (named `State`) exposed via `StateFlow` from ViewModel +- `Action` sealed interface (named `Action`) dispatched to ViewModel's `onAction()` - Jetpack Compose UI observes state and recomposes reactively **Clean Architecture layers:** @@ -34,9 +33,11 @@ data download. | `feature:satellites` | Satellite list, filtering, selection | | `feature:settings` | User preferences | -- `feature:*` modules depend only on `core:domain` + `core:presentation`. Features never depend on each other. +**Feature isolation:** +- `feature:*` modules depend only on `core:domain` and `core:presentation`. +- No feature-to-feature dependencies; cross-feature communication goes through core layers. -## Build & Run +## Build & Platform ```shell # Debug build @@ -50,54 +51,55 @@ data download. ``` - **Min SDK**: 24 | **Target SDK**: 36 | **JDK**: 17 -- **Gradle**: Uses version catalog (`gradle/libs.versions.toml`) + convention plugins in `build-logic/` +- **Gradle**: Version catalog in `gradle/libs.versions.toml` + convention plugins in `build-logic/` -## Key Libraries +## Tech Stack - **Compose** (BOM 2026.05.01) + Material3 Adaptive -- **Navigation3** (type-safe, uses `@Serializable` NavKeys) -- **Room** (KSP code generation) for local satellite/TLE storage -- **OkHttp** 5.x for TLE downloads +- **Navigation3**: Type-safe navigation with `@Serializable` nav keys +- **Room** (KSP code generation) for local satellite/orbital storage +- **OkHttp** 5.x for data downloads - **OSMDroid** for map rendering -- **Kotlin Serialization** for navigation args and data parsing +- **Kotlin Serialization** for navigation args and parsing - **Coroutines** + `StateFlow` for async/reactive patterns - -## Conventions - -- **Minimal dependencies**: Avoid adding libraries when a simple manual solution exists. Fewer deps = less maintenance. -- **DI**: Manual — ViewModels use companion `factory()` methods with `IMainContainer` interface. -- **Navigation**: Type-safe Compose Navigation3 with `@Serializable` data classes as nav keys. -- **State naming**: `State` data class + `Action` sealed interface per feature. -- **No feature-to-feature deps**: All cross-feature communication goes through core layers. -- **Localization**: 7 languages (en, es, ru, si, tr, uk, zh). +- **Localization**: 7 languages (en, es, ru, si, tr, uk, zh) ## Data Formats & Migration -**TLE vs. OMM/CSV format:** +Look4Sat supports both TLE and OMM (Orbit Mean-Elements Message) CSV formats: -Look4Sat supports both TLE and OMM (Orbit Mean-Elements Message) formats for backward compatibility: +- **TLE format**: Legacy 3-line element format limited by 5-digit NORAD IDs +- **OMM/CSV format**: Successor format with ISO 8601 timestamps and larger NORAD ID support +- New 5-digit NORAD IDs are exhausted; TLE is officially deprecated and OMM/CSV is the clear default +- `DataParser.kt` supports both via `parseTLEStream()` and `parseCSVStream()` +- Downloads auto-detect format; both produce identical `OrbitalData` objects +- Existing code already supports transparent source transition without feature changes +- Refresh orbital data weekly for accurate pass prediction (orbital decay) -- **TLE format**: Traditional 3-line element format (deprecated). NORAD catalog numbers are 5-digit integers, which - are running out of space. Celestrak has signaled that TLE format will eventually be phased out. -- **OMM/CSV format**: The future standard. CSV files contain the same orbital parameters as TLE but use ISO 8601 - timestamps and support larger NORAD IDs. Celestrak and SatNOGS already provide OMM data in CSV format. +## Engineering Heuristics (Lazy = Efficient) -**Current implementation:** -- `DataParser.kt` handles both `parseTLEStream()` and `parseCSVStream()` seamlessly -- TLE data is downloaded from configured sources and stored in Room database -- When downloading satellite data, the app automatically detects format and parses accordingly -- Both formats produce identical `OrbitalData` objects, ensuring transparent format switching +- Treat "lazy" as efficient, not careless: the best code is the code never written. +- First understand the task and trace the real flow end-to-end, then climb this ladder: + 1. Does this need to be built now? (YAGNI) + 2. Does it already exist in this codebase? Reuse helpers/patterns before rewriting. + 3. Does Kotlin/Java stdlib already solve it? + 4. Does the Android/platform API already solve it? + 5. Does an already-installed dependency solve it? + 6. Can this be simpler (including one-liner simple)? + 7. Only then: write the minimum code that works. +- Prefer deletion to addition, boring over clever, and the fewest touched files. +- Avoid new abstractions, dependencies, and boilerplate unless explicitly requested. +- Manual DI only: ViewModels use companion `factory()` methods with `IMainContainer`. +- Release builds use ProGuard: avoid reflection-heavy libraries unless explicitly approved. +- When two options are similar in size, choose the edge-case-correct one. +- If you keep a deliberate simplification (for example O(n^2) scan or global lock), leave a short comment with the ceiling and upgrade path. +- For complex asks, challenge scope when appropriate: "Do you need X, or does Y already cover it?" -**Migration path:** -As NORAD catalog space becomes constrained, OMM/CSV will become the primary format. Look4Sat is already positioned -to handle this transition without code changes — existing users can continue using TLE files while new sources -transition to OMM/CSV automatically. +## Bug-Fix Policy -## Code Style - -- Prefer **short, focused functions** — single responsibility, easy to read. -- **Exceptions**: Composable functions and math-heavy algorithms (SGP4/SDP4) may be longer. -- Strict code style — no dead code, no unused imports, consistent formatting. +- Fix root cause, not just the reported symptom. +- If touching a shared function, inspect callers and prefer one shared fix over per-caller patches. +- The smallest correct diff wins only after behavior is understood. ## Roadmap @@ -105,12 +107,9 @@ transition to OMM/CSV automatically. ## Gotchas -- Orbital math lives in `core:domain/predict/` — it's dense vector math (SGP4/SDP4). Tread carefully. -- TLE/OMM data must be refreshed weekly for accurate predictions (satellite orbits decay). TLE format is legacy and - will eventually be deprecated in favor of OMM/CSV as NORAD catalog numbers approach the 5-digit limit. +- Orbital math lives in `core:domain/predict/` — dense vector math (SGP4/SDP4). Tread carefully. - SSTV decoding in `feature:radar` is experimental; image quality depends on signal strength during satellite pass. -- `build-logic/convention/` contains all shared Gradle configuration — edit there, not in individual modules. -- ProGuard is enabled for release builds — don't add reflection-based libs or any other dependencies without asking. +- `build-logic/convention/` contains shared Gradle configuration — edit there, not in individual modules. ## Copilot Working Mode: Code-Only 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 a45e3481..75d7f7df 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 @@ -135,6 +135,13 @@ class MainContainer(private val context: Context) : IMainContainer { return SensorsRepo(manager, displayManager) } + override fun providePairedBluetoothDevices(): List> = buildList { + try { + val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + manager.adapter?.bondedDevices?.forEach { add(Pair(it.name ?: "Unknown", it.address ?: "")) } + } catch (_: SecurityException) {} + } + private fun provideDatabaseRepo(): IDatabaseRepo { val dbDispatcher = Dispatchers.Default val dataParser = DataParser(dbDispatcher) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt index 6836d264..757cccd9 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt @@ -62,7 +62,7 @@ class SelectionRepo( override fun getCurrentTypes() = currentTypes.value override fun getTypesList() = Sources.satelliteDataUrls.keys.sorted().toMutableList().apply { - removeAt(0) + remove("All") } override suspend fun getEntriesFlow() = withContext(dispatcher) { 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 a46bbaeb..34e3e8fa 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 @@ -39,6 +39,7 @@ interface IMainContainer { fun provideRxRadioController(): IRadioController fun provideAudioCapture(): IAudioCapture fun provideSaveImage(): ISaveImage + fun providePairedBluetoothDevices(): List> } interface IContainerProvider { diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt index 17c91e34..4eb72da4 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt @@ -46,6 +46,7 @@ object Sources { "Classified" to "https://www.mmccants.org/tles/classfd.zip", "McCants" to "https://www.mmccants.org/tles/inttles.zip", "R4UAB" to "https://r4uab.ru/satonline.txt", + "ARISS" to "https://live.ariss.org/iss.txt", "Other" to "" // key for sats filter ) val transceiversDataUrls = mapOf( diff --git a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Components.kt b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Components.kt index cb0a3b65..d95eaec3 100644 --- a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Components.kt +++ b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Components.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator @@ -357,7 +358,7 @@ private fun DialogShell( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, - shape = MaterialTheme.shapes.medium, + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), scrimColor = Color.Black.copy(alpha = 0.64f) ) { Column( diff --git a/core/presentation/src/main/res/values-es/strings.xml b/core/presentation/src/main/res/values-es/strings.xml index 1d9bd834..bf6d324d 100644 --- a/core/presentation/src/main/res/values-es/strings.xml +++ b/core/presentation/src/main/res/values-es/strings.xml @@ -14,7 +14,7 @@ Tipo: %s - Seleccionar tipo de satélite + Seleccionar tipos Id - Nombre Vaciar Vaciar todo @@ -35,7 +35,7 @@ Ventana AOS Invertir ventana AOS DeepSpace (período >225min) - Tipo de modulación + Seleccionar modos Elevación: %.1f° Altitud: %.0f km @@ -96,11 +96,11 @@ QTH Ubicación QTH no válida Ubicación actualizada con éxito - Ajustar ubicaicón de la estación - Latitud de tu estación terrestre - Longitud de tu estación terrestre - Ajustes ubicación QTH - Ajustar posición de la estación con tu ubicación + Ubicación de la estación + Latitud de estación + Longitud de estación + Localizador QTH + Posición por localizador Datos satelitales Satélites: %s @@ -110,39 +110,52 @@ Vaciar Datos eliminados con éxito Actualización completada con éxito + No se importaron satélites. Usa un TLE/3LE (.txt) u OMM (.csv) válido. + No se importaron transceptores. Usa un SatNOGS (.json) válido. - Custom data sources - Custom TLE URL - Custom transceivers URL + Fuentes personalizadas + URL TLE + URL transceptores + URL (HTTPS) - Data output - Network + Salida de datos + Red Bluetooth + CAT - Salida de datos de red - Enable rotation output + Control de radio + Control CAT + Modelo de radio + Dirección BT TX + Dirección BT RX + Nombre radio TX + Nombre radio RX + Activar CAT + + Salida de red + Activar salida de rotación IP:Puerto Formato de datos - Enable frequency output + Activar salida de frecuencia IP:Puerto Formato de datos - Salida por Bluetooth - Enable rotation output - Id dispositivo - Formato de datos - Enable frequency output - Id dispositivo - Formato de datos + Salida Bluetooth + Activar salida de rotación + ID dispositivo + Formato + Activar salida de frecuencia + ID dispositivo + Formato Revisa los permisos de Bluetooth Revisa los permisos de red - Otros ajustes - Mostrar tiempos de pase en UTC - Habilitar auto actualización de datos - Habilitar animaciones radar - Usar sensores para rotar vista de radar - Activar filtro nocturno rojo + Otros + Mostrar hora UTC + Actualización automática + Animación de radar + Sensores para radar + Filtro nocturno rojo 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 17031bf9..85a42f47 100644 --- a/core/presentation/src/main/res/values-ru/strings.xml +++ b/core/presentation/src/main/res/values-ru/strings.xml @@ -14,7 +14,7 @@ Тип: %s - Выберите тип спутника + Выберите типы Id - Название Очистить Очистить @@ -34,7 +34,7 @@ Окно AOS Инвертировать окно AOS DeepSpace (период >225мин) - Выберите тип модуляции + Выберите режимы Элевация: %.1f° Высота: %d км @@ -95,11 +95,11 @@ QTH Неправильный локатор QTH Позиция успешно обновлена - Настройки местоположения - Введите широту наземной станции - Введите долготу наземной станции - Настройки локатора QTH - Введите позицию через локатор + Положение станции + Широта станции + Долгота станции + Локатор QTH + Позиция по локатору Данные спутников Спутников: %s @@ -109,16 +109,30 @@ Очистить Очистка прошла успешно Обновление прошло успешно + Спутники не импортированы. Нужен TLE/3LE (.txt) или OMM (.csv). + Трансиверы не импортированы. Нужен SatNOGS (.json). - Свои источники данных - Custom TLE URL - Custom Transceivers URL + Свои источники + URL TLE + URL трансиверов + URL (HTTPS) - Вывод данных + Вывод Сеть Bluetooth + CAT - Вывод сетевых данных + Управление радио + + CAT управление + Модель радио + BT адрес TX + BT адрес RX + Имя радио TX + Имя радио RX + Включить CAT + + Сетевой вывод Включить вывод ротатора IP:Порт Формат @@ -126,22 +140,22 @@ IP:Порт Формат - Вывод данных Bluetooth + Вывод Bluetooth Включить вывод ротатора - Id устройства + ID устройства Формат Включить вывод частоты - Id устройства + ID устройства Формат Нет разрешения использовать bluetooth Нет разрешения использовать сеть - Другие настройки - Показывать время по UTC - Обновлять данные автоматически - Показывать анимацию радара - Использовать сенсоры устройства - Включить красный ночной фильтр + Прочее + Время в UTC + Автообновление данных + Анимация радара + Сенсоры для радара + Красный ночной фильтр Подсветка высоты Низко < %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 44d1433b..0310dbb1 100644 --- a/core/presentation/src/main/res/values-si/strings.xml +++ b/core/presentation/src/main/res/values-si/strings.xml @@ -14,7 +14,7 @@ වර්ගය: %s - චන්ද්‍රිකා ආකාරය තෝරන්න + ආකාර තෝරන්න Id - නම පිරිසිදු ක° සියල්ල පිරිසිදු ක° @@ -35,7 +35,7 @@ AOS කවුළුව AOS කවුළුව ප්‍රතිවිරුද්ධ කරන්න DeepSpace (කාලය >225min) - මූර්ජන ආකාරය තෝරන්න + මූර්ජන තෝරන්න උත්තෝලනය: %.1f° උන්නතාශය: %d km @@ -96,11 +96,11 @@ QTH QTH locator වලංගු නොවේ පිහිටීම යාවත්කාලීනය සාර්ථකයි - පොළෙහි පිහිටීම් පසුතල - ඔබගේ පොළෙහි අක්‍ෂාංශය සකසන්න - ඔබගේ පොළෙහි දේශාංශය සකසන්න - QTH locator පසුතල - ඔබගේ locator භාවිතා කර පොළහි පිහිටීම සකසන්න + පොළ පිහිටීම + පොළ අක්‍ෂාංශය + පොළ දේශාංශය + QTH locator + locator මඟින් පොළ පිහිටීම සකසන්න චන්ද්‍රිකා දත්ත චන්ද්‍රිකා: %s @@ -110,39 +110,52 @@ පිරිසිදු ක° දත්ත ඉවත් කිරීම සාර්ථකයි යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි + චන්ද්‍රිකා ආයාත නොවීය. වලංගු TLE/3LE (.txt) හෝ OMM (.csv) ගොනුවක් තෝරන්න. + සම්ප්‍රේෂක ආයාත නොවීය. වලංගු SatNOGS (.json) ගොනුවක් තෝරන්න. - Custom data sources - Custom TLE URL - Custom transceivers URL + අභිරුචි මූලාශ්‍ර + TLE URL + Transceivers URL + URL (HTTPS) - Data output - Network + දත්ත ප්‍රතිදානය + ජාලය Bluetooth + CAT + + රේඩියෝ පාලනය + CAT පාලනය + රේඩියෝ මාදිලිය + TX BT ලිපිනය + RX BT ලිපිනය + TX රේඩියෝ නම + RX රේඩියෝ නම + CAT සබල කරන්න ජාල ප්‍රතිදානය - Enable rotation output + Rotation ප්‍රතිදානය සබල කරන්න IP:Port - දත්ත අකාරය - Enable frequency output + දත්ත ආකෘතිය + Frequency ප්‍රතිදානය සබල කරන්න IP:Port - දත්ත අකාරය + දත්ත ආකෘතිය - Bluetooth දත්ත ප්‍රතිදානය - Enable rotation output - උපාංග id - දත්ත අකාරය - Enable frequency output - උපාංග id - දත්ත අකාරය + Bluetooth ප්‍රතිදානය + Rotation ප්‍රතිදානය සබල කරන්න + උපාංග ID + දත්ත ආකෘතිය + Frequency ප්‍රතිදානය සබල කරන්න + උපාංග ID + දත්ත ආකෘතිය Bluetooth අවසර පරික්‍ෂා ක° Network අවසර පරික්‍ෂා කරන්න - වෙනත් සැකසුම් - පසුකර වේලාවන් UTC මගින් - ස්වයං දත්ත යාවත්කාලීනය - radar sweep සජීවිකරණය සබල කරන්න - Radar දර්ශනය කරකැවීමට සංවේදක භාවිතා කරන්න - රතු රාත්‍රී පෙරහන සබල කරන්න + වෙනත් + UTC වේලාව පෙන්වන්න + දත්ත ස්වයං යාවත්කාලීන + Radar sweep සජීවිකරණය + Radar සඳහා සංවේදක භාවිතා කරන්න + රතු රාත්‍රී පෙරහන උස් උද්දීපනය අඩු < %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 7e65fb7b..4a0fec46 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -15,7 +15,7 @@ Uydu türü: %s - Uydu türü seçin + Türleri seçin Id - Ad Temizle Tümünü temizle @@ -36,7 +36,7 @@ AOS aralığını tersine çevir DeepSpace (periyot >225dk) -- : -- : -- - Modulasyon türü seçin + Modları seçin %05d Yükseklik açısı: %.1f° DeepSpace @@ -127,11 +127,11 @@ QTH Geçersiz QTH konumlandırıcısı Konum başarıyla güncellendi - İstasyon konumu ayarları - Yer istasyonunuzun enlemini girin - Yer istasyonunuzun boylamını girin - QTH konumlandırıcı ayarları - İstasyon konumunu konumlandırıcı ile ayarlayın + İstasyon konumu + İstasyon enlemi + İstasyon boylamı + QTH konumlandırıcı + Konumu lokatörle ayarla Uydu verisi Uydular: %s @@ -141,28 +141,30 @@ Temizle Veriler başarıyla temizlendi Güncelleme başarıyla tamamlandı + Uydu içe aktarılmadı. Geçerli bir TLE/3LE (.txt) veya OMM (.csv) dosyası seçin. + Transceiver içe aktarılmadı. Geçerli bir SatNOGS (.json) dosyası seçin. - Özel veri kaynakları - Özel TLE URL - Özel transceiver URL + Özel kaynaklar + TLE URL + Transceiver URL URL (HTTPS) - Veri aktarımı + Veri çıkışı Ağ Bluetooth CAT Radyo Kontrolü - CAT Radyo Kontrolü + CAT kontrolü Radyo Modeli - TX Radyo BT Adresi - RX Radyo BT Adresi + TX BT adresi + RX BT adresi TX Radyo Adı RX Radyo Adı - CAT Kontrolünü Etkinleştir + CAT\'i etkinleştir - Ağ veri çıkışı + Ağ çıkışı Döndürme çıkışını etkinleştir IP:Port Biçim @@ -170,22 +172,22 @@ IP:Port Biçim - Bluetooth veri çıkışı + Bluetooth çıkışı Döndürme çıkışını etkinleştir Cihaz kimliği - Veri biçimi + Biçim Frekans çıkışını etkinleştir Cihaz kimliği - Veri biçimi + Biçim Bluetooth izninizi kontrol edin Ağ izninizi kontrol edin - Diğer ayarlar - Geçiş saatini UTC olarak göster - Otomatik veri güncellemeyi etkinleştir - 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 + Diğer + UTC saatini göster + Veriyi otomatik güncelle + Radar tarama animasyonu + Radar için sensör kullan + Kırmızı gece filtresi 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 a6754918..cc6fb62c 100644 --- a/core/presentation/src/main/res/values-uk/strings.xml +++ b/core/presentation/src/main/res/values-uk/strings.xml @@ -14,7 +14,7 @@ Тип: %s - Виберіть тип супутника + Виберіть типи Id - Назва Очистити Очистити всі @@ -35,7 +35,7 @@ Вікно AOS Інвертувати вікно AOS DeepSpace (період >225хв) - Виберіть тип модуляції + Виберіть режими Піднес.: %.1f° Висота: %.0f км @@ -96,11 +96,11 @@ QTH Некоректний локатор QTH Позицію успішно оновлено - Налаштування місцезнаходження станції - Широта місцезнаходження станції - Довгота місцезнаходження станції - Налаштування QTH локатора - Задати позицію за QTH локатором + Позиція станції + Широта станції + Довгота станції + Локатор QTH + Позиція за локатором Супутникові дані Супутників: %s @@ -110,39 +110,52 @@ Очистити Дані успішно очищено Оновлення успішне + Супутники не імпортовано. Потрібен TLE/3LE (.txt) або OMM (.csv). + Трансивери не імпортовано. Потрібен SatNOGS (.json). - Custom data sources - Custom TLE URL - Custom transceivers URL + Власні джерела + URL TLE + URL трансиверів + URL (HTTPS) - Data output - Network + Вивід даних + Мережа Bluetooth + CAT - Вивід мережевих даних - Enable rotation output + Керування радіо + CAT керування + Модель радіо + BT адреса TX + BT адреса RX + Назва радіо TX + Назва радіо RX + Увімкнути CAT + + Мережевий вивід + Увімкнути вивід повороту IP:Порт Формат даних - Enable frequency output + Увімкнути вивід частоти IP:Порт Формат даних - Вивід даних Bluetooth - Enable rotation output - Id пристрою + Вивід Bluetooth + Увімкнути вивід повороту + ID пристрою Формат даних - Enable frequency output - Id пристрою + Увімкнути вивід частоти + ID пристрою Формат даних Перевірте дозвіл Bluetooth Перевірте дозвіл мережі - Інші налаштування - Показувати час в UTC - Автоматичне оновлення даних - Показувати анімацію радара - Використовувати сенсори пристрою - Увімкнути червоний нічний фільтр + Інше + Час в UTC + Автооновлення даних + Анімація радара + Сенсори для радара + Червоний нічний фільтр Підсвічування висоти Низько < %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 50c0f6f5..d97b35aa 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -14,7 +14,7 @@ 类型:%s - 选择卫星类型 + 选择类型 Id - 名称 清空 全部清空 @@ -30,7 +30,7 @@ AOS 时间段 反选 AOS 时间段 DeepSpace(周期 >225分钟) - 选择调制类型 + 选择模式 仰角:%.1f° 高度:%d km 请确保您的过滤器设置正确,并且已选择卫星 @@ -88,11 +88,11 @@ QTH 网格 无效的 QTH 网格 位置更新成功 - 站位设置 - 输入站位的纬度 - 输入站位的经度 - QTH 网格 - 使用梅登黑德网格设置站位 + 站位 + 站位纬度 + 站位经度 + QTH 定位 + 通过 QTH 设置站位 卫星数据更新 卫星:%s @@ -102,16 +102,29 @@ 清空 数据清空成功 更新成功 + 未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。 + 未导入收发器。请选择有效的 SatNOGS (.json) 文件。 - 自定义数据源 - 自定义TLE URL - 自定义收发器URL + 自定义源 + TLE URL + 收发器 URL + URL (HTTPS) 数据输出 网络 蓝牙 + CAT - 网络数据输出 + 电台控制 + CAT 控制 + 电台型号 + TX 电台蓝牙地址 + RX 电台蓝牙地址 + TX 电台名称 + RX 电台名称 + 启用 CAT + + 网络输出 启用方位俯仰角输出 IP:端口 数据格式 @@ -119,7 +132,7 @@ IP:端口 数据格式 - 蓝牙数据输出 + 蓝牙输出 启用方位俯仰角输出 蓝牙设备 ID 数据格式 @@ -129,12 +142,12 @@ 请检查蓝牙权限 请检查网络权限 - 其他设置 - 以 UTC 时间显示 - 启用卫星数据自动更新 - 启用雷达扫描动画 - 使用传感器旋转雷达视图 - 启用红色夜间模式 + 其他 + UTC 时间 + 自动更新数据 + 雷达扫描动画 + 传感器控制雷达 + 红色夜间模式 仰角高亮 低 < %1$d° diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 530545e5..380a7f71 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ Type: %s - Select satellite type + Select types Id - Name Clear Clear all @@ -36,7 +36,7 @@ Invert AOS window DeepSpace (period >225min) -- : -- : -- - Select modulation type + Select modes %05d Elevation: %.1f° DeepSpace @@ -124,11 +124,11 @@ QTH Invalid QTH locator Position updated successfully - Station position settings - Set your ground station\'s latitude - Set your ground station\'s longitude - QTH locator settings - Set station\'s position using locator + Station position + Station latitude + Station longitude + QTH locator + Set position via locator Satellite data Satellites: %s @@ -141,9 +141,9 @@ No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file. No transceivers imported. Select a valid SatNOGS (.json) file. - Custom data sources - Custom TLE URL - Custom transceivers URL + Custom sources + TLE URL + Transceivers URL URL (HTTPS) Data output @@ -153,15 +153,15 @@ Radio Control - CAT Radio Control + CAT control Radio Model - TX Radio BT Address - RX Radio BT Address + TX BT address + RX BT address TX Radio Name RX Radio Name - Enable CAT Control + Enable CAT - Network data output + Network output Enable rotation output IP:Port Format @@ -169,22 +169,22 @@ IP:Port Format - Bluetooth data output + Bluetooth output Enable rotation output - Device id - Data format + Device ID + Format Enable frequency output - Device id - Data format + Device ID + Format Check your bluetooth permission Check your network permission - Other settings - Show pass time in UTC - Enable automatic data update - Enable radar sweep animation - Use sensors to rotate radar view - Enable red night mode filter + Other + Show UTC time + Auto-update data + Radar sweep animation + Use sensors for radar + Red night filter Elevation highlight Low < %1$d° diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt index 6b9752e2..10a75a46 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -147,6 +148,7 @@ private fun PassesScreen( color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(horizontal = padding) ) + Spacer(modifier = Modifier.height(0.dp)) } } ScreenColumn( diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt index 41bab435..c47f20e1 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt @@ -369,10 +369,11 @@ private fun ExpandedRadioControl( ) Spacer(modifier = Modifier.height(4.dp)) FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + maxItemsInEachRow = 5, modifier = Modifier.fillMaxWidth() ) { - val chipModifier = Modifier.width(64.dp) + val chipModifier = Modifier.weight(1f) FilterChip( selected = radioControl.ctcssTone == null, onClick = { onAction(RadarAction.SetCtcssTone(null)) }, diff --git a/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt b/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt index 78e6bb5a..c487e3b4 100644 --- a/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt +++ b/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt @@ -108,6 +108,7 @@ private fun SatellitesScreen( color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(horizontal = padding) ) + Spacer(modifier = Modifier.height(0.dp)) } } 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 c0897b4d..42ca9169 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 @@ -17,12 +17,9 @@ */ package com.rtbishop.look4sat.feature.settings -import android.bluetooth.BluetoothManager -import android.content.Context import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -30,18 +27,19 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -66,18 +64,27 @@ fun PositionDialog(lat: Double, lon: Double, dismiss: () -> Unit, save: (Double, val titleText = stringResource(id = R.string.prefs_station_title) val onAccept = { saveValues(latValue.value, lonValue.value, save).also { dismiss() } } SharedDialog(title = titleText, onCancel = dismiss, onAccept = onAccept) { - OutlinedTextField( - value = latValue.value, - onValueChange = { latValue.value = it }, - label = { Text(text = stringResource(id = R.string.prefs_station_lat_text)) }, - modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large), - ) - OutlinedTextField( - value = lonValue.value, - onValueChange = { lonValue.value = it }, - label = { Text(text = stringResource(id = R.string.prefs_station_lon_text)) }, - modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large), - ) + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = LocalSpacing.current.large) + ) { + OutlinedTextField( + value = latValue.value, + onValueChange = { latValue.value = it }, + label = { Text(text = stringResource(id = R.string.prefs_station_lat_text)) }, + singleLine = true, + modifier = Modifier.weight(1f), + ) + OutlinedTextField( + value = lonValue.value, + onValueChange = { lonValue.value = it }, + label = { Text(text = stringResource(id = R.string.prefs_station_lon_text)) }, + singleLine = true, + modifier = Modifier.weight(1f), + ) + } Spacer(modifier = Modifier.height(0.dp)) } } @@ -100,12 +107,18 @@ private fun LocatorDialogPreview() { fun LocatorDialog(qthLocator: String, dismiss: () -> Unit, save: (String) -> Unit) { val locator = rememberSaveable { mutableStateOf(qthLocator) } val onAccept = { save(locator.value).also { dismiss() } } - SharedDialog(title = stringResource(R.string.prefs_locator_title), onCancel = dismiss, onAccept = onAccept) { + SharedDialog( + title = stringResource(R.string.prefs_locator_title), + onCancel = dismiss, + onAccept = onAccept + ) { OutlinedTextField( value = locator.value, onValueChange = { locator.value = it }, label = { Text(text = stringResource(id = R.string.prefs_locator_text)) }, - modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = LocalSpacing.current.large), ) Spacer(modifier = Modifier.height(0.dp)) } @@ -147,7 +160,12 @@ fun DataSourcesDialog( val urlTle = rememberSaveable { mutableStateOf(tleUrl) } val urlTransceivers = rememberSaveable { mutableStateOf(transceiversUrl) } val onAccept = { - onSave(isEnabledCustomTle.value, isEnabledCustomTransceivers.value, urlTle.value, urlTransceivers.value) + onSave( + isEnabledCustomTle.value, + isEnabledCustomTransceivers.value, + urlTle.value, + urlTransceivers.value + ) onDismiss() } val onCancel = { onDismiss() } @@ -377,11 +395,15 @@ fun BluetoothOutputDialog( ) { val padding = LocalSpacing.current.large val rotatorState = rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorState) } - val rotatorAddress = rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorAddress) } + val rotatorAddress = + rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorAddress) } val rotatorFormat = rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorFormat) } - val frequencyState = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyState) } - val frequencyAddress = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyAddress) } - val frequencyFormat = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyFormat) } + val frequencyState = + rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyState) } + val frequencyAddress = + rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyAddress) } + val frequencyFormat = + rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyFormat) } val onAccept = { onSave( rotatorState.value, rotatorAddress.value, rotatorFormat.value, @@ -470,23 +492,22 @@ private fun OutputChannelSection( } } -@OptIn(ExperimentalLayoutApi::class) @Composable fun RadioControlDialog( initialSettings: RadioControlSettings, + pairedBluetoothDevices: List>, onDismiss: () -> Unit, onSave: (RadioControlSettings) -> Unit ) { - val context = androidx.compose.ui.platform.LocalContext.current - val padding = LocalSpacing.current.large - val enabled = rememberSaveable { mutableStateOf(initialSettings.enabled) } + val padding = LocalSpacing.current.large + val enabled = rememberSaveable { mutableStateOf(initialSettings.enabled) } val radioModel = rememberSaveable { mutableStateOf(initialSettings.radioModel) } - val splitMode = rememberSaveable { mutableStateOf(initialSettings.splitMode) } - val txAddress = rememberSaveable { mutableStateOf(initialSettings.txRadioAddress) } - val rxAddress = rememberSaveable { mutableStateOf(initialSettings.rxRadioAddress) } - val txName = rememberSaveable { mutableStateOf(initialSettings.txRadioName) } - val rxName = rememberSaveable { mutableStateOf(initialSettings.rxRadioName) } - val baudRate = rememberSaveable { mutableIntStateOf(initialSettings.baudRate) } + val splitMode = rememberSaveable { mutableStateOf(initialSettings.splitMode) } + val txAddress = rememberSaveable { mutableStateOf(initialSettings.txRadioAddress) } + val rxAddress = rememberSaveable { mutableStateOf(initialSettings.rxRadioAddress) } + val txName = rememberSaveable { mutableStateOf(initialSettings.txRadioName) } + val rxName = rememberSaveable { mutableStateOf(initialSettings.rxRadioName) } + val baudRate = rememberSaveable { mutableIntStateOf(initialSettings.baudRate) } val selectingFor = rememberSaveable { mutableStateOf("") } // "tx", "rx", or "" val isIcom = radioModel.value == RadioControlSettings.MODEL_ICOM_IC705 @@ -496,157 +517,157 @@ fun RadioControlDialog( if (!isIcom && splitMode.value) splitMode.value = false val baudRates = if (isIcom) RadioControlSettings.BAUD_RATES_ICOM - else RadioControlSettings.BAUD_RATES_YAESU + else RadioControlSettings.BAUD_RATES_YAESU // If current baud rate is not in the new list, default to the first available if (baudRate.intValue !in baudRates) baudRate.intValue = baudRates.first() - val pairedDevices: List> = remember { - buildList { - try { - val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - manager.adapter?.bondedDevices?.forEach { - add(Pair(it.name ?: "Unknown", it.address ?: "")) - } - } catch (_: SecurityException) { } - } - } - val onAccept = { onSave( RadioControlSettings( - enabled = enabled.value, - radioModel = radioModel.value, + enabled = enabled.value, + radioModel = radioModel.value, txRadioAddress = txAddress.value, rxRadioAddress = if (isSingleRadio) "" else rxAddress.value, - txRadioName = txName.value, - rxRadioName = if (isSingleRadio) "" else rxName.value, - baudRate = baudRate.intValue, - splitMode = splitMode.value + txRadioName = txName.value, + rxRadioName = if (isSingleRadio) "" else rxName.value, + baudRate = baudRate.intValue, + splitMode = splitMode.value ) ) onDismiss() } SharedDialog( - title = stringResource(R.string.rc_settings_title), + title = stringResource(R.string.rc_settings_title), onCancel = onDismiss, onAccept = onAccept ) { Column(modifier = Modifier.padding(horizontal = padding)) { - // Enable switch Row( horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.rc_enable_switch)) Switch(checked = enabled.value, onCheckedChange = { enabled.value = it }) } Spacer(modifier = Modifier.height(6.dp)) - // Radio model — FlowRow so chips wrap on small screens Text( - text = stringResource(R.string.rc_radio_model), + text = stringResource(R.string.rc_radio_model), fontWeight = FontWeight.Medium, - color = androidx.compose.material3.MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary ) - FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.fillMaxWidth() + ) { RadioControlSettings.SUPPORTED_RADIOS.forEach { model -> FilterChip( selected = radioModel.value == model, - onClick = { radioModel.value = model }, - label = { Text(model, fontSize = 12.sp) }, - enabled = enabled.value + onClick = { radioModel.value = model }, + label = { + Text( + text = compactRadioModelLabel(model), + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + }, + enabled = enabled.value, + modifier = Modifier.weight(1f) ) } } Spacer(modifier = Modifier.height(6.dp)) - // IC-705 split-mode toggle (only shown for IC-705) - if (isIcom) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Column(modifier = Modifier.weight(1f)) { - Text("Split mode (single radio)", fontWeight = FontWeight.Medium) - Text( - text = "Use VFO-A/B split on one IC-705 instead of two radios", - fontSize = 12.sp, - color = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Switch( - checked = splitMode.value, - onCheckedChange = { splitMode.value = it }, - enabled = enabled.value - ) - } - Spacer(modifier = Modifier.height(6.dp)) + val isSplitModeAvailable = enabled.value && isIcom + val splitModeLabelColor = if (isSplitModeAvailable) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.56f) } - - // TX Radio (always shown; in split mode this is the single IC-705) - val txLabel = if (isSingleRadio) "Radio (IC-705)" else "TX Radio (Uplink)" - Text(txLabel, fontWeight = FontWeight.Medium) - if (txAddress.value.isNotBlank()) { - Text("${txName.value} — ${txAddress.value}", fontSize = 13.sp) - } - CardButton( - onClick = { selectingFor.value = "tx" }, - text = if (isSingleRadio) "Select Device" else "Select TX Device", + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() - ) + ) { + Text( + text = "Split mode (IC-705 only)", + fontWeight = FontWeight.Medium, + color = splitModeLabelColor + ) + Switch( + checked = splitMode.value, + onCheckedChange = { splitMode.value = it }, + enabled = isSplitModeAvailable + ) + } Spacer(modifier = Modifier.height(6.dp)) - // RX Radio (hidden in split mode — the same radio handles both) - if (!isSingleRadio) { - Text("RX Radio (Downlink)", fontWeight = FontWeight.Medium) - if (rxAddress.value.isNotBlank()) { - Text("${rxName.value} — ${rxAddress.value}", fontSize = 13.sp) - } + Text("Radio devices", fontWeight = FontWeight.Medium) + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.fillMaxWidth() + ) { CardButton( - onClick = { selectingFor.value = "rx" }, - text = "Select RX Device", - modifier = Modifier.fillMaxWidth() + onClick = { selectingFor.value = "tx" }, + text = if (isSingleRadio) "Select Radio" else "Select TX", + modifier = Modifier.weight(1f) + ) + CardButton( + onClick = { if (!isSingleRadio) selectingFor.value = "rx" }, + text = if (isSingleRadio) "RX = TX" else "Select RX", + modifier = Modifier.weight(1f) ) - Spacer(modifier = Modifier.height(6.dp)) } + if (txAddress.value.isNotBlank()) { + Text("TX: ${txName.value} — ${txAddress.value}", fontSize = 13.sp) + } + if (isSingleRadio && txAddress.value.isNotBlank()) { + Text("RX: same as TX", fontSize = 13.sp) + } else if (rxAddress.value.isNotBlank()) { + Text("RX: ${rxName.value} — ${rxAddress.value}", fontSize = 13.sp) + } + Spacer(modifier = Modifier.height(6.dp)) - // Paired device picker (inline, shown while selecting) if (selectingFor.value.isNotBlank()) { Text( - text = "Paired Bluetooth Devices:", + text = "Paired Bluetooth Devices:", fontWeight = FontWeight.Medium, - color = androidx.compose.material3.MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.height(2.dp)) - if (pairedDevices.isEmpty()) { + if (pairedBluetoothDevices.isEmpty()) { Text( "No paired devices found. Pair your BT adapter in Android Bluetooth settings first.", fontSize = 13.sp ) } else { - pairedDevices.forEach { (name, address) -> + pairedBluetoothDevices.forEach { (name, address) -> androidx.compose.material3.Surface( modifier = Modifier .fillMaxWidth() .clickable { if (selectingFor.value == "tx") { txAddress.value = address - txName.value = name + txName.value = name + if (isSingleRadio) { + rxAddress.value = address + rxName.value = name + } } else { rxAddress.value = address - rxName.value = name + rxName.value = name } selectingFor.value = "" } .padding(vertical = 4.dp) ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text(name, modifier = Modifier.weight(1f)) @@ -658,15 +679,26 @@ fun RadioControlDialog( Spacer(modifier = Modifier.height(6.dp)) } - // Baud rate — FlowRow so all chips fit on narrow screens Text("Baud Rate:", fontWeight = FontWeight.Medium) - FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(2.dp), + maxItemsInEachRow = 6, + modifier = Modifier.fillMaxWidth() + ) { baudRates.forEach { rate -> FilterChip( selected = rate == baudRate.intValue, - onClick = { baudRate.intValue = rate }, - label = { Text(rate.toString(), fontSize = 12.sp) }, - enabled = enabled.value + onClick = { baudRate.intValue = rate }, + label = { + Text( + text = baudLabel(rate), + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + }, + enabled = enabled.value, + modifier = Modifier.weight(1f) ) } } @@ -674,3 +706,20 @@ fun RadioControlDialog( } } } + +private fun compactRadioModelLabel(model: String): String = when (model) { + RadioControlSettings.MODEL_YAESU_FT817 -> "FT-817/818" + RadioControlSettings.MODEL_YAESU_FT857 -> "FT-857/897" + RadioControlSettings.MODEL_ICOM_IC705 -> "IC-705" + else -> model +} + +private fun baudLabel(rate: Int): String = when (rate) { + 4800 -> "4k" + 9600 -> "9k" + 19200 -> "19k" + 38400 -> "38k" + 57600 -> "57k" + 115200 -> "115k" + else -> rate.toString() +} 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 e4c9e2b0..9170aa04 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 @@ -190,6 +190,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> if (dialogs.radioControl) { RadioControlDialog( initialSettings = uiState.radioControlSettings, + pairedBluetoothDevices = uiState.pairedBluetoothDevices, onDismiss = { dialogs.radioControl = false }, onSave = { onAction(SettingsAction.UpdateRadioControl(it)) } ) 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 928cf60b..d5c66f5e 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 @@ -41,7 +41,8 @@ data class SettingsState( val otherSettings: OtherSettings, val rcSettings: RCSettings, val radioControlSettings: RadioControlSettings, - val dataSourcesSettings: DataSourcesSettings + val dataSourcesSettings: DataSourcesSettings, + val pairedBluetoothDevices: List> = emptyList() ) sealed interface 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 f43e91f6..33a5a5b1 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 @@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class SettingsViewModel( + private val container: IMainContainer, private val databaseRepo: IDatabaseRepo, private val settingsRepo: ISettingsRepo, private val showToast: IShowToast @@ -47,7 +48,8 @@ class SettingsViewModel( otherSettings = settingsRepo.otherSettings.value, rcSettings = settingsRepo.rcSettings.value, radioControlSettings = settingsRepo.radioControlSettings.value, - dataSourcesSettings = settingsRepo.dataSourcesSettings.value + dataSourcesSettings = settingsRepo.dataSourcesSettings.value, + pairedBluetoothDevices = container.providePairedBluetoothDevices() ) ) @@ -204,6 +206,7 @@ class SettingsViewModel( fun factory(container: IMainContainer) = viewModelFactory { initializer { SettingsViewModel( + container = container, databaseRepo = container.databaseRepo, settingsRepo = container.settingsRepo, showToast = container.provideShowToast()