Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
691c5f5bb1 | ||
|
|
8fbfcb3712 | ||
|
|
602b1553a2 | ||
|
|
bd0881fb4b | ||
|
|
a9dd3e6e55 | ||
|
|
905995ab44 | ||
|
|
f9806d7f7f | ||
|
|
2d3adfc6a6 |
No files matched your search
@@ -4,3 +4,7 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "bundler"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "never"
|
||||||
@@ -4,15 +4,10 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v**
|
- v**
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag_name:
|
|
||||||
description: 'Release tag name (e.g. v4.4.3)'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TAG_NAME: ${{ github.event.inputs.tag_name || github.ref_name }}
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -21,40 +16,66 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
- name: Assemble APK
|
- name: Assemble Artifacts
|
||||||
run: ./gradlew assembleRelease
|
run: ./gradlew assembleRelease bundleRelease
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
|
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
|
||||||
APK=$(find app/build/outputs/apk/release -name "*.apk" | head -1)
|
APK=$(find app/build/outputs/apk/release -name "*.apk" | head -1)
|
||||||
VERSION=${TAG_NAME#v}
|
|
||||||
SIGNED_APK="app/build/outputs/apk/release/Look4Sat-Pro-${VERSION}.apk"
|
|
||||||
BUILD_TOOLS=$(ls -d ${ANDROID_HOME}/build-tools/*/ | sort -V | tail -1)
|
BUILD_TOOLS=$(ls -d ${ANDROID_HOME}/build-tools/*/ | sort -V | tail -1)
|
||||||
${BUILD_TOOLS}apksigner sign \
|
${BUILD_TOOLS}apksigner sign \
|
||||||
--ks keystore.jks \
|
--ks keystore.jks \
|
||||||
--ks-pass pass:${{ secrets.KEY_STORE_PASSWORD }} \
|
--ks-pass pass:${{ secrets.KEY_STORE_PASSWORD }} \
|
||||||
--ks-key-alias ${{ secrets.KEY_ALIAS }} \
|
--ks-key-alias ${{ secrets.KEY_ALIAS }} \
|
||||||
--key-pass pass:${{ secrets.KEY_PASSWORD }} \
|
--key-pass pass:${{ secrets.KEY_PASSWORD }} \
|
||||||
--out "$SIGNED_APK" \
|
--out app/build/outputs/apk/release/look4sat.apk \
|
||||||
"$APK"
|
"$APK"
|
||||||
rm keystore.jks
|
rm keystore.jks
|
||||||
echo "SIGNED_APK=$SIGNED_APK" >> "$GITHUB_ENV"
|
|
||||||
|
- name: Sign Bundle
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
|
||||||
|
AAB=$(find app/build/outputs/bundle/release -name "*.aab" | head -1)
|
||||||
|
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
|
||||||
|
-keystore keystore.jks \
|
||||||
|
-storepass ${{ secrets.KEY_STORE_PASSWORD }} \
|
||||||
|
-keypass ${{ secrets.KEY_PASSWORD }} \
|
||||||
|
"$AAB" ${{ secrets.KEY_ALIAS }}
|
||||||
|
rm keystore.jks
|
||||||
|
|
||||||
|
- name: Setup Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: '3.4'
|
||||||
|
|
||||||
|
- name: Deploy Bundle to Google Play
|
||||||
|
run: |
|
||||||
|
gem install multi_json
|
||||||
|
gem install fastlane --no-document
|
||||||
|
AAB=$(find app/build/outputs/bundle/release -name "*.aab" | head -1)
|
||||||
|
echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||||
|
fastlane supply \
|
||||||
|
--aab "$AAB" \
|
||||||
|
--json_key service_account.json \
|
||||||
|
--package_name com.rtbishop.look4sat \
|
||||||
|
--track production \
|
||||||
|
--skip_upload_images true \
|
||||||
|
--skip_upload_screenshots true \
|
||||||
|
rm service_account.json
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
run: |
|
||||||
gh release create $TAG_NAME --title=$TAG_NAME --generate-notes
|
gh release create $TAG_NAME --title=$TAG_NAME --generate-notes
|
||||||
gh release upload $TAG_NAME "$SIGNED_APK"
|
gh release upload $TAG_NAME app/build/outputs/apk/release/look4sat.apk
|
||||||
+2
-4
@@ -1,9 +1,6 @@
|
|||||||
# Built application files
|
# Built application files
|
||||||
*.ap_
|
*.ap_
|
||||||
|
|
||||||
# Hermes AI plans and metadata
|
|
||||||
.hermes/
|
|
||||||
|
|
||||||
# Files for the ART/Dalvik VM
|
# Files for the ART/Dalvik VM
|
||||||
*.dex
|
*.dex
|
||||||
|
|
||||||
@@ -39,7 +36,8 @@ captures/
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# Keystore files
|
# Keystore files
|
||||||
*.jks
|
# Uncomment the following line if you do not want to check your keystore files in.
|
||||||
|
#*.jks
|
||||||
/*.properties
|
/*.properties
|
||||||
/keystore.properties
|
/keystore.properties
|
||||||
/app/keystore.jks
|
/app/keystore.jks
|
||||||
|
|||||||
@@ -8,16 +8,15 @@ All assistant-specific files (`CLAUDE.md`, `.github/copilot-instructions.md`) po
|
|||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Look4Sat is an open-source, fully offline Android satellite tracker and pass predictor. It tracks 9000+ active
|
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
|
satellites using Celestrak/SatNOGS orbital data, calculates positions via SGP4/SDP4, and predicts passes relative to
|
||||||
passes relative to the user's location. Features include polar radar visualization, SSTV image decoding, satellite
|
the user's location. Features include polar radar visualization, SSTV image decoding, and ground track mapping. No ads,
|
||||||
ground track mapping, and pass predictions up to 10 days ahead. No ads, no tracking, no network required after initial
|
no tracking, no network required after initial data download.
|
||||||
data download.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture & Design
|
||||||
|
|
||||||
**MVI (Model-View-Intent)** with unidirectional data flow:
|
**MVI (Model-View-Intent)** with unidirectional data flow:
|
||||||
- `State` data class → exposed via `StateFlow` from ViewModel
|
- `State` data class (named `<Feature>State`) exposed via `StateFlow` from ViewModel
|
||||||
- `Action` sealed interface → user intents dispatched to ViewModel's `onAction()`
|
- `Action` sealed interface (named `<Feature>Action`) dispatched to ViewModel's `onAction()`
|
||||||
- Jetpack Compose UI observes state and recomposes reactively
|
- Jetpack Compose UI observes state and recomposes reactively
|
||||||
|
|
||||||
**Clean Architecture layers:**
|
**Clean Architecture layers:**
|
||||||
@@ -34,9 +33,11 @@ data download.
|
|||||||
| `feature:satellites` | Satellite list, filtering, selection |
|
| `feature:satellites` | Satellite list, filtering, selection |
|
||||||
| `feature:settings` | User preferences |
|
| `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
|
```shell
|
||||||
# Debug build
|
# Debug build
|
||||||
@@ -50,54 +51,55 @@ data download.
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **Min SDK**: 24 | **Target SDK**: 36 | **JDK**: 17
|
- **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
|
- **Compose** (BOM 2026.05.01) + Material3 Adaptive
|
||||||
- **Navigation3** (type-safe, uses `@Serializable` NavKeys)
|
- **Navigation3**: Type-safe navigation with `@Serializable` nav keys
|
||||||
- **Room** (KSP code generation) for local satellite/TLE storage
|
- **Room** (KSP code generation) for local satellite/orbital storage
|
||||||
- **OkHttp** 5.x for TLE downloads
|
- **OkHttp** 5.x for data downloads
|
||||||
- **OSMDroid** for map rendering
|
- **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
|
- **Coroutines** + `StateFlow` for async/reactive patterns
|
||||||
|
- **Localization**: 7 languages (en, es, ru, si, tr, uk, zh)
|
||||||
## 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**: `<Feature>State` data class + `<Feature>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).
|
|
||||||
|
|
||||||
## Data Formats & Migration
|
## 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
|
## Engineering Heuristics (Lazy = Efficient)
|
||||||
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.
|
|
||||||
|
|
||||||
**Current implementation:**
|
- Treat "lazy" as efficient, not careless: the best code is the code never written.
|
||||||
- `DataParser.kt` handles both `parseTLEStream()` and `parseCSVStream()` seamlessly
|
- First understand the task and trace the real flow end-to-end, then climb this ladder:
|
||||||
- TLE data is downloaded from configured sources and stored in Room database
|
1. Does this need to be built now? (YAGNI)
|
||||||
- When downloading satellite data, the app automatically detects format and parses accordingly
|
2. Does it already exist in this codebase? Reuse helpers/patterns before rewriting.
|
||||||
- Both formats produce identical `OrbitalData` objects, ensuring transparent format switching
|
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:**
|
## Bug-Fix Policy
|
||||||
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.
|
|
||||||
|
|
||||||
## Code Style
|
- Fix root cause, not just the reported symptom.
|
||||||
|
- If touching a shared function, inspect callers and prefer one shared fix over per-caller patches.
|
||||||
- Prefer **short, focused functions** — single responsibility, easy to read.
|
- The smallest correct diff wins only after behavior is understood.
|
||||||
- **Exceptions**: Composable functions and math-heavy algorithms (SGP4/SDP4) may be longer.
|
|
||||||
- Strict code style — no dead code, no unused imports, consistent formatting.
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
@@ -105,12 +107,9 @@ transition to OMM/CSV automatically.
|
|||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- Orbital math lives in `core:domain/predict/` — it's dense vector math (SGP4/SDP4). Tread carefully.
|
- Orbital math lives in `core:domain/predict/` — 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.
|
|
||||||
- SSTV decoding in `feature:radar` is experimental; image quality depends on signal strength during satellite pass.
|
- 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.
|
- `build-logic/convention/` contains 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.
|
|
||||||
|
|
||||||
## Copilot Working Mode: Code-Only
|
## Copilot Working Mode: Code-Only
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,3 @@
|
|||||||
import java.util.Properties
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.convention.applicationPlugin)
|
alias(libs.plugins.convention.applicationPlugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load signing config from keystore.properties (gitignored, never commit credentials)
|
|
||||||
val keystoreProperties = Properties().apply {
|
|
||||||
val propsFile = rootProject.file("keystore.properties")
|
|
||||||
if (propsFile.exists()) propsFile.inputStream().use { load(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
// CW 解码 native 库仅 armeabi-v7a(照搬 Morse Expert 1.15): 全 ABI 打包会在
|
|
||||||
// arm64 设备 loadLibrary 失败, 强制 32 位兼容(用户设备为 32 位软件)
|
|
||||||
defaultConfig {
|
|
||||||
ndk {
|
|
||||||
abiFilters += listOf("armeabi-v7a")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
androidResources {
|
|
||||||
// 显式保留全部语言(防 shrinkResources 丢弃 in/id 印尼语配置); AGP 9 用 localeFilters
|
|
||||||
localeFilters += listOf(
|
|
||||||
"en", "zh", "tr", "in", "id", "es", "ru", "si", "uk"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
signingConfigs {
|
|
||||||
if (keystoreProperties["storeFile"] != null) {
|
|
||||||
create("release") {
|
|
||||||
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
|
|
||||||
storePassword = keystoreProperties["storePassword"] as String
|
|
||||||
keyAlias = keystoreProperties["keyAlias"] as String
|
|
||||||
keyPassword = keystoreProperties["keyPassword"] as String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
signingConfig = signingConfigs.findByName("release")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,10 +14,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<application
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<application
|
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -28,7 +25,6 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.Look4Sat.SplashScreen">
|
android:theme="@style/Theme.Look4Sat.SplashScreen">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -46,11 +42,7 @@
|
|||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
|
android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
|
||||||
android:value="com.rtbishop.look4sat.bg7nta" />
|
android:value="com.rtbishop.look4sat" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="com.rtbishop.look4sat.app.AprsForegroundService"
|
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="dataSync" />
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
package com.rtbishop.look4sat.app
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.widget.Toast
|
|
||||||
import android.content.pm.ServiceInfo
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import com.rtbishop.look4sat.MainApplication
|
|
||||||
import com.rtbishop.look4sat.core.presentation.R
|
|
||||||
import com.rtbishop.look4sat.core.data.aprs.AprsConfig
|
|
||||||
import com.rtbishop.look4sat.core.data.aprs.AprsStore
|
|
||||||
import com.rtbishop.look4sat.core.data.aprs.AprsReporter
|
|
||||||
import com.rtbishop.look4sat.core.data.aprs.AprsState
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APRS 前台服务:系统通知保活,切换页面不断线。
|
|
||||||
* START_STICKY:被系统杀掉后自动重启(APRSdroid 同款策略)。
|
|
||||||
*/
|
|
||||||
class AprsForegroundService : Service() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ACTION_START = AprsStore.ACTION_START
|
|
||||||
const val ACTION_STOP = AprsStore.ACTION_STOP
|
|
||||||
const val ACTION_REPORT_NOW = AprsStore.ACTION_REPORT_NOW
|
|
||||||
const val CHANNEL_ID = "aprs_service"
|
|
||||||
const val NOTIF_ID = 101
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
private var reporter: AprsReporter? = null
|
|
||||||
private var lastState: AprsState = AprsState.Idle
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
createChannel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
when (intent?.action) {
|
|
||||||
ACTION_STOP -> stopReporting()
|
|
||||||
ACTION_REPORT_NOW -> {
|
|
||||||
if (reporter == null) {
|
|
||||||
// 服务没在跑:先启动(未配置会 Toast 提示)
|
|
||||||
startReporting()
|
|
||||||
}
|
|
||||||
reporter?.reportNow()
|
|
||||||
}
|
|
||||||
else -> startReporting()
|
|
||||||
}
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startReporting() {
|
|
||||||
val cfg = AprsStore.loadConfig(this)
|
|
||||||
if (!cfg.enabled || cfg.callsign.isBlank()) {
|
|
||||||
runCatching {
|
|
||||||
Toast.makeText(this, getString(R.string.aprs_toast_not_configured), Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
stopSelf()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
startForegroundWithNotification(cfg)
|
|
||||||
val rep = AprsReporter(
|
|
||||||
configProvider = { AprsStore.loadConfig(this) },
|
|
||||||
positionProvider = { stationPosition() },
|
|
||||||
onState = { lastState = it },
|
|
||||||
onReport = { report ->
|
|
||||||
AprsStore.saveLastReport(this, report.ok, report.detail)
|
|
||||||
updateNotification(cfg)
|
|
||||||
// 上报结果必达:成功 Toast 短显,失败 Toast 长显+原因
|
|
||||||
val msg = if (report.ok) {
|
|
||||||
getString(R.string.aprs_toast_ok)
|
|
||||||
} else {
|
|
||||||
getString(R.string.aprs_toast_fail, report.detail)
|
|
||||||
}
|
|
||||||
runCatching {
|
|
||||||
Toast.makeText(this, msg,
|
|
||||||
if (report.ok) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
reporter = rep
|
|
||||||
rep.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopReporting() {
|
|
||||||
reporter?.stop()
|
|
||||||
reporter = null
|
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startForegroundWithNotification(cfg: AprsConfig) {
|
|
||||||
try {
|
|
||||||
val notif = buildNotification(cfg)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
|
||||||
} else {
|
|
||||||
startForeground(NOTIF_ID, notif)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// 厂商 ROM / 旧系统兼容兜底:启动前台失败只停服务,不崩进程
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildNotification(cfg: AprsConfig): Notification {
|
|
||||||
val pi = PendingIntent.getActivity(
|
|
||||||
this, 0, packageManager.getLaunchIntentForPackage(packageName),
|
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
val stopPi = PendingIntent.getService(
|
|
||||||
this, 1, Intent(this, AprsForegroundService::class.java).setAction(ACTION_STOP),
|
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
val stateText = when (lastState) {
|
|
||||||
AprsState.Connected -> getString(R.string.aprs_notif_connected)
|
|
||||||
AprsState.Error -> getString(R.string.aprs_notif_error)
|
|
||||||
else -> getString(R.string.aprs_notif_running)
|
|
||||||
}
|
|
||||||
return Notification.Builder(this, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ic_radios)
|
|
||||||
.setContentTitle(getString(R.string.aprs_notif_title, cfg.callsign))
|
|
||||||
.setContentText(stateText)
|
|
||||||
.setContentIntent(pi)
|
|
||||||
.setOngoing(true)
|
|
||||||
.addAction(0, getString(R.string.aprs_notif_stop), stopPi)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateNotification(cfg: AprsConfig) {
|
|
||||||
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
nm.notify(NOTIF_ID, buildNotification(cfg))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 上报位置:优先用设置里的站位(用户拍板);站位无效时实时 GPS 兜底 */
|
|
||||||
private fun stationPosition(): Pair<Double, Double>? {
|
|
||||||
// ① 站位(设置页设置的位置)
|
|
||||||
val station = runCatching {
|
|
||||||
val container = (application as MainApplication).getMainContainer()
|
|
||||||
container.settingsRepo.stationPosition.value
|
|
||||||
}.getOrNull()
|
|
||||||
if (station != null && (station.latitude != 0.0 || station.longitude != 0.0)) {
|
|
||||||
return Pair(station.latitude, station.longitude)
|
|
||||||
}
|
|
||||||
// ② 兜底:实时 GPS 最后位置
|
|
||||||
return runCatching {
|
|
||||||
val lm = getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager
|
|
||||||
val providers = listOf(
|
|
||||||
android.location.LocationManager.GPS_PROVIDER,
|
|
||||||
android.location.LocationManager.NETWORK_PROVIDER
|
|
||||||
)
|
|
||||||
for (p in providers) {
|
|
||||||
val loc = lm.getLastKnownLocation(p) ?: continue
|
|
||||||
if (loc.latitude != 0.0 || loc.longitude != 0.0) {
|
|
||||||
return Pair(loc.latitude, loc.longitude)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createChannel() {
|
|
||||||
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
CHANNEL_ID, getString(R.string.aprs_notif_channel),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
)
|
|
||||||
nm.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
reporter?.stop()
|
|
||||||
reporter = null
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,29 +30,10 @@ class MainApplication : Application(), IContainerProvider {
|
|||||||
|
|
||||||
private lateinit var container: IMainContainer
|
private lateinit var container: IMainContainer
|
||||||
|
|
||||||
/** 全局崩溃捕获:堆栈写入 files/crash_log.txt,重启后可查看(用户要求错误报告) */
|
|
||||||
private fun installCrashHandler() {
|
|
||||||
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
|
||||||
runCatching {
|
|
||||||
val log = StringBuilder()
|
|
||||||
log.append("=== Crash ${System.currentTimeMillis()} ===\n")
|
|
||||||
log.append("Thread: ").append(thread.name).append("\n")
|
|
||||||
val sw = java.io.StringWriter()
|
|
||||||
throwable.printStackTrace(java.io.PrintWriter(sw))
|
|
||||||
log.append(sw.toString()).append("\n")
|
|
||||||
val file = java.io.File(filesDir, "crash_log.txt")
|
|
||||||
file.appendText(log.toString())
|
|
||||||
}
|
|
||||||
defaultHandler?.uncaughtException(thread, throwable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMainContainer(): IMainContainer = container
|
override fun getMainContainer(): IMainContainer = container
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
installCrashHandler()
|
|
||||||
container = MainContainer(this)
|
container = MainContainer(this)
|
||||||
// trigger automatic update every 48 hours
|
// trigger automatic update every 48 hours
|
||||||
container.appScope.launch { checkAutoUpdate() }
|
container.appScope.launch { checkAutoUpdate() }
|
||||||
|
|||||||
@@ -17,23 +17,16 @@
|
|||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat
|
package com.rtbishop.look4sat
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.RepeatMode
|
import androidx.compose.animation.core.RepeatMode
|
||||||
import androidx.compose.animation.core.Spring
|
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.expandVertically
|
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
@@ -60,9 +53,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -73,16 +63,13 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.ViewModelStoreOwner
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||||
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
|
||||||
import com.rtbishop.look4sat.core.presentation.DeeplinkResolver
|
import com.rtbishop.look4sat.core.presentation.DeeplinkResolver
|
||||||
import com.rtbishop.look4sat.core.presentation.ElevationThresholds
|
import com.rtbishop.look4sat.core.presentation.ElevationThresholds
|
||||||
import com.rtbishop.look4sat.core.presentation.LocalElevationThresholds
|
import com.rtbishop.look4sat.core.presentation.LocalElevationThresholds
|
||||||
@@ -90,15 +77,9 @@ import com.rtbishop.look4sat.core.presentation.RadarDestination
|
|||||||
import com.rtbishop.look4sat.core.presentation.Screen
|
import com.rtbishop.look4sat.core.presentation.Screen
|
||||||
import com.rtbishop.look4sat.core.presentation.hasEnoughHeight
|
import com.rtbishop.look4sat.core.presentation.hasEnoughHeight
|
||||||
import com.rtbishop.look4sat.core.presentation.hasEnoughWidth
|
import com.rtbishop.look4sat.core.presentation.hasEnoughWidth
|
||||||
import com.rtbishop.look4sat.feature.cw.CwDecodeScreen
|
|
||||||
import com.rtbishop.look4sat.feature.map.MapDestination
|
import com.rtbishop.look4sat.feature.map.MapDestination
|
||||||
import com.rtbishop.look4sat.feature.mutual.MutualScreen
|
|
||||||
import com.rtbishop.look4sat.feature.mutual.MutualViewModel
|
|
||||||
import com.rtbishop.look4sat.feature.passes.PassesDestination
|
import com.rtbishop.look4sat.feature.passes.PassesDestination
|
||||||
import com.rtbishop.look4sat.feature.radar.RadarDestination
|
import com.rtbishop.look4sat.feature.radar.RadarDestination
|
||||||
import com.rtbishop.look4sat.feature.radar.WavelogLogScreen
|
|
||||||
import com.rtbishop.look4sat.feature.status.SatStatusScreen
|
|
||||||
import com.rtbishop.look4sat.feature.roaming.RoamingScreen
|
|
||||||
import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
|
import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
|
||||||
import com.rtbishop.look4sat.feature.settings.SettingsDestination
|
import com.rtbishop.look4sat.feature.settings.SettingsDestination
|
||||||
|
|
||||||
@@ -144,40 +125,12 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
val currentKey = backStack.lastOrNull()
|
val currentKey = backStack.lastOrNull()
|
||||||
val navigateBack: () -> Unit = { backStack.removeLastOrNull() }
|
val navigateBack: () -> Unit = { backStack.removeLastOrNull() }
|
||||||
val fadeTransition = fadeIn(animationSpec = tween(350)) togetherWith fadeOut(animationSpec = tween(350))
|
val fadeTransition = fadeIn(animationSpec = tween(350)) togetherWith fadeOut(animationSpec = tween(350))
|
||||||
|
val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Map, Screen.Settings)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||||
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
||||||
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
||||||
// UI 设置: 按 screenOrder 排序(空 = 默认顺序), 再按 hiddenScreens 过滤(设置页固定保留)
|
|
||||||
val allNavItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.CwDecode, Screen.WavelogLog, Screen.AmSat, Screen.Map, Screen.Settings)
|
|
||||||
.sortedBy { screen ->
|
|
||||||
// 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置(漫游↔地图), 再兜底最后
|
|
||||||
val idx = otherSettings.screenOrder.indexOf(screen.screenId)
|
|
||||||
if (idx != -1) idx
|
|
||||||
else com.rtbishop.look4sat.core.presentation.defaultScreenOrder.indexOf(screen.screenId).let {
|
|
||||||
if (it != -1) it else Int.MAX_VALUE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter { it.screenId !in otherSettings.hiddenScreens || it is Screen.Settings }
|
|
||||||
// 4.5.1 折叠菜单: 主菜单(底部栏 5 槽) + 更多菜单(溢出页面)
|
|
||||||
// 老用户迁移: 已持久化的 subMenuOrder 不含新页面 WavelogLog → 追加到子菜单尾部
|
|
||||||
val subOrder = (otherSettings.subMenuOrder.ifEmpty { com.rtbishop.look4sat.core.presentation.defaultSubMenuOrder })
|
|
||||||
.let { list -> if ("WavelogLog" in list) list else list + "WavelogLog" }
|
|
||||||
.let { list -> if ("AMSAT" in list) list else list + "AMSAT" }
|
|
||||||
val mainNavItems = remember(allNavItems, subOrder) {
|
|
||||||
allNavItems.filter { it.screenId !in subOrder }.take(5)
|
|
||||||
}
|
|
||||||
val moreNavItems = remember(allNavItems, subOrder) {
|
|
||||||
subOrder.mapNotNull { id -> allNavItems.find { it.screenId == id } }
|
|
||||||
}
|
|
||||||
var moreExpanded by remember { mutableStateOf(false) }
|
|
||||||
// 更多菜单打开时拦截返回: 先关菜单
|
|
||||||
BackHandler(enabled = moreExpanded) { moreExpanded = false }
|
|
||||||
// Activity-scoped so the mutual query results survive navigation to Radar and back
|
|
||||||
val mutualViewModel: MutualViewModel = viewModel(
|
|
||||||
viewModelStoreOwner = context as ViewModelStoreOwner,
|
|
||||||
factory = MutualViewModel.factory(container)
|
|
||||||
)
|
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalElevationThresholds provides ElevationThresholds(
|
LocalElevationThresholds provides ElevationThresholds(
|
||||||
@@ -187,15 +140,11 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
) {
|
) {
|
||||||
NavigationSuiteScaffold(
|
NavigationSuiteScaffold(
|
||||||
navigationSuiteItems = {
|
navigationSuiteItems = {
|
||||||
mainNavItems.forEach { screen ->
|
navItems.forEach { screen ->
|
||||||
val isSelected = when (currentKey) {
|
val isSelected = when (currentKey) {
|
||||||
is Screen.Satellites -> screen is Screen.Satellites
|
is Screen.Satellites -> screen is Screen.Satellites
|
||||||
is Screen.Passes -> screen is Screen.Passes
|
is Screen.Passes -> screen is Screen.Passes
|
||||||
is Screen.Radar -> screen is Screen.Radar
|
is Screen.Radar -> screen is Screen.Radar
|
||||||
is Screen.Mutual -> screen is Screen.Mutual
|
|
||||||
is Screen.CwDecode -> screen is Screen.CwDecode
|
|
||||||
is Screen.WavelogLog -> screen is Screen.WavelogLog
|
|
||||||
is Screen.AmSat -> screen is Screen.AmSat
|
|
||||||
is Screen.Map -> screen is Screen.Map
|
is Screen.Map -> screen is Screen.Map
|
||||||
is Screen.Settings -> screen is Screen.Settings
|
is Screen.Settings -> screen is Screen.Settings
|
||||||
else -> false
|
else -> false
|
||||||
@@ -206,26 +155,11 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isSelected) return@item
|
if (isSelected) return@item
|
||||||
moreExpanded = false
|
|
||||||
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
|
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
|
||||||
if (screen !is Screen.Passes) backStack.add(screen)
|
if (screen !is Screen.Passes) backStack.add(screen)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 更多菜单按钮(固定第 6 槽, 子菜单非空才显示)
|
|
||||||
if (moreNavItems.isNotEmpty()) {
|
|
||||||
item(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painterResource(com.rtbishop.look4sat.R.drawable.ic_more),
|
|
||||||
stringResource(com.rtbishop.look4sat.core.presentation.R.string.nav_more)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(com.rtbishop.look4sat.core.presentation.R.string.nav_more)) },
|
|
||||||
selected = moreExpanded,
|
|
||||||
onClick = { moreExpanded = !moreExpanded }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
navigationSuiteColors = NavigationSuiteDefaults.colors(
|
navigationSuiteColors = NavigationSuiteDefaults.colors(
|
||||||
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
|
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||||
@@ -236,137 +170,90 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
else -> NavigationSuiteType.ShortNavigationBarMedium
|
else -> NavigationSuiteType.ShortNavigationBarMedium
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box {
|
Column {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
NavDisplay(
|
||||||
NavDisplay(
|
backStack = backStack,
|
||||||
backStack = backStack,
|
modifier = Modifier.weight(1f),
|
||||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
onBack = navigateBack,
|
||||||
onBack = navigateBack,
|
transitionSpec = { fadeTransition },
|
||||||
transitionSpec = { fadeTransition },
|
popTransitionSpec = { fadeTransition },
|
||||||
popTransitionSpec = { fadeTransition },
|
predictivePopTransitionSpec = { fadeTransition },
|
||||||
predictivePopTransitionSpec = { fadeTransition },
|
entryDecorators = listOf(
|
||||||
entryDecorators = listOf(
|
// Required for saving Compose state per entry
|
||||||
// Required for saving Compose state per entry
|
rememberSaveableStateHolderNavEntryDecorator(),
|
||||||
rememberSaveableStateHolderNavEntryDecorator(),
|
// Required for ViewModel scoping per entry
|
||||||
// Required for ViewModel scoping per entry
|
rememberViewModelStoreNavEntryDecorator()
|
||||||
rememberViewModelStoreNavEntryDecorator()
|
),
|
||||||
),
|
entryProvider = entryProvider {
|
||||||
entryProvider = entryProvider {
|
entry<Screen.Satellites> {
|
||||||
entry<Screen.Satellites> {
|
SatellitesDestination(navigateUp = navigateBack)
|
||||||
SatellitesDestination(navigateUp = navigateBack)
|
}
|
||||||
}
|
entry<Screen.Passes> {
|
||||||
entry<Screen.Passes> {
|
PassesDestination { catNum, aosTime ->
|
||||||
PassesDestination { catNum, aosTime ->
|
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||||
container.setMutualPassData(MutualPassData())
|
backStack.add(Screen.Radar)
|
||||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
// navigateToRadar()
|
||||||
backStack.add(Screen.Radar)
|
|
||||||
// navigateToRadar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entry<Screen.Radar> {
|
|
||||||
RadarDestination(navigateUp = navigateBack)
|
|
||||||
}
|
|
||||||
entry<Screen.Map> {
|
|
||||||
MapDestination()
|
|
||||||
}
|
|
||||||
entry<Screen.Mutual> {
|
|
||||||
MutualScreen(
|
|
||||||
viewModel = mutualViewModel,
|
|
||||||
navigateUp = navigateBack,
|
|
||||||
navigateToRadar = { catNum, aosTime, pass ->
|
|
||||||
container.setMutualPassData(pass ?: MutualPassData())
|
|
||||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
|
||||||
backStack.add(Screen.Radar)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
entry<Screen.Roaming> {
|
|
||||||
RoamingScreen()
|
|
||||||
}
|
|
||||||
entry<Screen.CwDecode> {
|
|
||||||
CwDecodeScreen()
|
|
||||||
}
|
|
||||||
entry<Screen.AmSat> {
|
|
||||||
SatStatusScreen(container = container)
|
|
||||||
}
|
|
||||||
entry<Screen.WavelogLog> {
|
|
||||||
WavelogLogScreen(queue = container.wavelogQueue)
|
|
||||||
}
|
|
||||||
entry<Screen.Settings> {
|
|
||||||
SettingsDestination()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
entry<Screen.Radar> {
|
||||||
// Radio tracking status banner
|
RadarDestination(navigateUp = navigateBack)
|
||||||
if (trackingState.isActive) {
|
}
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse")
|
entry<Screen.Map> {
|
||||||
val alpha by infiniteTransition.animateFloat(
|
MapDestination()
|
||||||
initialValue = 1f, targetValue = 0.4f,
|
}
|
||||||
animationSpec = infiniteRepeatable(
|
entry<Screen.Settings> {
|
||||||
animation = tween(1000, easing = LinearEasing),
|
SettingsDestination()
|
||||||
repeatMode = RepeatMode.Reverse
|
|
||||||
), label = "pulseAlpha"
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
|
||||||
.clickable {
|
|
||||||
val pass = trackingState.currentPass
|
|
||||||
if (pass != null) {
|
|
||||||
container.setMutualPassData(MutualPassData())
|
|
||||||
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
|
|
||||||
backStack.add(Screen.Radar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(8.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(Color(0xFF4CAF50).copy(alpha = alpha))
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(com.rtbishop.look4sat.core.presentation.R.string.tracking_status, trackingState.currentPass?.name ?: ""),
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
val txOk = if (trackingState.txConnected) "TX" else ""
|
|
||||||
val rxOk = if (trackingState.rxConnected) "RX" else ""
|
|
||||||
Text(
|
|
||||||
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
// 更多菜单弹出面板(覆盖在内容上, 底部栏上方; spring 弹跳)
|
// Radio tracking status banner
|
||||||
AnimatedVisibility(
|
if (trackingState.isActive) {
|
||||||
visible = moreExpanded,
|
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse")
|
||||||
modifier = Modifier.fillMaxSize(),
|
val alpha by infiniteTransition.animateFloat(
|
||||||
enter = expandVertically(
|
initialValue = 1f, targetValue = 0.4f,
|
||||||
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
|
animationSpec = infiniteRepeatable(
|
||||||
) + fadeIn(),
|
animation = tween(1000, easing = LinearEasing),
|
||||||
exit = shrinkVertically() + fadeOut()
|
repeatMode = RepeatMode.Reverse
|
||||||
) {
|
), label = "pulseAlpha"
|
||||||
MoreMenuPopup(
|
|
||||||
items = moreNavItems,
|
|
||||||
currentKey = currentKey,
|
|
||||||
onDismiss = { moreExpanded = false },
|
|
||||||
onSelect = { screen ->
|
|
||||||
moreExpanded = false
|
|
||||||
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
|
|
||||||
if (screen !is Screen.Passes) backStack.add(screen)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
|
.clickable {
|
||||||
|
val pass = trackingState.currentPass
|
||||||
|
if (pass != null) {
|
||||||
|
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
|
||||||
|
backStack.add(Screen.Radar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(8.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(0xFF4CAF50).copy(alpha = alpha))
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Tracking: ${trackingState.currentPass?.name ?: ""}",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
val txOk = if (trackingState.txConnected) "TX" else ""
|
||||||
|
val rxOk = if (trackingState.rxConnected) "RX" else ""
|
||||||
|
Text(
|
||||||
|
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
* MoreMenuPopup.kt — 底部导航「更多」二级菜单弹出面板(4.5.1)。
|
|
||||||
*
|
|
||||||
* 覆盖在内容区上(底部栏上方, 右对齐), 竖排菜单项(图标+文本+箭头),
|
|
||||||
* 当前页高亮; 点击遮罩关闭, 点击项跳转。弹出/收起动画由调用处
|
|
||||||
* (MainScreen 的 AnimatedVisibility + spring)驱动。
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
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.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
|
||||||
import com.rtbishop.look4sat.core.presentation.Screen
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MoreMenuPopup(
|
|
||||||
items: List<Screen>,
|
|
||||||
currentKey: NavKey?,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onSelect: (Screen) -> Unit
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.35f))
|
|
||||||
.clickable(onClick = onDismiss)
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(12.dp),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.padding(vertical = 4.dp)) {
|
|
||||||
items.forEach { screen ->
|
|
||||||
val isSelected = when (currentKey) {
|
|
||||||
is Screen.Satellites -> screen is Screen.Satellites
|
|
||||||
is Screen.Passes -> screen is Screen.Passes
|
|
||||||
is Screen.Radar -> screen is Screen.Radar
|
|
||||||
is Screen.Mutual -> screen is Screen.Mutual
|
|
||||||
is Screen.CwDecode -> screen is Screen.CwDecode
|
|
||||||
is Screen.WavelogLog -> screen is Screen.WavelogLog
|
|
||||||
is Screen.Map -> screen is Screen.Map
|
|
||||||
is Screen.Settings -> screen is Screen.Settings
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { onSelect(screen) }
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(screen.iconResId),
|
|
||||||
contentDescription = stringResource(screen.titleResId),
|
|
||||||
tint = if (isSelected) MaterialTheme.colorScheme.primary
|
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(screen.titleResId),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.primary
|
|
||||||
else MaterialTheme.colorScheme.onSurface,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "›",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
|
|
||||||
</vector>
|
|
||||||
-4
@@ -31,15 +31,11 @@ internal class ApplicationPlugin : Plugin<Project> {
|
|||||||
implementation(project(":core:data"))
|
implementation(project(":core:data"))
|
||||||
implementation(project(":core:domain"))
|
implementation(project(":core:domain"))
|
||||||
implementation(project(":core:presentation"))
|
implementation(project(":core:presentation"))
|
||||||
implementation(project(":feature:cw"))
|
|
||||||
implementation(project(":feature:map"))
|
implementation(project(":feature:map"))
|
||||||
implementation(project(":feature:mutual"))
|
|
||||||
implementation(project(":feature:passes"))
|
implementation(project(":feature:passes"))
|
||||||
implementation(project(":feature:radar"))
|
implementation(project(":feature:radar"))
|
||||||
implementation(project(":feature:roaming"))
|
|
||||||
implementation(project(":feature:satellites"))
|
implementation(project(":feature:satellites"))
|
||||||
implementation(project(":feature:settings"))
|
implementation(project(":feature:settings"))
|
||||||
implementation(project(":feature:status"))
|
|
||||||
implementation(libs.androidx.core.splashscreen)
|
implementation(libs.androidx.core.splashscreen)
|
||||||
implementation(libs.compose.material3.adaptive)
|
implementation(libs.compose.material3.adaptive)
|
||||||
implementation(libs.compose.navigation3)
|
implementation(libs.compose.navigation3)
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ internal fun Project.setupAndroidApp() {
|
|||||||
namespace = libs.versions.packageName.get()
|
namespace = libs.versions.packageName.get()
|
||||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = libs.versions.applicationId.get()
|
applicationId = libs.versions.packageName.get()
|
||||||
minSdk = libs.versions.minSdk.get().toInt()
|
minSdk = libs.versions.minSdk.get().toInt()
|
||||||
versionCode = libs.versions.appVersionCode.get().toInt()
|
versionCode = libs.versions.appVersionCode.get().toInt()
|
||||||
versionName = libs.versions.appVersionName.get()
|
versionName = libs.versions.appVersionName.get()
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
package com.rtbishop.look4sat.core.data.aprs
|
|
||||||
|
|
||||||
import com.rtbishop.look4sat.core.domain.aprs.AprsPacket
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.io.OutputStreamWriter
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.Socket
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APRS-IS TCP 客户端(从 APRSdroid TcpUploader.scala 逆向移植)。
|
|
||||||
* 纯文本协议:登录一行 + 每行一个包;断线 30 秒重连。
|
|
||||||
*/
|
|
||||||
class AprsIsClient(
|
|
||||||
private val host: String,
|
|
||||||
private val port: Int,
|
|
||||||
private val callsign: String,
|
|
||||||
private val ssid: String,
|
|
||||||
private val passcode: Int,
|
|
||||||
private val version: String,
|
|
||||||
private val filter: String = "",
|
|
||||||
private val timeoutSec: Int = 120
|
|
||||||
) {
|
|
||||||
private var socket: Socket? = null
|
|
||||||
private var writer: PrintWriter? = null
|
|
||||||
private var reader: BufferedReader? = null
|
|
||||||
private val lock = Any()
|
|
||||||
|
|
||||||
val isConnected: Boolean
|
|
||||||
get() = synchronized(lock) { socket?.isConnected == true && !socket!!.isClosed }
|
|
||||||
|
|
||||||
/** 建立连接 + 登录(同步阻塞,调用方放后台线程) */
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun connect() {
|
|
||||||
disconnect()
|
|
||||||
val s = Socket()
|
|
||||||
s.connect(InetSocketAddress(host, port), 30_000)
|
|
||||||
s.soTimeout = timeoutSec * 1000
|
|
||||||
s.tcpNoDelay = true
|
|
||||||
synchronized(lock) {
|
|
||||||
socket = s
|
|
||||||
writer = PrintWriter(OutputStreamWriter(s.getOutputStream(), Charsets.ISO_8859_1), true)
|
|
||||||
reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.ISO_8859_1), 256)
|
|
||||||
}
|
|
||||||
// 登录行
|
|
||||||
val login = AprsPacket.formatLogin(callsign, ssid, passcode, version) + filter
|
|
||||||
writer?.println(login)
|
|
||||||
// 读登录响应(aprsc 返回 # logresp ... verified/unverified)
|
|
||||||
runCatching {
|
|
||||||
s.soTimeout = 8000
|
|
||||||
val resp = reader?.readLine()
|
|
||||||
if (resp != null && (resp.contains("Invalid", ignoreCase = true) ||
|
|
||||||
resp.contains("unverified", ignoreCase = true))) {
|
|
||||||
throw IllegalArgumentException(resp.trim())
|
|
||||||
}
|
|
||||||
// 恢复超时
|
|
||||||
s.soTimeout = timeoutSec * 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送一个 APRS 包(一行)并尝试读服务器确认。
|
|
||||||
* 返回 null=发送失败;Pair(ok, detail)=发送结果(服务器错误原文在 detail)
|
|
||||||
*/
|
|
||||||
fun sendPacket(packetLine: String): Pair<Boolean, String>? {
|
|
||||||
synchronized(lock) {
|
|
||||||
val w = writer ?: return null
|
|
||||||
w.println(packetLine)
|
|
||||||
if (w.checkError()) return Pair(false, "write failed")
|
|
||||||
}
|
|
||||||
// 尝试读服务器响应(短超时 3s;APRS-IS 对格式错误会回错误行)
|
|
||||||
return runCatching {
|
|
||||||
val s = socket ?: return@runCatching Pair(true, "OK")
|
|
||||||
val oldTimeout = s.soTimeout
|
|
||||||
s.soTimeout = 3000
|
|
||||||
try {
|
|
||||||
val resp = reader?.readLine()
|
|
||||||
if (resp != null && (resp.contains("Invalid", ignoreCase = true) ||
|
|
||||||
resp.contains("error", ignoreCase = true))) {
|
|
||||||
Pair(false, resp.trim())
|
|
||||||
} else {
|
|
||||||
Pair(true, if (resp.isNullOrBlank()) "OK" else resp.trim())
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
s.soTimeout = oldTimeout
|
|
||||||
}
|
|
||||||
}.getOrElse { Pair(true, "OK") }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 读一行(服务器响应,超时抛异常) */
|
|
||||||
fun readLine(): String? {
|
|
||||||
return reader?.readLine()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disconnect() {
|
|
||||||
synchronized(lock) {
|
|
||||||
runCatching { writer?.close() }
|
|
||||||
runCatching { reader?.close() }
|
|
||||||
runCatching { socket?.close() }
|
|
||||||
writer = null
|
|
||||||
reader = null
|
|
||||||
socket = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package com.rtbishop.look4sat.core.data.aprs
|
|
||||||
|
|
||||||
import com.rtbishop.look4sat.core.domain.aprs.AprsPacket
|
|
||||||
import com.rtbishop.look4sat.core.domain.aprs.AprsPosition
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/** APRS 连接状态 */
|
|
||||||
enum class AprsState { Idle, Connecting, Connected, Disconnected, Error }
|
|
||||||
|
|
||||||
/** APRS 配置(SharedPreferences 持久化,填入即保存) */
|
|
||||||
data class AprsConfig(
|
|
||||||
val enabled: Boolean = false,
|
|
||||||
val server: String = "euro.aprs2.net",
|
|
||||||
val port: Int = 14580,
|
|
||||||
val callsign: String = "",
|
|
||||||
val ssid: String = "",
|
|
||||||
val passcode: String = "",
|
|
||||||
val intervalMin: Int = 5,
|
|
||||||
val statusText: String = "Look4Sat APRS",
|
|
||||||
val symbolTable: String = "/",
|
|
||||||
val symbolCode: String = ">",
|
|
||||||
val includeCourseSpeed: Boolean = true,
|
|
||||||
val includeAltitude: Boolean = true
|
|
||||||
)
|
|
||||||
|
|
||||||
/** APRS 上报结果 */
|
|
||||||
data class AprsReport(
|
|
||||||
val timestamp: Long,
|
|
||||||
val packet: String,
|
|
||||||
val ok: Boolean,
|
|
||||||
val detail: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 上报调度器(周期上报 + 手动触发),连接管理放在前台服务 */
|
|
||||||
class AprsReporter(
|
|
||||||
private val configProvider: () -> AprsConfig,
|
|
||||||
private val positionProvider: () -> Pair<Double, Double>? = { null },
|
|
||||||
private val onState: (AprsState) -> Unit = {},
|
|
||||||
private val onReport: (AprsReport) -> Unit = {}
|
|
||||||
) {
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
private var client: AprsIsClient? = null
|
|
||||||
private var job: Job? = null
|
|
||||||
private var manualJob: Job? = null
|
|
||||||
|
|
||||||
val isRunning: Boolean get() = job?.isActive == true
|
|
||||||
|
|
||||||
/** 启动周期上报(前台服务调用) */
|
|
||||||
fun start() {
|
|
||||||
stop()
|
|
||||||
val cfg = configProvider()
|
|
||||||
if (!cfg.enabled || cfg.callsign.isBlank()) {
|
|
||||||
onState(AprsState.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
job = scope.launch {
|
|
||||||
while (isActive) {
|
|
||||||
reportOnce()
|
|
||||||
delay(cfg.intervalMin.coerceAtLeast(1) * 60_000L)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
job?.cancel()
|
|
||||||
job = null
|
|
||||||
runCatching { client?.disconnect() }
|
|
||||||
client = null
|
|
||||||
onState(AprsState.Idle)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 手动触发一次(立即上报,不等待周期) */
|
|
||||||
fun reportNow() {
|
|
||||||
manualJob?.cancel()
|
|
||||||
manualJob = scope.launch { reportOnce() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun reportOnce() {
|
|
||||||
val cfg = configProvider()
|
|
||||||
if (!cfg.enabled || cfg.callsign.isBlank()) return
|
|
||||||
onState(AprsState.Connecting)
|
|
||||||
try {
|
|
||||||
val c = client ?: AprsIsClient(
|
|
||||||
host = cfg.server,
|
|
||||||
port = cfg.port,
|
|
||||||
callsign = cfg.callsign,
|
|
||||||
ssid = cfg.ssid,
|
|
||||||
passcode = cfg.passcode.toIntOrNull()?.takeIf { it >= 0 } ?: AprsPacket.passcode(cfg.callsign),
|
|
||||||
version = "Look4Sat 4.5.4"
|
|
||||||
).also { client = it }
|
|
||||||
if (!c.isConnected) c.connect()
|
|
||||||
onState(AprsState.Connected)
|
|
||||||
|
|
||||||
val pos = positionProvider()
|
|
||||||
val packetLine = buildPositionPacket(cfg, pos?.first, pos?.second)
|
|
||||||
val result = c.sendPacket(packetLine)
|
|
||||||
val ok = result?.first == true
|
|
||||||
val detail = result?.second ?: "no connection"
|
|
||||||
onReport(AprsReport(System.currentTimeMillis(), packetLine, ok, detail))
|
|
||||||
if (ok) onState(AprsState.Connected) else onState(AprsState.Error)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
runCatching { client?.disconnect() }
|
|
||||||
client = null
|
|
||||||
onState(AprsState.Error)
|
|
||||||
onReport(AprsReport(System.currentTimeMillis(), "", false, e.message ?: "error"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 构造位置包:BG7NTA-5>APRS:=DDMM.MMN/DDDMM.MME<状态文本 */
|
|
||||||
private fun buildPositionPacket(cfg: AprsConfig, lat: Double? = null, lon: Double? = null): String {
|
|
||||||
val source = AprsPacket.formatCallSsid(cfg.callsign, cfg.ssid)
|
|
||||||
val pos = AprsPosition(
|
|
||||||
latitude = lat ?: 0.0,
|
|
||||||
longitude = lon ?: 0.0,
|
|
||||||
symbolTable = cfg.symbolTable.firstOrNull() ?: '/',
|
|
||||||
symbolCode = cfg.symbolCode.firstOrNull() ?: '>'
|
|
||||||
)
|
|
||||||
return "$source>APRS:=${pos.toUncompressedString()}${cfg.statusText}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package com.rtbishop.look4sat.core.data.aprs
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APRS 配置存取 + 服务 action 常量(被 feature/settings 和 app 服务共用,
|
|
||||||
* 避免 feature 反向依赖 app)。
|
|
||||||
*/
|
|
||||||
object AprsStore {
|
|
||||||
|
|
||||||
const val ACTION_START = "com.rtbishop.look4sat.aprs.START"
|
|
||||||
const val ACTION_STOP = "com.rtbishop.look4sat.aprs.STOP"
|
|
||||||
const val ACTION_REPORT_NOW = "com.rtbishop.look4sat.aprs.REPORT_NOW"
|
|
||||||
const val SERVICE_CLASS = "com.rtbishop.look4sat.app.AprsForegroundService"
|
|
||||||
|
|
||||||
private const val PREFS = "aprs_config"
|
|
||||||
private const val KEY_ENABLED = "enabled"
|
|
||||||
private const val KEY_SERVER = "server"
|
|
||||||
private const val KEY_PORT = "port"
|
|
||||||
private const val KEY_CALLSIGN = "callsign"
|
|
||||||
private const val KEY_SSID = "ssid"
|
|
||||||
private const val KEY_PASSCODE = "passcode"
|
|
||||||
private const val KEY_INTERVAL = "interval"
|
|
||||||
private const val KEY_STATUS = "status"
|
|
||||||
private const val KEY_SYMBOL_TABLE = "symbol_table"
|
|
||||||
private const val KEY_SYMBOL_CODE = "symbol_code"
|
|
||||||
private const val KEY_LAST_TIME = "last_report_time"
|
|
||||||
private const val KEY_LAST_OK = "last_report_ok"
|
|
||||||
private const val KEY_LAST_DETAIL = "last_report_detail"
|
|
||||||
|
|
||||||
/** 读取配置(填入即保存,无需每次打开重填) */
|
|
||||||
fun loadConfig(context: Context): AprsConfig {
|
|
||||||
val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
return AprsConfig(
|
|
||||||
enabled = p.getBoolean(KEY_ENABLED, false),
|
|
||||||
server = p.getString(KEY_SERVER, "euro.aprs2.net") ?: "euro.aprs2.net",
|
|
||||||
port = p.getInt(KEY_PORT, 14580),
|
|
||||||
callsign = p.getString(KEY_CALLSIGN, "") ?: "",
|
|
||||||
ssid = p.getString(KEY_SSID, "") ?: "",
|
|
||||||
passcode = p.getString(KEY_PASSCODE, "") ?: "",
|
|
||||||
intervalMin = p.getInt(KEY_INTERVAL, 5),
|
|
||||||
statusText = p.getString(KEY_STATUS, "Look4Sat APRS") ?: "Look4Sat APRS",
|
|
||||||
symbolTable = p.getString(KEY_SYMBOL_TABLE, "/") ?: "/",
|
|
||||||
symbolCode = p.getString(KEY_SYMBOL_CODE, ">") ?: ">"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 上次上报结果(供设置卡片显示) */
|
|
||||||
data class LastReport(val time: Long = 0L, val ok: Boolean = false, val detail: String = "")
|
|
||||||
|
|
||||||
fun loadLastReport(context: Context): LastReport {
|
|
||||||
val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
return LastReport(
|
|
||||||
time = p.getLong(KEY_LAST_TIME, 0L),
|
|
||||||
ok = p.getBoolean(KEY_LAST_OK, false),
|
|
||||||
detail = p.getString(KEY_LAST_DETAIL, "") ?: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveLastReport(context: Context, ok: Boolean, detail: String) {
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
|
||||||
.putLong(KEY_LAST_TIME, System.currentTimeMillis())
|
|
||||||
.putBoolean(KEY_LAST_OK, ok)
|
|
||||||
.putString(KEY_LAST_DETAIL, detail)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 保存配置 */
|
|
||||||
fun saveConfig(context: Context, cfg: AprsConfig) {
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
|
||||||
.putBoolean(KEY_ENABLED, cfg.enabled)
|
|
||||||
.putString(KEY_SERVER, cfg.server)
|
|
||||||
.putInt(KEY_PORT, cfg.port)
|
|
||||||
.putString(KEY_CALLSIGN, cfg.callsign)
|
|
||||||
.putString(KEY_SSID, cfg.ssid)
|
|
||||||
.putString(KEY_PASSCODE, cfg.passcode)
|
|
||||||
.putInt(KEY_INTERVAL, cfg.intervalMin)
|
|
||||||
.putString(KEY_STATUS, cfg.statusText)
|
|
||||||
.putString(KEY_SYMBOL_TABLE, cfg.symbolTable)
|
|
||||||
.putString(KEY_SYMBOL_CODE, cfg.symbolCode)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -45,7 +45,12 @@ interface Look4SatDao {
|
|||||||
@Query("DELETE FROM entries")
|
@Query("DELETE FROM entries")
|
||||||
suspend fun deleteEntries()
|
suspend fun deleteEntries()
|
||||||
|
|
||||||
@Query("SELECT catnum FROM radios WHERE downlinkMode IN (:modes)")
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT catnum FROM radios WHERE isAlive = 1
|
||||||
|
AND (downlinkMode IN (:modes) OR uplinkMode IN (:modes))
|
||||||
|
"""
|
||||||
|
)
|
||||||
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM radios")
|
@Query("SELECT COUNT(*) FROM radios")
|
||||||
|
|||||||
+12
-35
@@ -41,9 +41,6 @@ import com.rtbishop.look4sat.core.data.usecase.AddToCalendar
|
|||||||
import com.rtbishop.look4sat.core.data.usecase.AudioCapture
|
import com.rtbishop.look4sat.core.data.usecase.AudioCapture
|
||||||
import com.rtbishop.look4sat.core.data.usecase.SaveImage
|
import com.rtbishop.look4sat.core.data.usecase.SaveImage
|
||||||
import com.rtbishop.look4sat.core.data.usecase.ShowToast
|
import com.rtbishop.look4sat.core.data.usecase.ShowToast
|
||||||
import com.rtbishop.look4sat.core.domain.wavelog.IWavelogQueueStore
|
|
||||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
|
||||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader
|
|
||||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||||
@@ -54,7 +51,6 @@ import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
|||||||
import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||||
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
|
||||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||||
@@ -66,34 +62,24 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class MainContainer(private val context: Context) : IMainContainer {
|
class MainContainer(private val context: Context) : IMainContainer {
|
||||||
|
|
||||||
private val localSource = provideLocalSource()
|
private val localSource = provideLocalSource()
|
||||||
private val remoteSource by lazy { provideRemoteSource() }
|
private val remoteSource = provideRemoteSource()
|
||||||
private val mainHandler = CoroutineExceptionHandler { _, error -> println("MainHandler: $error") }
|
private val mainHandler = CoroutineExceptionHandler { _, error -> println("MainHandler: $error") }
|
||||||
override val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
|
override val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
|
||||||
override val settingsRepo = provideSettingsRepo()
|
override val settingsRepo = provideSettingsRepo()
|
||||||
override val selectionRepo = provideSelectionRepo()
|
override val selectionRepo = provideSelectionRepo()
|
||||||
override val satelliteRepo = provideSatelliteRepo()
|
override val satelliteRepo = provideSatelliteRepo()
|
||||||
override val databaseRepo = provideDatabaseRepo()
|
override val databaseRepo = provideDatabaseRepo()
|
||||||
override val amSatRepo = AmSatRepository(remoteSource)
|
override val amSatRepo by lazy { AmSatRepository(remoteSource) }
|
||||||
override val radioTrackingService: IRadioTrackingService by lazy {
|
override val radioTrackingService: IRadioTrackingService by lazy {
|
||||||
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
RadioTrackingService(appScope, manager, satelliteRepo, settingsRepo)
|
RadioTrackingService(appScope, manager, satelliteRepo, settingsRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _mutualPassData = MutableStateFlow(MutualPassData())
|
|
||||||
override val mutualPassData: StateFlow<MutualPassData> = _mutualPassData.asStateFlow()
|
|
||||||
|
|
||||||
override fun setMutualPassData(data: MutualPassData) {
|
|
||||||
_mutualPassData.value = data
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context)
|
override fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context)
|
||||||
|
|
||||||
override fun provideShowToast(): IShowToast = ShowToast(context)
|
override fun provideShowToast(): IShowToast = ShowToast(context)
|
||||||
@@ -102,16 +88,6 @@ class MainContainer(private val context: Context) : IMainContainer {
|
|||||||
|
|
||||||
override fun provideSaveImage(): ISaveImage = SaveImage(context)
|
override fun provideSaveImage(): ISaveImage = SaveImage(context)
|
||||||
|
|
||||||
// WaveLog 日志(4.5.2): 本地队列 + 上传器(共享实例)
|
|
||||||
override val wavelogQueue: WavelogQueue by lazy {
|
|
||||||
val prefs = context.getSharedPreferences("wavelog", Context.MODE_PRIVATE)
|
|
||||||
WavelogQueue(object : IWavelogQueueStore {
|
|
||||||
override fun load(): String = prefs.getString("wavelog_queue", "[]") ?: "[]"
|
|
||||||
override fun save(json: String) = prefs.edit().putString("wavelog_queue", json).apply()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
override fun provideWavelogUploader(): WavelogUploader = WavelogUploader(settingsRepo, wavelogQueue)
|
|
||||||
|
|
||||||
override fun provideBluetoothReporter(): IReporter {
|
override fun provideBluetoothReporter(): IReporter {
|
||||||
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
val rc = settingsRepo.rcSettings.value
|
val rc = settingsRepo.rcSettings.value
|
||||||
@@ -162,9 +138,17 @@ class MainContainer(private val context: Context) : IMainContainer {
|
|||||||
return SensorsRepo(manager, displayManager)
|
return SensorsRepo(manager, displayManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun providePairedBluetoothDevices(): List<Pair<String, String>> = 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 {
|
private fun provideDatabaseRepo(): IDatabaseRepo {
|
||||||
val dbDispatcher = Dispatchers.Default
|
val dbDispatcher = Dispatchers.Default
|
||||||
val dataParser = DataParser(dbDispatcher)
|
val dataParser = DataParser(dbDispatcher)
|
||||||
|
val remoteSource = provideRemoteSource()
|
||||||
return DatabaseRepo(dbDispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
return DatabaseRepo(dbDispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,14 +159,7 @@ class MainContainer(private val context: Context) : IMainContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun provideRemoteSource(): IRemoteSource {
|
private fun provideRemoteSource(): IRemoteSource {
|
||||||
return RemoteSource(
|
return RemoteSource(Dispatchers.IO, context.contentResolver, OkHttpClient.Builder().build())
|
||||||
Dispatchers.IO, context.contentResolver,
|
|
||||||
OkHttpClient.Builder()
|
|
||||||
.connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.readTimeout(20, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.writeTimeout(20, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun provideSatelliteRepo(): ISatelliteRepo {
|
private fun provideSatelliteRepo(): ISatelliteRepo {
|
||||||
@@ -198,6 +175,6 @@ class MainContainer(private val context: Context) : IMainContainer {
|
|||||||
val appPrefsFileName = "${context.packageName}_preferences"
|
val appPrefsFileName = "${context.packageName}_preferences"
|
||||||
val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE)
|
val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE)
|
||||||
val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.4"
|
val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.4"
|
||||||
return SettingsRepo(context, manager, appPreferences, appVersionName)
|
return SettingsRepo(manager, appPreferences, appVersionName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+144
-9
@@ -1,21 +1,156 @@
|
|||||||
package com.rtbishop.look4sat.core.data.repository
|
package com.rtbishop.look4sat.core.data.repository
|
||||||
|
|
||||||
import com.rtbishop.look4sat.core.data.source.AmSatParser
|
import com.rtbishop.look4sat.core.domain.model.SatDay
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.SatReport
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.SatSlot
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.SatStatus
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
|
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IAmSatRepository
|
import com.rtbishop.look4sat.core.domain.repository.IAmSatRepository
|
||||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
/** AMSAT 状态仓库: 抓 HTML → 解析 → SatStatusPage */
|
/** One report from the AMSAT API (data layer model). */
|
||||||
|
private data class ApiReport(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val callsign: String,
|
||||||
|
val report: String,
|
||||||
|
val gridSquare: String,
|
||||||
|
val reportedTimeUtcSec: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
/** AMSAT status repository using RemoteSource (Clean Architecture: data layer handles HTTP). */
|
||||||
class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepository {
|
class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepository {
|
||||||
|
|
||||||
override suspend fun fetchStatus(): SatStatusPage? {
|
private val isoUtcFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
|
||||||
val html = remoteSource.getStatusHtml() ?: return null
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
if (html.isBlank()) return null
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchStatus(): SatStatusPage? = withContext(Dispatchers.IO) {
|
||||||
|
val nowSec = System.currentTimeMillis() / 1000
|
||||||
|
val catalogJson = remoteSource.getAmSatCatalog() ?: return@withContext null
|
||||||
|
val reportsJson = remoteSource.getAmSatReports(hours = 168, limit = 500) ?: return@withContext null
|
||||||
|
|
||||||
|
val names = parseCatalog(catalogJson)
|
||||||
|
val reports = parseReports(reportsJson)
|
||||||
|
|
||||||
|
if (names.isEmpty() && reports.isEmpty()) return@withContext null
|
||||||
|
|
||||||
|
val statuses = buildStatuses(names, reports, nowSec)
|
||||||
|
val reportMap = reports.associate { it.id to toSatReport(it) }
|
||||||
|
SatStatusPage(System.currentTimeMillis(), statuses, reportMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse catalog JSON to list of satellite names */
|
||||||
|
private fun parseCatalog(json: String): List<String> {
|
||||||
return try {
|
return try {
|
||||||
AmSatParser.parse(html, System.currentTimeMillis())
|
val arr = JSONObject(json).getJSONArray("data")
|
||||||
} catch (exception: Exception) {
|
(0 until arr.length()).map { arr.getJSONObject(it).getString("name") }
|
||||||
println("AmSatRepository parse exception: $exception")
|
} catch (e: Exception) {
|
||||||
null
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse reports JSON to list of ApiReport domain objects */
|
||||||
|
private fun parseReports(json: String): List<ApiReport> {
|
||||||
|
return try {
|
||||||
|
val arr = JSONObject(json).getJSONArray("data")
|
||||||
|
(0 until arr.length()).mapNotNull { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
val iso = o.optString("reported_time", "")
|
||||||
|
if (iso.isEmpty()) null else ApiReport(
|
||||||
|
id = o.optString("id", ""),
|
||||||
|
name = o.optString("name", ""),
|
||||||
|
callsign = o.optString("callsign", ""),
|
||||||
|
report = o.optString("report", ""),
|
||||||
|
gridSquare = o.optString("grid_square", ""),
|
||||||
|
reportedTimeUtcSec = parseIsoUtcSec(iso)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse ISO 8601 UTC timestamp to epoch seconds (e.g., "2026-08-05T07:30:00Z") */
|
||||||
|
private fun parseIsoUtcSec(iso: String): Long {
|
||||||
|
return try {
|
||||||
|
(isoUtcFormat.parse(iso)?.time ?: 0L) / 1000
|
||||||
|
} catch (e: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build one SatStatus (6 days x 12 slots) per catalog satellite, slotting reports by age. */
|
||||||
|
private fun buildStatuses(names: List<String>, reports: List<ApiReport>, nowSec: Long): List<SatStatus> {
|
||||||
|
val byName = reports.groupBy { it.name }
|
||||||
|
val monthAbbr = arrayOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
||||||
|
val utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||||
|
val labels = (0 until 6).map { d ->
|
||||||
|
utc.timeInMillis = (nowSec - d * 86400L) * 1000
|
||||||
|
"${monthAbbr[utc.get(Calendar.MONTH)]} ${utc.get(Calendar.DAY_OF_MONTH)}"
|
||||||
|
}
|
||||||
|
return names.map { name ->
|
||||||
|
val slots = (0 until 72).map { slotIdx ->
|
||||||
|
val slotStart = nowSec - (slotIdx + 1) * 7200L
|
||||||
|
val slotEnd = nowSec - slotIdx * 7200L
|
||||||
|
val inSlot = byName[name].orEmpty().filter { it.reportedTimeUtcSec in slotStart until slotEnd }
|
||||||
|
if (inSlot.isEmpty()) {
|
||||||
|
SatSlot(statusColor = NO_REPORT_GRAY, count = 0)
|
||||||
|
} else {
|
||||||
|
val newest = inSlot.maxByOrNull { it.reportedTimeUtcSec }!!
|
||||||
|
SatSlot(
|
||||||
|
statusColor = statusColorOf(newest.report),
|
||||||
|
count = inSlot.size,
|
||||||
|
reportIds = inSlot.map { it.id }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val days = (0 until 6).map { d ->
|
||||||
|
SatDay(dateLabel = labels[d], slots = slots.subList(d * 12, (d + 1) * 12))
|
||||||
|
}
|
||||||
|
SatStatus(name = name, days = days)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toSatReport(r: ApiReport): SatReport {
|
||||||
|
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||||
|
cal.timeInMillis = r.reportedTimeUtcSec * 1000
|
||||||
|
val hh = cal.get(Calendar.HOUR_OF_DAY).toString().padStart(2, '0')
|
||||||
|
val mm = cal.get(Calendar.MINUTE).toString().padStart(2, '0')
|
||||||
|
val y = cal.get(Calendar.YEAR)
|
||||||
|
val mo = (cal.get(Calendar.MONTH) + 1).toString().padStart(2, '0')
|
||||||
|
val d = cal.get(Calendar.DAY_OF_MONTH).toString().padStart(2, '0')
|
||||||
|
return SatReport(
|
||||||
|
id = r.id,
|
||||||
|
statusText = r.report,
|
||||||
|
call = r.callsign,
|
||||||
|
grid = r.gridSquare,
|
||||||
|
dateUtc = "$y-$mo-$d",
|
||||||
|
timeUtc = "$hh:$mm UTC"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map status text to color value (for UI rendering). */
|
||||||
|
private fun statusColorOf(report: String): Long = when (report.lowercase()) {
|
||||||
|
"heard", "crew active" -> ACTIVE_BLUE
|
||||||
|
"telemetry only" -> TLM_ORANGE
|
||||||
|
"not heard" -> NOT_HEARD_PINK
|
||||||
|
else -> CONFLICT_DEEP_ORANGE
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// AMSAT official status colors (from amsat.org/status)
|
||||||
|
private const val ACTIVE_BLUE = 0xFF648FFF
|
||||||
|
private const val TLM_ORANGE = 0xFFFFB000
|
||||||
|
private const val NOT_HEARD_PINK = 0xFFDC267F
|
||||||
|
private const val CONFLICT_DEEP_ORANGE = 0xFFFE6100
|
||||||
|
private const val NO_REPORT_GRAY = 0xFFC0C0C0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,6 @@ class DatabaseRepo(
|
|||||||
remoteSource.getFileStream(uri)?.let { stream ->
|
remoteSource.getFileStream(uri)?.let { stream ->
|
||||||
val entries = parseSatelliteStream(uri, unwrapIfZipped(uri, stream))
|
val entries = parseSatelliteStream(uri, unwrapIfZipped(uri, stream))
|
||||||
localSource.insertEntries(entries)
|
localSource.insertEntries(entries)
|
||||||
settingsRepo.setSatelliteTypeIds(customSourceType, entries.map { it.catnum })
|
|
||||||
importedCount = entries.size
|
importedCount = entries.size
|
||||||
}
|
}
|
||||||
setUpdateSuccessful(System.currentTimeMillis())
|
setUpdateSuccessful(System.currentTimeMillis())
|
||||||
@@ -69,33 +68,20 @@ class DatabaseRepo(
|
|||||||
val dataSourcesSettings = settingsRepo.dataSourcesSettings.value
|
val dataSourcesSettings = settingsRepo.dataSourcesSettings.value
|
||||||
val tleUrls = buildMap {
|
val tleUrls = buildMap {
|
||||||
putAll(Sources.satelliteDataUrls)
|
putAll(Sources.satelliteDataUrls)
|
||||||
// 开关开且 URL 非空 -> All 用自定义 URL;否则用默认 URL(在线更新默认源)
|
if (dataSourcesSettings.useCustomTLE) put(customSourceType, dataSourcesSettings.tleUrl)
|
||||||
put("All", if (dataSourcesSettings.useCustomTLE && dataSourcesSettings.tleUrl.isNotBlank())
|
|
||||||
dataSourcesSettings.tleUrl else Sources.defaultTleUrl)
|
|
||||||
}.filterValues { it.isNotBlank() }
|
}.filterValues { it.isNotBlank() }
|
||||||
val radioUrls = buildMap {
|
val radioUrls = buildMap {
|
||||||
putAll(Sources.transceiversDataUrls)
|
putAll(Sources.transceiversDataUrls)
|
||||||
put("SatNOGS", if (dataSourcesSettings.useCustomTransceivers && dataSourcesSettings.transceiversUrl.isNotBlank())
|
if (dataSourcesSettings.useCustomTransceivers) put(customSourceType, dataSourcesSettings.transceiversUrl)
|
||||||
dataSourcesSettings.transceiversUrl else Sources.defaultTransceiversUrl)
|
|
||||||
}.filterValues { it.isNotBlank() }
|
}.filterValues { it.isNotBlank() }
|
||||||
// launch all network requests concurrently
|
// launch all network requests concurrently
|
||||||
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
||||||
val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
||||||
// 统计成功源数: 0 成功视为更新失败(不刷时间戳, 抛异常让 UI 提示)
|
// parse fetched data concurrently
|
||||||
val tleResults = tleJobs.awaitAll()
|
val importedEntries = tleJobs.awaitAll().flatMap { (url, stream) ->
|
||||||
val radioResults = radioJobs.awaitAll()
|
stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty()
|
||||||
val successCount = tleResults.count { it.second != null } + radioResults.count { it.second != null }
|
|
||||||
if (successCount == 0) {
|
|
||||||
throw java.io.IOException("All data sources failed to download")
|
|
||||||
}
|
}
|
||||||
// parse fetched data concurrently and associate with types
|
val importedRadios = radioJobs.awaitAll().flatMap { (url, stream) ->
|
||||||
val importedEntries = tleResults.flatMap { (url, stream) ->
|
|
||||||
val type = tleUrls.entries.find { it.value == url }?.key ?: customSourceType
|
|
||||||
stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty().also { entries ->
|
|
||||||
settingsRepo.setSatelliteTypeIds(type, entries.map { it.catnum })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val importedRadios = radioResults.flatMap { (url, stream) ->
|
|
||||||
stream?.let { dataParser.parseJSONStream(unwrapIfZipped(url, it)) }.orEmpty()
|
stream?.let { dataParser.parseJSONStream(unwrapIfZipped(url, it)) }.orEmpty()
|
||||||
}
|
}
|
||||||
// insert parsed data into the database
|
// insert parsed data into the database
|
||||||
|
|||||||
+25
-22
@@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class SatelliteRepo(
|
class SatelliteRepo(
|
||||||
private val dispatcher: CoroutineDispatcher,
|
private val dispatcher: CoroutineDispatcher,
|
||||||
@@ -73,7 +74,7 @@ class SatelliteRepo(
|
|||||||
aosStartMinute = settings.aosStartMinute,
|
aosStartMinute = settings.aosStartMinute,
|
||||||
aosEndMinute = settings.aosEndMinute,
|
aosEndMinute = settings.aosEndMinute,
|
||||||
invertAosTimeWindow = settings.invertAosTimeWindow,
|
invertAosTimeWindow = settings.invertAosTimeWindow,
|
||||||
modes = settings.selectedModes
|
modes = settingsRepo.selectedSatModes.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,8 +96,14 @@ class SatelliteRepo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRadios(satPos: OrbitalPos, radios: List<SatRadio>): List<SatRadio> {
|
override suspend fun getRadios(
|
||||||
|
sat: OrbitalObject,
|
||||||
|
pos: GeoPos,
|
||||||
|
radios: List<SatRadio>,
|
||||||
|
time: Long
|
||||||
|
): List<SatRadio> {
|
||||||
return withContext(dispatcher) {
|
return withContext(dispatcher) {
|
||||||
|
val satPos = sat.getPosition(pos, time)
|
||||||
radios.map { transmitter ->
|
radios.map { transmitter ->
|
||||||
transmitter.copy(
|
transmitter.copy(
|
||||||
downlinkLow = transmitter.downlinkLow?.let { satPos.getDownlinkFreq(it) },
|
downlinkLow = transmitter.downlinkLow?.let { satPos.getDownlinkFreq(it) },
|
||||||
@@ -152,7 +159,7 @@ class SatelliteRepo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
newPasses.sortBy { it.aosTime }
|
newPasses.sortBy { it.aosTime }
|
||||||
delay(1000) // Simulate loading time for better UX
|
delay(1000.milliseconds) // Simulate loading time for better UX
|
||||||
_passes.update { newPasses }
|
_passes.update { newPasses }
|
||||||
}
|
}
|
||||||
_isCalculating.value = false
|
_isCalculating.value = false
|
||||||
@@ -170,7 +177,7 @@ class SatelliteRepo(
|
|||||||
val inRange = if (aosStartMinute <= aosEndMinute) {
|
val inRange = if (aosStartMinute <= aosEndMinute) {
|
||||||
aosMinute in aosStartMinute..aosEndMinute
|
aosMinute in aosStartMinute..aosEndMinute
|
||||||
} else {
|
} else {
|
||||||
aosMinute >= aosStartMinute || aosMinute <= aosEndMinute
|
aosMinute !in (aosEndMinute + 1)..<aosStartMinute
|
||||||
}
|
}
|
||||||
return if (invertAosTimeWindow) !inRange else inRange
|
return if (invertAosTimeWindow) !inRange else inRange
|
||||||
}
|
}
|
||||||
@@ -236,16 +243,13 @@ class SatelliteRepo(
|
|||||||
if (elevation > maxElevation) maxElevation = elevation
|
if (elevation > maxElevation) maxElevation = elevation
|
||||||
} while (elevation < 0.0)
|
} while (elevation < 0.0)
|
||||||
|
|
||||||
// refine AOS to ~500ms precision via binary search.
|
// refine AOS to ~500ms precision
|
||||||
// Elevation is monotonic across the horizon crossing, so binary search
|
calendarTimeMillis -= 60L * 1000L
|
||||||
// finds the crossing in ~8 SGP4 calls instead of up to 120 linear steps.
|
do {
|
||||||
var aosLo = calendarTimeMillis - 60L * 1000L // elevation < 0 (below horizon)
|
calendarTimeMillis += 500L
|
||||||
var aosHi = calendarTimeMillis // elevation >= 0 (above horizon)
|
elevation = sat.getElevation(pos, calendarTimeMillis)
|
||||||
while (aosHi - aosLo > 500L) {
|
if (elevation > maxElevation) maxElevation = elevation
|
||||||
val mid = (aosLo + aosHi) / 2
|
} while (elevation < 0.0)
|
||||||
if (sat.getElevation(pos, mid) < 0.0) aosLo = mid else aosHi = mid
|
|
||||||
}
|
|
||||||
calendarTimeMillis = aosHi
|
|
||||||
|
|
||||||
// Get full position for AOS data (azimuth, altitude)
|
// Get full position for AOS data (azimuth, altitude)
|
||||||
val aosPos = sat.getFullPosition(pos, calendarTimeMillis)
|
val aosPos = sat.getFullPosition(pos, calendarTimeMillis)
|
||||||
@@ -259,14 +263,13 @@ class SatelliteRepo(
|
|||||||
if (elevation > maxElevation) maxElevation = elevation
|
if (elevation > maxElevation) maxElevation = elevation
|
||||||
} while (elevation > 0.0)
|
} while (elevation > 0.0)
|
||||||
|
|
||||||
// refine LOS to ~500ms precision via binary search (same monotonic argument)
|
// refine LOS to ~500ms precision
|
||||||
var losLo = calendarTimeMillis - 30L * 1000L // elevation > 0 (above horizon)
|
calendarTimeMillis -= 30L * 1000L
|
||||||
var losHi = calendarTimeMillis // elevation <= 0 (below horizon)
|
do {
|
||||||
while (losHi - losLo > 500L) {
|
calendarTimeMillis += 500L
|
||||||
val mid = (losLo + losHi) / 2
|
elevation = sat.getElevation(pos, calendarTimeMillis)
|
||||||
if (sat.getElevation(pos, mid) > 0.0) losLo = mid else losHi = mid
|
if (elevation > maxElevation) maxElevation = elevation
|
||||||
}
|
} while (elevation > 0.0)
|
||||||
calendarTimeMillis = losHi
|
|
||||||
|
|
||||||
// Get full position for LOS data (azimuth, altitude)
|
// Get full position for LOS data (azimuth, altitude)
|
||||||
val losPos = sat.getFullPosition(pos, calendarTimeMillis)
|
val losPos = sat.getFullPosition(pos, calendarTimeMillis)
|
||||||
|
|||||||
+16
-13
@@ -38,16 +38,16 @@ class SelectionRepo(
|
|||||||
) : ISelectionRepo {
|
) : ISelectionRepo {
|
||||||
|
|
||||||
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
|
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
|
||||||
private val currentTypes = MutableStateFlow(settingsRepo.selectedTypes.value)
|
|
||||||
private val currentQuery = MutableStateFlow("")
|
private val currentQuery = MutableStateFlow("")
|
||||||
|
|
||||||
// Resolve type IDs once when types change, then filter items reactively.
|
// Resolve sat IDs once when modes change, then filter items reactively.
|
||||||
// The HashSet gives O(1) catnum lookups instead of O(n) with a List.
|
// The HashSet gives O(1) catnum lookups instead of O(n) with a List.
|
||||||
private val itemsWithTypes = currentTypes.flatMapLatest { types: List<String> ->
|
// Directly observe settingsRepo.selectedSatModes to ensure real-time sync across screens.
|
||||||
val catnumSet: Set<Int>? = if (types.isEmpty()) {
|
private val itemsWithModes = settingsRepo.selectedSatModes.flatMapLatest { list: List<String> ->
|
||||||
|
val catnumSet: Set<Int>? = if (list.isEmpty()) {
|
||||||
null // null = no filtering
|
null // null = no filtering
|
||||||
} else {
|
} else {
|
||||||
val ids = settingsRepo.getSatelliteTypesIds(types)
|
val ids = localSource.getIdsWithModes(list)
|
||||||
if (ids.isEmpty()) null else ids.toHashSet()
|
if (ids.isEmpty()) null else ids.toHashSet()
|
||||||
}
|
}
|
||||||
currentItems.map { items ->
|
currentItems.map { items ->
|
||||||
@@ -56,14 +56,18 @@ class SelectionRepo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val itemsWithQuery = currentQuery.flatMapLatest { query ->
|
private val itemsWithQuery = currentQuery.flatMapLatest { query ->
|
||||||
itemsWithTypes.map { items -> filterByQuery(items, query) }
|
itemsWithModes.map { items ->
|
||||||
|
filterByQuery(items, query).sortedWith(
|
||||||
|
compareByDescending<SatItem> { it.isSelected }
|
||||||
|
.thenBy { it.name }
|
||||||
|
.thenBy { it.catnum }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentTypes() = currentTypes.value
|
override fun getCurrentModes() = settingsRepo.selectedSatModes.value
|
||||||
|
|
||||||
override fun getTypesList() = Sources.satelliteDataUrls.keys.sorted().toMutableList().apply {
|
override fun getModesList() = Sources.satelliteModes
|
||||||
removeAt(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getEntriesFlow() = withContext(dispatcher) {
|
override suspend fun getEntriesFlow() = withContext(dispatcher) {
|
||||||
val selectedIds = settingsRepo.selectedIds.value.toHashSet()
|
val selectedIds = settingsRepo.selectedIds.value.toHashSet()
|
||||||
@@ -73,9 +77,8 @@ class SelectionRepo(
|
|||||||
return@withContext itemsWithQuery
|
return@withContext itemsWithQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setTypes(types: List<String>) {
|
override suspend fun setModes(modes: List<String>) {
|
||||||
currentTypes.value = types
|
settingsRepo.setSelectedSatModes(modes)
|
||||||
settingsRepo.setSelectedTypes(types)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setQuery(query: String) {
|
override suspend fun setQuery(query: String) {
|
||||||
|
|||||||
+39
-106
@@ -29,7 +29,6 @@ import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
|||||||
import com.rtbishop.look4sat.core.domain.model.PassesSettings
|
import com.rtbishop.look4sat.core.domain.model.PassesSettings
|
||||||
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
||||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||||
import com.rtbishop.look4sat.core.domain.source.Sources
|
|
||||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||||
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
||||||
@@ -40,7 +39,6 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
class SettingsRepo(
|
class SettingsRepo(
|
||||||
private val context: android.content.Context,
|
|
||||||
private val locationManager: LocationManager,
|
private val locationManager: LocationManager,
|
||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
override val appVersionName: String
|
override val appVersionName: String
|
||||||
@@ -70,16 +68,13 @@ class SettingsRepo(
|
|||||||
private val keyFrequencyPort = "frequencyPort"
|
private val keyFrequencyPort = "frequencyPort"
|
||||||
private val keyFrequencyFormat = "frequencyFormat"
|
private val keyFrequencyFormat = "frequencyFormat"
|
||||||
private val keySelectedIds = "selectedIds"
|
private val keySelectedIds = "selectedIds"
|
||||||
private val keySelectedTypes = "selectedTypes"
|
private val keySelectedSatModes = "selectedSatModes"
|
||||||
private val keySelectedModes = "selectedModes"
|
|
||||||
private val keyStateOfAutoUpdate = "stateOfAutoUpdate"
|
private val keyStateOfAutoUpdate = "stateOfAutoUpdate"
|
||||||
private val keyStateOfSensors = "stateOfSensors"
|
private val keyStateOfSensors = "stateOfSensors"
|
||||||
private val keyStateOfSweep = "stateOfSweep"
|
private val keyStateOfSweep = "stateOfSweep"
|
||||||
private val keyStateOfUtc = "stateOfUtc"
|
private val keyStateOfUtc = "stateOfUtc"
|
||||||
private val keyStateOfLightTheme = "stateOfLightTheme"
|
private val keyStateOfLightTheme = "stateOfLightTheme"
|
||||||
private val keyStateOfNightMode = "stateOfNightMode"
|
private val keyStateOfNightMode = "stateOfNightMode"
|
||||||
private val keyHiddenScreens = "hiddenScreens"
|
|
||||||
private val keyScreenOrder = "screenOrder"
|
|
||||||
private val keyStationAltitude = "stationAltitude"
|
private val keyStationAltitude = "stationAltitude"
|
||||||
private val keyStationLatitude = "stationLatitude"
|
private val keyStationLatitude = "stationLatitude"
|
||||||
private val keyStationLongitude = "stationLongitude"
|
private val keyStationLongitude = "stationLongitude"
|
||||||
@@ -91,22 +86,19 @@ class SettingsRepo(
|
|||||||
private val keySstvMode = "sstvMode"
|
private val keySstvMode = "sstvMode"
|
||||||
private val keyLowElevation = "lowElevation"
|
private val keyLowElevation = "lowElevation"
|
||||||
private val keyHighElevation = "highElevation"
|
private val keyHighElevation = "highElevation"
|
||||||
|
private val keyRadarCompassOffset = "radarCompassOffset"
|
||||||
|
private val keyRadarCompassOffsetElev = "radarCompassOffsetElev"
|
||||||
private val keyUseCustomTle = "useCustomTle"
|
private val keyUseCustomTle = "useCustomTle"
|
||||||
private val keyUseCustomTransceivers = "useCustomTransceivers"
|
private val keyUseCustomTransceivers = "useCustomTransceivers"
|
||||||
private val keyTleUrl = "tleUrl"
|
private val keyTleUrl = "tleUrl"
|
||||||
private val keyTransceiversUrl = "transceiversUrl"
|
private val keyTransceiversUrl = "transceiversUrl"
|
||||||
private val keySubMenuOrder = "subMenuOrder"
|
|
||||||
private val keyWavelogUrl = "wavelogUrl"
|
|
||||||
private val keyWavelogApiKey = "wavelogApiKey"
|
|
||||||
private val keyWavelogStationId = "wavelogStationId"
|
|
||||||
private val keyWavelogAutoUpload = "wavelogAutoUpload"
|
|
||||||
private val separatorComma = ","
|
private val separatorComma = ","
|
||||||
|
|
||||||
//region # Satellites selection settings
|
//region # Satellites selection settings
|
||||||
private val _satelliteSelection = MutableStateFlow(getSelectedIds())
|
private val _satelliteSelection = MutableStateFlow(getSelectedIds())
|
||||||
private val _typesSelection = MutableStateFlow(getSelectedTypes())
|
private val _satelliteModeSelection = MutableStateFlow(getSelectedSatModes())
|
||||||
override val selectedIds: StateFlow<List<Int>> = _satelliteSelection
|
override val selectedIds: StateFlow<List<Int>> = _satelliteSelection
|
||||||
override val selectedTypes: StateFlow<List<String>> = _typesSelection
|
override val selectedSatModes: StateFlow<List<String>> = _satelliteModeSelection
|
||||||
|
|
||||||
override fun setSelectedIds(ids: List<Int>) {
|
override fun setSelectedIds(ids: List<Int>) {
|
||||||
val selectionString = ids.joinToString(separatorComma)
|
val selectionString = ids.joinToString(separatorComma)
|
||||||
@@ -114,10 +106,10 @@ class SettingsRepo(
|
|||||||
_satelliteSelection.value = ids
|
_satelliteSelection.value = ids
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setSelectedTypes(types: List<String>) {
|
override fun setSelectedSatModes(modes: List<String>) {
|
||||||
val typesString = types.joinToString(separatorComma)
|
val modesString = modes.joinToString(separatorComma)
|
||||||
preferences.edit { putString(keySelectedTypes, typesString) }
|
preferences.edit { putString(keySelectedSatModes, modesString) }
|
||||||
_typesSelection.value = types
|
_satelliteModeSelection.value = modes
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSelectedIds(): List<Int> {
|
private fun getSelectedIds(): List<Int> {
|
||||||
@@ -126,10 +118,10 @@ class SettingsRepo(
|
|||||||
return selectionString.split(separatorComma).map { it.toInt() }
|
return selectionString.split(separatorComma).map { it.toInt() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSelectedTypes(): List<String> {
|
private fun getSelectedSatModes(): List<String> {
|
||||||
val typesString = preferences.getString(keySelectedTypes, "Amateur")
|
val modesString = preferences.getString(keySelectedSatModes, null)
|
||||||
if (typesString.isNullOrEmpty()) return emptyList()
|
if (modesString.isNullOrEmpty()) return emptyList()
|
||||||
return typesString.split(separatorComma)
|
return modesString.split(separatorComma).sorted()
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
@@ -144,7 +136,6 @@ class SettingsRepo(
|
|||||||
putInt(keyFilterAosStartMinute, settings.aosStartMinute)
|
putInt(keyFilterAosStartMinute, settings.aosStartMinute)
|
||||||
putInt(keyFilterAosEndMinute, settings.aosEndMinute)
|
putInt(keyFilterAosEndMinute, settings.aosEndMinute)
|
||||||
putBoolean(keyFilterAosInvert, settings.invertAosTimeWindow)
|
putBoolean(keyFilterAosInvert, settings.invertAosTimeWindow)
|
||||||
putString(keySelectedModes, settings.selectedModes.joinToString(separatorComma))
|
|
||||||
_passesSettings.value = settings
|
_passesSettings.value = settings
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,16 +146,13 @@ class SettingsRepo(
|
|||||||
val aosStartMinute = preferences.getInt(keyFilterAosStartMinute, 0).coerceIn(0, 23 * 60 + 59)
|
val aosStartMinute = preferences.getInt(keyFilterAosStartMinute, 0).coerceIn(0, 23 * 60 + 59)
|
||||||
val aosEndMinute = preferences.getInt(keyFilterAosEndMinute, 23 * 60 + 59).coerceIn(0, 23 * 60 + 59)
|
val aosEndMinute = preferences.getInt(keyFilterAosEndMinute, 23 * 60 + 59).coerceIn(0, 23 * 60 + 59)
|
||||||
val invertAosTimeWindow = preferences.getBoolean(keyFilterAosInvert, false)
|
val invertAosTimeWindow = preferences.getBoolean(keyFilterAosInvert, false)
|
||||||
val selectedModesString = preferences.getString(keySelectedModes, null)
|
|
||||||
val selectedModes = selectedModesString?.split(separatorComma)?.sorted() ?: emptyList()
|
|
||||||
return PassesSettings(
|
return PassesSettings(
|
||||||
showDeepSpace,
|
showDeepSpace,
|
||||||
hoursAhead,
|
hoursAhead,
|
||||||
minElevation,
|
minElevation,
|
||||||
aosStartMinute,
|
aosStartMinute,
|
||||||
aosEndMinute,
|
aosEndMinute,
|
||||||
invertAosTimeWindow,
|
invertAosTimeWindow
|
||||||
selectedModes
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
@@ -187,41 +175,23 @@ class SettingsRepo(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GPS 定位: 一次性 getCurrentLocation(GPS 优先, 15 秒超时), 拿到位置才返回 true */
|
override fun setStationPosition(): Boolean {
|
||||||
override suspend fun setStationPosition(): Boolean {
|
|
||||||
// 权限前置: 无定位权限直接失败(不吞异常)
|
|
||||||
if (androidx.core.content.ContextCompat.checkSelfPermission(
|
|
||||||
context, android.Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
) != android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
println("GPS: no fine location permission")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
|
if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
|
||||||
return kotlinx.coroutines.suspendCancellableCoroutine { cont ->
|
try {
|
||||||
val signal = android.os.CancellationSignal()
|
|
||||||
val handler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
||||||
val executor = java.util.concurrent.Executor { handler.post(it) }
|
|
||||||
// 15 秒超时
|
|
||||||
val timeout = handler.postDelayed({
|
|
||||||
signal.cancel()
|
|
||||||
if (cont.isActive) cont.resume(false) { }
|
|
||||||
}, 15_000L)
|
|
||||||
val listener = androidx.core.util.Consumer<Location> { location ->
|
|
||||||
handler.removeCallbacksAndMessages(null)
|
|
||||||
setStationPosition(location.latitude, location.longitude, location.altitude)
|
|
||||||
if (cont.isActive) cont.resume(true) { }
|
|
||||||
}
|
|
||||||
val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps)
|
val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps)
|
||||||
val provider = if (hasGps) providerGps else providerNet
|
val hasNet = LocationManagerCompat.hasProvider(locationManager, providerNet)
|
||||||
try {
|
val provider = if (hasGps) providerGps else if (hasNet) providerNet else providerDef
|
||||||
LocationManagerCompat.getCurrentLocation(locationManager, provider, signal, executor, listener)
|
val location = locationManager.getLastKnownLocation(providerDef)
|
||||||
} catch (exception: SecurityException) {
|
if (location == null || System.currentTimeMillis() - location.time > 600_000L) {
|
||||||
handler.removeCallbacksAndMessages(null)
|
println("Requesting location for $provider provider")
|
||||||
if (cont.isActive) cont.resume(false) { }
|
locationManager.requestLocationUpdates(provider, 0L, 0f, this)
|
||||||
|
} else {
|
||||||
|
setStationPosition(location.latitude, location.longitude, location.altitude)
|
||||||
}
|
}
|
||||||
cont.invokeOnCancellation { signal.cancel(); handler.removeCallbacksAndMessages(null) }
|
} catch (exception: SecurityException) {
|
||||||
|
println("No permissions were given - $exception")
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setStationPosition(locator: String): Boolean {
|
override fun setStationPosition(locator: String): Boolean {
|
||||||
@@ -240,8 +210,8 @@ class SettingsRepo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setStationPosition(latitude: Double, longitude: Double, altitude: Double, locator: String) {
|
private fun setStationPosition(latitude: Double, longitude: Double, altitude: Double, locator: String) {
|
||||||
val newLat = latitude.round(5)
|
val newLat = latitude.round(4)
|
||||||
val newLon = longitude.round(5)
|
val newLon = longitude.round(4)
|
||||||
val newAlt = altitude.round(1)
|
val newAlt = altitude.round(1)
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
println("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
|
println("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
|
||||||
@@ -262,25 +232,6 @@ class SettingsRepo(
|
|||||||
private val _databaseState = MutableStateFlow(getDatabaseState())
|
private val _databaseState = MutableStateFlow(getDatabaseState())
|
||||||
override val databaseState: StateFlow<DatabaseState> = _databaseState
|
override val databaseState: StateFlow<DatabaseState> = _databaseState
|
||||||
|
|
||||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> {
|
|
||||||
val idsSet = mutableSetOf<Int>()
|
|
||||||
types.forEach { type ->
|
|
||||||
val typeString = preferences.getString("type$type", null)
|
|
||||||
val typeIds = if (typeString.isNullOrBlank()) {
|
|
||||||
emptyList()
|
|
||||||
} else {
|
|
||||||
typeString.split(separatorComma).map { it.toInt() }
|
|
||||||
}
|
|
||||||
idsSet.addAll(typeIds)
|
|
||||||
}
|
|
||||||
return idsSet.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
|
|
||||||
if (type == "All") return
|
|
||||||
val typesString = ids.joinToString(separatorComma)
|
|
||||||
preferences.edit { putString("type$type", typesString) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateDatabaseState(state: DatabaseState) = preferences.edit {
|
override fun updateDatabaseState(state: DatabaseState) = preferences.edit {
|
||||||
putInt(keyNumberOfSatellites, state.numberOfSatellites)
|
putInt(keyNumberOfSatellites, state.numberOfSatellites)
|
||||||
@@ -383,13 +334,8 @@ class SettingsRepo(
|
|||||||
putString(keySstvMode, new.sstvMode)
|
putString(keySstvMode, new.sstvMode)
|
||||||
putLong(keyLowElevation, new.lowElevation.toRawBits())
|
putLong(keyLowElevation, new.lowElevation.toRawBits())
|
||||||
putLong(keyHighElevation, new.highElevation.toRawBits())
|
putLong(keyHighElevation, new.highElevation.toRawBits())
|
||||||
putStringSet(keyHiddenScreens, new.hiddenScreens.toSet())
|
putFloat(keyRadarCompassOffset, new.radarCompassOffset)
|
||||||
putString(keyScreenOrder, new.screenOrder.joinToString(","))
|
putFloat(keyRadarCompassOffsetElev, new.radarCompassOffsetElev)
|
||||||
putString(keySubMenuOrder, new.subMenuOrder.joinToString(","))
|
|
||||||
putString(keyWavelogUrl, new.wavelogUrl)
|
|
||||||
putString(keyWavelogApiKey, new.wavelogApiKey)
|
|
||||||
putString(keyWavelogStationId, new.wavelogStationId)
|
|
||||||
putBoolean(keyWavelogAutoUpload, new.wavelogAutoUpload)
|
|
||||||
}
|
}
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
@@ -405,15 +351,10 @@ class SettingsRepo(
|
|||||||
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true),
|
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true),
|
||||||
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true),
|
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true),
|
||||||
sstvMode = preferences.getString(keySstvMode, null) ?: "Auto",
|
sstvMode = preferences.getString(keySstvMode, null) ?: "Auto",
|
||||||
hiddenScreens = preferences.getStringSet(keyHiddenScreens, emptySet())?.toList() ?: emptyList(),
|
|
||||||
screenOrder = preferences.getString(keyScreenOrder, null)?.split(",")?.filter { it.isNotBlank() } ?: emptyList(),
|
|
||||||
subMenuOrder = preferences.getString(keySubMenuOrder, null)?.split(",")?.filter { it.isNotBlank() } ?: emptyList(),
|
|
||||||
lowElevation = Double.fromBits(preferences.getLong(keyLowElevation, 15.0.toRawBits())),
|
lowElevation = Double.fromBits(preferences.getLong(keyLowElevation, 15.0.toRawBits())),
|
||||||
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits())),
|
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits())),
|
||||||
wavelogUrl = preferences.getString(keyWavelogUrl, null) ?: "",
|
radarCompassOffset = preferences.getFloat(keyRadarCompassOffset, 0f),
|
||||||
wavelogApiKey = preferences.getString(keyWavelogApiKey, null) ?: "",
|
radarCompassOffsetElev = preferences.getFloat(keyRadarCompassOffsetElev, 0f)
|
||||||
wavelogStationId = preferences.getString(keyWavelogStationId, null) ?: "",
|
|
||||||
wavelogAutoUpload = preferences.getBoolean(keyWavelogAutoUpload, false)
|
|
||||||
)
|
)
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
@@ -431,20 +372,12 @@ class SettingsRepo(
|
|||||||
_dataSourcesSettings.value = settings
|
_dataSourcesSettings.value = settings
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDataSourcesSettings(): DataSourcesSettings {
|
private fun getDataSourcesSettings(): DataSourcesSettings = DataSourcesSettings(
|
||||||
// 4.4.8 修复: 旧版 example.com 占位 URL 视为未配置 -> 替换为真实默认 URL 且开关强制关闭,
|
useCustomTLE = preferences.getBoolean(keyUseCustomTle, false),
|
||||||
// 否则在线更新的 All/SatNOGS 源会指向错误地址导致更新失败
|
useCustomTransceivers = preferences.getBoolean(keyUseCustomTransceivers, false),
|
||||||
val storedTleUrl = preferences.getString(keyTleUrl, Sources.defaultTleUrl) ?: Sources.defaultTleUrl
|
tleUrl = preferences.getString(keyTleUrl, "https://example.com/tle.txt") ?: "",
|
||||||
val storedTxUrl = preferences.getString(keyTransceiversUrl, Sources.defaultTransceiversUrl) ?: Sources.defaultTransceiversUrl
|
transceiversUrl = preferences.getString(keyTransceiversUrl, "https://example.com/radio.json") ?: ""
|
||||||
val tleUrl = if (storedTleUrl == "https://example.com/tle.txt") Sources.defaultTleUrl else storedTleUrl
|
)
|
||||||
val txUrl = if (storedTxUrl == "https://example.com/radio.json") Sources.defaultTransceiversUrl else storedTxUrl
|
|
||||||
return DataSourcesSettings(
|
|
||||||
useCustomTLE = preferences.getBoolean(keyUseCustomTle, false) && tleUrl != Sources.defaultTleUrl,
|
|
||||||
useCustomTransceivers = preferences.getBoolean(keyUseCustomTransceivers, false) && txUrl != Sources.defaultTransceiversUrl,
|
|
||||||
tleUrl = tleUrl,
|
|
||||||
transceiversUrl = txUrl
|
|
||||||
)
|
|
||||||
}
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region # Radio control settings
|
//region # Radio control settings
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
package com.rtbishop.look4sat.core.data.source
|
|
||||||
|
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatDay
|
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatReport
|
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatSlot
|
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatStatus
|
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AMSAT 卫星状态页解析器(https://amsat.org/status/)
|
|
||||||
*
|
|
||||||
* 页面结构(静态 HTML, 2026-08 实测):
|
|
||||||
* - 状态表: <table> 内 48 行, 表头 = Name + 6 天(每个 colspan=12)
|
|
||||||
* 数据行 73 格: [0]=卫星名, [1..72] = 6 天 × 12 个 2 小时槽(新→旧)
|
|
||||||
* 每格 <td width=9 bgcolor="颜色">数字</td>, 有报告时数字带
|
|
||||||
* docTips.show('id') 链接
|
|
||||||
* - 报告详情在页面内嵌 JS:
|
|
||||||
* tips.a885153 = new Array(5,5,120,'状态<br>呼号<br>网格<br>日期<br>时间段 UTC')
|
|
||||||
*
|
|
||||||
* 状态色: #648fff=活跃, #ffb000=仅遥测, #dc267f=未听到, #fe6100=矛盾
|
|
||||||
*/
|
|
||||||
object AmSatParser {
|
|
||||||
|
|
||||||
private val STATUS_COLORS = mapOf(
|
|
||||||
"#648fff" to 0xFF648FFF.toLong(),
|
|
||||||
"#ffb000" to 0xFFFFB000.toLong(),
|
|
||||||
"#dc267f" to 0xFFDC267F.toLong(),
|
|
||||||
"#fe6100" to 0xFFFE6100.toLong()
|
|
||||||
)
|
|
||||||
private val GRAY = 0xFFC0C0C0.toLong()
|
|
||||||
|
|
||||||
private val rowRe = Pattern.compile("<tr>(.*?)</tr>", Pattern.DOTALL)
|
|
||||||
private val cellRe = Pattern.compile("(<t[dh][^>]*>.*?</t[dh]>)", Pattern.DOTALL)
|
|
||||||
private val bgRe = Pattern.compile("bgcolor=\"?(#[0-9a-fA-F]{6}|C0C0C0)\"?")
|
|
||||||
private val linkRe = Pattern.compile("docTips\\.show\\('(a\\d+)'\\)")
|
|
||||||
private val tipRe = Pattern.compile(
|
|
||||||
"tips\\.(a\\d+)\\s*=\\s*new\\s+Array\\(\\s*\\d+,\\s*\\d+,\\s*\\d+,\\s*'([^']*)'"
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 解析完整页面 */
|
|
||||||
fun parse(html: String, fetchedAtUtcMs: Long): SatStatusPage {
|
|
||||||
val reports = parseReports(html)
|
|
||||||
val statuses = parseStatusTable(html, reports)
|
|
||||||
return SatStatusPage(fetchedAtUtcMs, statuses, reports)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 提取全部报告(tips.* JS 数组) */
|
|
||||||
fun parseReports(html: String): Map<String, SatReport> {
|
|
||||||
val map = mutableMapOf<String, SatReport>()
|
|
||||||
val m = tipRe.matcher(html)
|
|
||||||
while (m.find()) {
|
|
||||||
val id = m.group(1)
|
|
||||||
val parts = m.group(2).split("<br>")
|
|
||||||
map[id] = SatReport(
|
|
||||||
id = id,
|
|
||||||
statusText = parts.getOrElse(0) { "" }.trim(),
|
|
||||||
call = parts.getOrElse(1) { "" }.trim(),
|
|
||||||
grid = parts.getOrElse(2) { "" }.trim(),
|
|
||||||
dateUtc = parts.getOrElse(3) { "" }.trim(),
|
|
||||||
timeUtc = parts.getOrElse(4) { "" }.trim()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 解析状态表(48 行卫星) */
|
|
||||||
fun parseStatusTable(html: String, reports: Map<String, SatReport>): List<SatStatus> {
|
|
||||||
val result = mutableListOf<SatStatus>()
|
|
||||||
val tables = extractTables(html)
|
|
||||||
for (table in tables) {
|
|
||||||
val rows = rowRe.matcher(table)
|
|
||||||
val parsed = mutableListOf<SatStatus>()
|
|
||||||
var rowIndex = 0
|
|
||||||
var dayHeaders: List<String> = emptyList()
|
|
||||||
while (rows.find()) {
|
|
||||||
val rowHtml = rows.group(1)
|
|
||||||
val cells = cellRe.matcher(rowHtml)
|
|
||||||
val cellList = mutableListOf<String>()
|
|
||||||
while (cells.find()) cellList.add(cells.group(1))
|
|
||||||
if (rowIndex == 0) {
|
|
||||||
// 表头: Name + 6 天(每个 colspan=12)
|
|
||||||
dayHeaders = cellList.drop(1).take(6).map { stripHtml(it) }
|
|
||||||
rowIndex++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (cellList.size < 7) { rowIndex++; continue }
|
|
||||||
val name = stripHtml(cellList[0])
|
|
||||||
if (name.isBlank()) { rowIndex++; continue }
|
|
||||||
val days = mutableListOf<SatDay>()
|
|
||||||
for (d in 0 until 6) {
|
|
||||||
val start = 1 + d * 12
|
|
||||||
val end = start + 12
|
|
||||||
val slots = (start until end).mapNotNull { i ->
|
|
||||||
cellList.getOrNull(i)?.let { cell ->
|
|
||||||
val bg = bgRe.matcher(cell)
|
|
||||||
val color = if (bg.find()) {
|
|
||||||
STATUS_COLORS[bg.group(1).lowercase()] ?: GRAY
|
|
||||||
} else GRAY
|
|
||||||
val link = linkRe.matcher(cell)
|
|
||||||
val ids = mutableListOf<String>()
|
|
||||||
while (link.find()) ids.add(link.group(1))
|
|
||||||
val hasReport = ids.isNotEmpty()
|
|
||||||
val count = if (hasReport) {
|
|
||||||
stripHtml(cell).trim().toIntOrNull() ?: ids.size
|
|
||||||
} else 0
|
|
||||||
SatSlot(
|
|
||||||
statusColor = if (hasReport) color else GRAY,
|
|
||||||
count = count,
|
|
||||||
reportIds = ids
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
days.add(SatDay(dayHeaders.getOrElse(d) { "" }, slots))
|
|
||||||
}
|
|
||||||
parsed.add(SatStatus(name, days))
|
|
||||||
rowIndex++
|
|
||||||
}
|
|
||||||
if (parsed.isNotEmpty()) {
|
|
||||||
result.addAll(parsed)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractTables(html: String): List<String> {
|
|
||||||
val result = mutableListOf<String>()
|
|
||||||
val m = Pattern.compile("<table.*?</table>", Pattern.DOTALL).matcher(html)
|
|
||||||
while (m.find()) result.add(m.group())
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stripHtml(s: String): String =
|
|
||||||
s.replace(Regex("<[^>]+>"), "").trim()
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,6 @@ import kotlinx.coroutines.CoroutineDispatcher
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
class RemoteSource(
|
class RemoteSource(
|
||||||
@@ -43,32 +42,52 @@ class RemoteSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStatusHtml(): String? = withContext(dispatcher) {
|
|
||||||
try {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("https://amsat.org/status/")
|
|
||||||
.header("User-Agent", "Mozilla/5.0 (Linux; Android 13) Look4Sat/4.5")
|
|
||||||
.build()
|
|
||||||
httpClient.newCall(request).execute().use { response ->
|
|
||||||
if (!response.isSuccessful) return@use null
|
|
||||||
response.body?.string()
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
println("RemoteSource amsat status exception: $exception")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
|
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
|
||||||
try {
|
try {
|
||||||
val networkRequest = Request.Builder().url(url).build()
|
val networkRequest = Request.Builder().url(url).build()
|
||||||
httpClient.newCall(networkRequest).execute().use { response ->
|
val response = httpClient.newCall(networkRequest).execute()
|
||||||
if (!response.isSuccessful) return@withContext null
|
if (!response.isSuccessful) {
|
||||||
ByteArrayInputStream(response.body.bytes())
|
response.close()
|
||||||
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
// Return the body stream directly as the caller is responsible for closing it
|
||||||
|
// That returns the connection to OkHttp's pool
|
||||||
|
response.body.byteStream().buffered()
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
println("RemoteSource network stream exception: $exception")
|
println("RemoteSource network stream exception: $exception")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getAmSatCatalog(): String? = withContext(dispatcher) {
|
||||||
|
try {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("https://www.amsat.org/status/api/v1/catalog.php")
|
||||||
|
.header("User-Agent", "Look4Sat")
|
||||||
|
.build()
|
||||||
|
httpClient.newCall(request).execute().use { response ->
|
||||||
|
if (!response.isSuccessful) return@use null
|
||||||
|
response.body.string()
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
println("RemoteSource getAmSatCatalog exception: $exception")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAmSatReports(hours: Int, limit: Int): String? = withContext(dispatcher) {
|
||||||
|
try {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("https://www.amsat.org/status/api/v1/reports.php?hours=$hours&limit=$limit")
|
||||||
|
.header("User-Agent", "Look4Sat")
|
||||||
|
.build()
|
||||||
|
httpClient.newCall(request).execute().use { response ->
|
||||||
|
if (!response.isSuccessful) return@use null
|
||||||
|
response.body.string()
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
println("RemoteSource getAmSatReports exception: $exception")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,4 @@ class ShowToast(private val context: Context) : IShowToast {
|
|||||||
override fun invoke(message: String) {
|
override fun invoke(message: String) {
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invoke(resId: Int) {
|
|
||||||
invoke(context.getString(resId))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+3
-13
@@ -62,7 +62,6 @@ class DatabaseRepoTest {
|
|||||||
|
|
||||||
assertEquals(1, localSource.insertedEntries.size)
|
assertEquals(1, localSource.insertedEntries.size)
|
||||||
assertEquals(25544, localSource.insertedEntries.first().catnum)
|
assertEquals(25544, localSource.insertedEntries.first().catnum)
|
||||||
assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["Other"])
|
|
||||||
assertTrue(settingsRepo.databaseState.value.numberOfSatellites > 0)
|
assertTrue(settingsRepo.databaseState.value.numberOfSatellites > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +101,6 @@ class DatabaseRepoTest {
|
|||||||
repository.updateFromRemote()
|
repository.updateFromRemote()
|
||||||
|
|
||||||
assertTrue(localSource.insertedEntries.any { it.catnum == 25544 })
|
assertTrue(localSource.insertedEntries.any { it.catnum == 25544 })
|
||||||
// 新语义: 开关开 + URL 非空 -> All 源用自定义 URL, 数据归入 All 类型
|
|
||||||
assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["All"])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validCsvStream(): InputStream = """
|
private fun validCsvStream(): InputStream = """
|
||||||
@@ -166,10 +163,10 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
|
|||||||
|
|
||||||
override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList())
|
override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
override val selectedTypes: StateFlow<List<String>> = MutableStateFlow(emptyList())
|
override val selectedSatModes: StateFlow<List<String>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
override val passesSettings: StateFlow<PassesSettings> = MutableStateFlow(
|
override val passesSettings: StateFlow<PassesSettings> = MutableStateFlow(
|
||||||
PassesSettings(hoursAhead = 24, minElevation = 0.0, selectedModes = emptyList())
|
PassesSettings(hoursAhead = 24, minElevation = 0.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
override val stationPosition: StateFlow<GeoPos> = MutableStateFlow(GeoPos(0.0, 0.0))
|
override val stationPosition: StateFlow<GeoPos> = MutableStateFlow(GeoPos(0.0, 0.0))
|
||||||
@@ -190,11 +187,9 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
|
|||||||
RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600)
|
RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600)
|
||||||
)
|
)
|
||||||
|
|
||||||
val satelliteTypeIdsByType = mutableMapOf<String, List<Int>>()
|
|
||||||
|
|
||||||
override fun setSelectedIds(ids: List<Int>) = Unit
|
override fun setSelectedIds(ids: List<Int>) = Unit
|
||||||
|
|
||||||
override fun setSelectedTypes(types: List<String>) = Unit
|
override fun setSelectedSatModes(modes: List<String>) = Unit
|
||||||
|
|
||||||
override fun setPassesSettings(settings: PassesSettings) = Unit
|
override fun setPassesSettings(settings: PassesSettings) = Unit
|
||||||
|
|
||||||
@@ -204,11 +199,6 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
|
|||||||
|
|
||||||
override fun setStationPosition(locator: String): Boolean = true
|
override fun setStationPosition(locator: String): Boolean = true
|
||||||
|
|
||||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> = emptyList()
|
|
||||||
|
|
||||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
|
|
||||||
satelliteTypeIdsByType[type] = ids
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateDatabaseState(state: DatabaseState) {
|
override fun updateDatabaseState(state: DatabaseState) {
|
||||||
databaseState.value = state
|
databaseState.value = state
|
||||||
|
|||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.rtbishop.look4sat.core.data.repository
|
||||||
|
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.DatabaseState
|
||||||
|
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.SatItem
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||||
|
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||||
|
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||||
|
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class SelectionRepoTest {
|
||||||
|
|
||||||
|
private val dispatcher = StandardTestDispatcher()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unknown mode values do not crash and do not filter out entries`() = runTest(dispatcher) {
|
||||||
|
val localSource = FakeLocalSource(
|
||||||
|
entries = listOf(
|
||||||
|
SatItem(25544, "ISS (ZARYA)", false),
|
||||||
|
SatItem(40967, "TIANGONG", false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val settingsRepo = FakeSettingsRepo(selectedModes = listOf("REMOVED_MODE"))
|
||||||
|
val repository = SelectionRepo(dispatcher, localSource, settingsRepo)
|
||||||
|
|
||||||
|
val flow = repository.getEntriesFlow()
|
||||||
|
repository.setModes(listOf("REMOVED_MODE"))
|
||||||
|
|
||||||
|
val items = flow.first()
|
||||||
|
|
||||||
|
assertEquals(listOf(25544, 40967), items.map { it.catnum })
|
||||||
|
assertEquals(listOf("REMOVED_MODE"), repository.getCurrentModes())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `selected satellites are shown first`() = runTest(dispatcher) {
|
||||||
|
val localSource = FakeLocalSource(
|
||||||
|
entries = listOf(
|
||||||
|
SatItem(44444, "Zeta", false),
|
||||||
|
SatItem(25544, "Alpha", false),
|
||||||
|
SatItem(40967, "Beta", false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val settingsRepo = FakeSettingsRepo(selectedModes = emptyList())
|
||||||
|
val repository = SelectionRepo(dispatcher, localSource, settingsRepo)
|
||||||
|
|
||||||
|
val flow = repository.getEntriesFlow()
|
||||||
|
repository.setSelection(listOf(40967), true)
|
||||||
|
|
||||||
|
val items = flow.first()
|
||||||
|
|
||||||
|
assertEquals(listOf(40967, 25544, 44444), items.map { it.catnum })
|
||||||
|
assertEquals(listOf(true, false, false), items.map { it.isSelected })
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeLocalSource(
|
||||||
|
private val entries: List<SatItem>
|
||||||
|
) : ILocalSource {
|
||||||
|
override suspend fun getEntriesTotal(): Int = entries.size
|
||||||
|
|
||||||
|
override suspend fun getEntriesList(): List<SatItem> = entries
|
||||||
|
|
||||||
|
override suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject> = emptyList()
|
||||||
|
|
||||||
|
override suspend fun insertEntries(entries: List<com.rtbishop.look4sat.core.domain.predict.OrbitalData>) = Unit
|
||||||
|
|
||||||
|
override suspend fun deleteEntries() = Unit
|
||||||
|
|
||||||
|
override suspend fun getIdsWithModes(modes: List<String>): List<Int> = emptyList()
|
||||||
|
|
||||||
|
override suspend fun getRadiosTotal(): Int = 0
|
||||||
|
|
||||||
|
override suspend fun getRadiosWithId(id: Int): List<SatRadio> = emptyList()
|
||||||
|
|
||||||
|
override suspend fun insertRadios(radios: List<SatRadio>) = Unit
|
||||||
|
|
||||||
|
override suspend fun deleteRadios() = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeSettingsRepo(
|
||||||
|
selectedModes: List<String>
|
||||||
|
) : ISettingsRepo {
|
||||||
|
|
||||||
|
override val appVersionName: String = "test"
|
||||||
|
|
||||||
|
override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
|
override val selectedSatModes: MutableStateFlow<List<String>> = MutableStateFlow(selectedModes)
|
||||||
|
|
||||||
|
override val passesSettings: StateFlow<PassesSettings> = MutableStateFlow(
|
||||||
|
PassesSettings(hoursAhead = 24, minElevation = 0.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val stationPosition: StateFlow<GeoPos> = MutableStateFlow(GeoPos(0.0, 0.0))
|
||||||
|
|
||||||
|
override val databaseState: MutableStateFlow<DatabaseState> = MutableStateFlow(DatabaseState(0, 0, 0L))
|
||||||
|
|
||||||
|
override val rcSettings: StateFlow<RCSettings> = MutableStateFlow(
|
||||||
|
RCSettings(false, "", "", "", false, "", "", "", false, "", "", "", false, "", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
override val otherSettings: StateFlow<OtherSettings> = MutableStateFlow(
|
||||||
|
OtherSettings(false, false, false, false, false, false, false, false)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val dataSourcesSettings: StateFlow<DataSourcesSettings> = MutableStateFlow(
|
||||||
|
DataSourcesSettings(false, false, "", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
override val radioControlSettings: StateFlow<RadioControlSettings> = MutableStateFlow(
|
||||||
|
RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun setSelectedIds(ids: List<Int>) = Unit
|
||||||
|
|
||||||
|
override fun setSelectedSatModes(modes: List<String>) {
|
||||||
|
selectedSatModes.value = modes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setPassesSettings(settings: PassesSettings) = Unit
|
||||||
|
|
||||||
|
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean = true
|
||||||
|
|
||||||
|
override fun setStationPosition(): Boolean = true
|
||||||
|
|
||||||
|
override fun setStationPosition(locator: String): Boolean = true
|
||||||
|
|
||||||
|
override fun updateDatabaseState(state: DatabaseState) {
|
||||||
|
databaseState.value = state
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateRCSettings(settings: RCSettings) = Unit
|
||||||
|
|
||||||
|
override fun updateOtherSettings(transform: (OtherSettings) -> OtherSettings) = Unit
|
||||||
|
|
||||||
|
override fun updateDataSourcesSettings(settings: DataSourcesSettings) = Unit
|
||||||
|
|
||||||
|
override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.convention.coreDomainPlugin)
|
alias(libs.plugins.convention.coreDomainPlugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// 编译期使用 org.json(构造/解析 WaveLog API 请求体); 运行时用 Android 系统自带的 org.json
|
|
||||||
compileOnly("org.json:json:20240303")
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package com.rtbishop.look4sat.core.domain.aprs
|
|
||||||
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.round
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APRS-IS 协议核心(纯 Kotlin,无 Android 依赖)。
|
|
||||||
* 从 APRSdroid 1.6.3d 逆向移植:AprsPacket$.scala + ab0oo Position.java。
|
|
||||||
*/
|
|
||||||
object AprsPacket {
|
|
||||||
|
|
||||||
/** APRS-IS passcode 算法(标准):0x73E2 初值,呼号大写+\0,每对字符 XOR */
|
|
||||||
fun passcode(callsign: String): Int {
|
|
||||||
val s = callsign.split("-")[0].uppercase() + "\u0000"
|
|
||||||
var hash = 29666 // 0x73E2
|
|
||||||
var i = 0
|
|
||||||
while (i <= s.length - 2) {
|
|
||||||
hash = hash xor (s[i].code * 256 + s[i + 1].code)
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
return hash and 0x7FFF
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 登录行:user CALL-SSID pass XXXX vers XXXX */
|
|
||||||
fun formatLogin(callsign: String, ssid: String, passcode: Int, version: String): String {
|
|
||||||
val callSsid = formatCallSsid(callsign, ssid)
|
|
||||||
return "user $callSsid pass $passcode vers $version"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 呼号-SSID 拼接(BG7NTA + 5 → BG7NTA-5) */
|
|
||||||
fun formatCallSsid(callsign: String, ssid: String): String {
|
|
||||||
if (ssid.isNullOrEmpty()) return callsign
|
|
||||||
return "$callsign-$ssid"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 可选距离过滤: filter r/lat/lon/dist */
|
|
||||||
fun formatRangeFilter(latitude: Double, longitude: Double, distKm: Int): String {
|
|
||||||
return String.format("r/%.3f/%.3f/%d", latitude, longitude, distKm)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 高度扩展 /A=00000(英尺) */
|
|
||||||
fun formatAltitude(altitudeMeters: Double?): String {
|
|
||||||
if (altitudeMeters == null) return ""
|
|
||||||
return String.format("/A=%06d", (altitudeMeters * 3.2808399).toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 航速航向扩展(节/度) */
|
|
||||||
fun formatCourseSpeed(speedMps: Double?, bearing: Float?): String {
|
|
||||||
if (speedMps == null || bearing == null) return ""
|
|
||||||
val knots = (speedMps * 1.94384449).toInt()
|
|
||||||
return String.format("/%03d/%03d", bearing.toInt(), knots)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APRS 位置编码(ab0oo Position.java 逆向移植)。
|
|
||||||
* 非压缩:DDMM.MMN/DDDMM.MME;压缩:base91。
|
|
||||||
*/
|
|
||||||
class AprsPosition(
|
|
||||||
val latitude: Double,
|
|
||||||
val longitude: Double,
|
|
||||||
val symbolTable: Char,
|
|
||||||
val symbolCode: Char,
|
|
||||||
val positionAmbiguity: Int = 0
|
|
||||||
) {
|
|
||||||
|
|
||||||
/** 非压缩格式(APRS-IS 默认上报格式) */
|
|
||||||
fun toUncompressedString(): String {
|
|
||||||
val lat = getDMS(latitude, true)
|
|
||||||
val lon = getDMS(longitude, false)
|
|
||||||
return "$lat$symbolTable$lon$symbolCode"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 压缩格式(base91,标准 APRS 算法) */
|
|
||||||
fun toCompressedString(): String {
|
|
||||||
val jRound = round((90.0 - latitude) * 380926.0).toLong()
|
|
||||||
val j = jRound / 753571 + 33
|
|
||||||
val j2 = jRound % 753571
|
|
||||||
val j3 = j2 / 8281 + 33
|
|
||||||
val j4 = j2 % 8281
|
|
||||||
val i = (j4 % 91).toInt() + 33
|
|
||||||
val jRound2 = round((longitude + 180.0) * 190463.0).toLong()
|
|
||||||
val j5 = jRound2 / 753571 + 33
|
|
||||||
val j6 = jRound2 % 753571
|
|
||||||
val j7 = 33 + j6 / 8281
|
|
||||||
val j8 = j6 % 8281
|
|
||||||
return "" + symbolTable + j.toInt().toChar() + j3.toInt().toChar() +
|
|
||||||
((j4 / 91).toInt() + 33).toChar() + i.toChar() +
|
|
||||||
j5.toInt().toChar() + j7.toInt().toChar() +
|
|
||||||
((j8 / 91).toInt() + 33).toChar() +
|
|
||||||
((j8 % 91).toInt() + 33).toChar() + symbolCode
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 单轴 DMS 编码(百分位) */
|
|
||||||
private fun getDMS(value: Double, isLat: Boolean): String {
|
|
||||||
var iRound = round(value * 6000.0).toInt()
|
|
||||||
if (iRound < 0) iRound = -iRound
|
|
||||||
val degrees = iRound / 6000
|
|
||||||
val minutes = (iRound / 100) % 60
|
|
||||||
val hundredths = iRound % 100
|
|
||||||
val frac = when (positionAmbiguity) {
|
|
||||||
1 -> " . "
|
|
||||||
2 -> String.format("%d . ", minutes / 10)
|
|
||||||
3 -> String.format("%02d. ", minutes)
|
|
||||||
4 -> String.format("%02d.%d ", minutes, hundredths / 10)
|
|
||||||
else -> String.format("%02d.%02d", minutes, hundredths)
|
|
||||||
}
|
|
||||||
return if (isLat) {
|
|
||||||
val ns = if (value >= 0) 'N' else 'S'
|
|
||||||
String.format("%02d%s%c", degrees, frac, ns)
|
|
||||||
} else {
|
|
||||||
val ew = if (value >= 0) 'E' else 'W'
|
|
||||||
String.format("%03d%s%c", degrees, frac, ew)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bayesian Morse timing decoder.
|
|
||||||
* Replaces hard thresholds with probability-based decision making.
|
|
||||||
*
|
|
||||||
* Inspired by VE3NEA's CW Skimmer approach:
|
|
||||||
* "Instead of making a hard decision at every input sample whether the signal
|
|
||||||
* is present or not, compute the probability that the signal is present."
|
|
||||||
*
|
|
||||||
* Uses Gaussian probability density centered on expected durations:
|
|
||||||
* P(dit | duration) = exp(-(duration - dotMs)^2 / (2 * variance^2))
|
|
||||||
* P(dash | duration) = exp(-(duration - 3*dotMs)^2 / (2 * variance^2))
|
|
||||||
*/
|
|
||||||
internal class CwBayesianDecoder {
|
|
||||||
|
|
||||||
// Morse timing parameters
|
|
||||||
private var dotDurationMs = 60f // initial 20 WPM
|
|
||||||
private var speedWpm = 20f
|
|
||||||
|
|
||||||
// Current symbol being accumulated
|
|
||||||
private var currentSymbol = StringBuilder()
|
|
||||||
private var textBuffer = StringBuilder()
|
|
||||||
|
|
||||||
// Recent dit lengths for speed estimation
|
|
||||||
private val recentDits = mutableListOf<Float>()
|
|
||||||
|
|
||||||
// Output
|
|
||||||
private var _decodedText = ""
|
|
||||||
val decodedText: String get() = _decodedText
|
|
||||||
|
|
||||||
/** Gaussian probability. */
|
|
||||||
private fun gaussianProb(durationMs: Float, expectedMs: Float, varianceMs: Float): Float {
|
|
||||||
if (varianceMs <= 0f) return 0f
|
|
||||||
val diff = durationMs - expectedMs
|
|
||||||
return kotlin.math.exp(-(diff * diff) / (2 * varianceMs * varianceMs))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Process a tone duration. Returns the symbol type with highest probability. */
|
|
||||||
fun processTone(durationMs: Float): ToneResult {
|
|
||||||
val ditProb = gaussianProb(durationMs, dotDurationMs, dotDurationMs * 0.4f)
|
|
||||||
val dashProb = gaussianProb(durationMs, dotDurationMs * 3f, dotDurationMs * 0.6f)
|
|
||||||
|
|
||||||
return if (ditProb > dashProb && ditProb > 0.05f) {
|
|
||||||
currentSymbol.append('0')
|
|
||||||
recentDits.add(durationMs)
|
|
||||||
updateSpeed()
|
|
||||||
ToneResult('0', ditProb)
|
|
||||||
} else if (dashProb > 0.05f) {
|
|
||||||
currentSymbol.append('1')
|
|
||||||
ToneResult('1', dashProb)
|
|
||||||
} else {
|
|
||||||
ToneResult(null, 0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Process a gap duration. Returns decoded character or null. */
|
|
||||||
fun processGap(durationMs: Float): Char? {
|
|
||||||
if (currentSymbol.isEmpty()) {
|
|
||||||
val wordProb = gaussianProb(durationMs, dotDurationMs * 7f, dotDurationMs * 1.2f)
|
|
||||||
if (wordProb > 0.2f) {
|
|
||||||
textBuffer.append(' ')
|
|
||||||
_decodedText = textBuffer.toString()
|
|
||||||
return ' '
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val interCharProb = gaussianProb(durationMs, dotDurationMs * 3f, dotDurationMs * 0.6f)
|
|
||||||
val wordProb = gaussianProb(durationMs, dotDurationMs * 7f, dotDurationMs * 1.2f)
|
|
||||||
|
|
||||||
if (wordProb > interCharProb && wordProb > 0.2f) {
|
|
||||||
val char = flushSymbol()
|
|
||||||
textBuffer.append(' ')
|
|
||||||
_decodedText = textBuffer.toString()
|
|
||||||
return char
|
|
||||||
}
|
|
||||||
if (interCharProb > 0.15f) {
|
|
||||||
val char = flushSymbol()
|
|
||||||
_decodedText = textBuffer.toString()
|
|
||||||
return char
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun flushSymbol(): Char? {
|
|
||||||
if (currentSymbol.isEmpty()) return null
|
|
||||||
val morse = currentSymbol.toString()
|
|
||||||
currentSymbol.clear()
|
|
||||||
val char = morseToChar(morse)
|
|
||||||
if (char != null) textBuffer.append(char)
|
|
||||||
return char
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSpeed() {
|
|
||||||
if (recentDits.size < 3) return
|
|
||||||
val sorted = recentDits.sorted()
|
|
||||||
val median = sorted[sorted.size / 2]
|
|
||||||
if (median > 0f) {
|
|
||||||
dotDurationMs = dotDurationMs * 0.7f + median * 0.3f
|
|
||||||
val wpm = 60.0f / (50.0f * dotDurationMs / 1000.0f)
|
|
||||||
if (wpm in 5f..55f) speedWpm = wpm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSpeed(): Float = speedWpm
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
dotDurationMs = 60f
|
|
||||||
speedWpm = 20f
|
|
||||||
recentDits.clear()
|
|
||||||
currentSymbol.clear()
|
|
||||||
textBuffer.clear()
|
|
||||||
_decodedText = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val MORSE_TABLE = mapOf(
|
|
||||||
"01" to 'A', "1000" to 'B', "1010" to 'C', "100" to 'D', "0" to 'E',
|
|
||||||
"0010" to 'F', "110" to 'G', "0000" to 'H', "00" to 'I', "0111" to 'J',
|
|
||||||
"101" to 'K', "0100" to 'L', "11" to 'M', "10" to 'N', "111" to 'O',
|
|
||||||
"0110" to 'P', "1101" to 'Q', "010" to 'R', "000" to 'S', "1" to 'T',
|
|
||||||
"001" to 'U', "0001" to 'V', "011" to 'W', "1001" to 'X', "1011" to 'Y',
|
|
||||||
"1100" to 'Z', "01111" to '1', "00111" to '2', "00011" to '3',
|
|
||||||
"00001" to '4', "00000" to '5', "10000" to '6', "11000" to '7',
|
|
||||||
"11100" to '8', "11110" to '9', "11111" to '0',
|
|
||||||
"010101" to '.', "110011" to ',', "001100" to '?', "011110" to '\'',
|
|
||||||
"101011" to '!', "10010" to '/', "10110" to '(', "101101" to ')',
|
|
||||||
"01000" to '&', "111000" to ':', "101010" to ';', "10001" to '=',
|
|
||||||
"01010" to '+', "100001" to '-', "001101" to '_', "010010" to '"',
|
|
||||||
"0001001" to '$', "011010" to '@'
|
|
||||||
)
|
|
||||||
fun morseToChar(morse: String): Char? = MORSE_TABLE[morse]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ToneResult(val symbol: Char?, val probability: Float)
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multi-channel CW signal tracker.
|
|
||||||
* Monitors the spectrogram for active frequency bins and extracts
|
|
||||||
* energy envelopes for each detected signal.
|
|
||||||
*
|
|
||||||
* Inspired by CW Skimmer's multi-channel approach:
|
|
||||||
* tracks all active signals in the passband simultaneously,
|
|
||||||
* selects the best one for decoded output.
|
|
||||||
*/
|
|
||||||
internal class CwChannelTracker(
|
|
||||||
private val spectrogram: CwSpectrogram,
|
|
||||||
private val maxChannels: Int = 3
|
|
||||||
) {
|
|
||||||
data class Channel(
|
|
||||||
val bin: Int,
|
|
||||||
val frequency: Float,
|
|
||||||
var active: Boolean = false,
|
|
||||||
var energy: Float = 0f,
|
|
||||||
val history: MutableList<Float> = mutableListOf(),
|
|
||||||
var confidence: Float = 0f
|
|
||||||
)
|
|
||||||
|
|
||||||
private val channels = Array(maxChannels) { Channel(0, 0f) }
|
|
||||||
|
|
||||||
/** Scan the current spectrogram column and update channel tracking. */
|
|
||||||
fun update(): List<Channel> {
|
|
||||||
val col = spectrogram.getCurrentColumn()
|
|
||||||
val peaks = findPeaks(col, threshold = 0.3f, minDistance = 2)
|
|
||||||
|
|
||||||
// Update existing channels
|
|
||||||
for (ch in channels) {
|
|
||||||
if (ch.active) {
|
|
||||||
if (peaks.contains(ch.bin)) {
|
|
||||||
ch.energy = col[ch.bin]
|
|
||||||
ch.history.add(ch.energy)
|
|
||||||
if (ch.history.size > 40) ch.history.removeAt(0)
|
|
||||||
ch.confidence = computeConfidence(ch.history)
|
|
||||||
} else {
|
|
||||||
// Signal lost — decay confidence
|
|
||||||
ch.history.add(0f)
|
|
||||||
if (ch.history.size > 40) ch.history.removeAt(0)
|
|
||||||
ch.confidence *= 0.9f
|
|
||||||
if (ch.confidence < 0.1f) ch.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign new peaks to inactive channels
|
|
||||||
var peakIdx = 0
|
|
||||||
for (ch in channels) {
|
|
||||||
if (!ch.active && peakIdx < peaks.size) {
|
|
||||||
val bin = peaks[peakIdx]
|
|
||||||
val freq = spectrogram.binToFreq(bin)
|
|
||||||
// Re-initialize channel
|
|
||||||
channels[peakIdx] = Channel(bin, freq, true, col[bin], mutableListOf(), 0.5f)
|
|
||||||
peakIdx++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels.filter { it.active }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Find peak bins in the spectrum. */
|
|
||||||
private fun findPeaks(spectrum: FloatArray, threshold: Float, minDistance: Int): List<Int> {
|
|
||||||
val peaks = mutableListOf<Int>()
|
|
||||||
for (i in 1 until spectrum.size - 1) {
|
|
||||||
if (spectrum[i] > spectrum[i - 1] && spectrum[i] > spectrum[i + 1] && spectrum[i] > threshold) {
|
|
||||||
if (peaks.isEmpty() || i - peaks.last() >= minDistance) {
|
|
||||||
peaks.add(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return peaks.sortedByDescending { spectrum[it] }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Compute confidence from energy history. Lower variance = higher confidence. */
|
|
||||||
private fun computeConfidence(history: List<Float>): Float {
|
|
||||||
if (history.size < 10) return 0.3f
|
|
||||||
val recent = history.takeLast(10)
|
|
||||||
val mean = recent.average().toFloat()
|
|
||||||
val variance = recent.map { (it - mean) * (it - mean) }.average().toFloat()
|
|
||||||
return if (mean > 0f) (mean / (mean + variance + 0.1f)).coerceIn(0f, 1f) else 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the channel with highest confidence. */
|
|
||||||
fun getBestChannel(): Channel? {
|
|
||||||
return channels.filter { it.active }.maxByOrNull { it.confidence }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
for (i in channels.indices) {
|
|
||||||
channels[i] = Channel(0, 0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CW (Morse code) decoder v3 — Spectrogram-based multi-channel Bayesian decoder.
|
|
||||||
*
|
|
||||||
* Architecture inspired by Morse Expert / CW Skimmer (VE3NEA):
|
|
||||||
* 1. FFT spectrogram creates a frequency×time matrix
|
|
||||||
* 2. Multi-channel peak detector finds all active signals
|
|
||||||
* 3. Per-channel energy envelope extraction
|
|
||||||
* 4. Bayesian probability for symbol timing (Gaussian likelihood)
|
|
||||||
* 5. Best channel selected for output
|
|
||||||
*
|
|
||||||
* Timing analysis is performed per spectrogram column (hop).
|
|
||||||
* Each column represents hopSize/sampleRate seconds of audio.
|
|
||||||
*/
|
|
||||||
class CwDecoder(
|
|
||||||
val sampleRate: Int = 8000,
|
|
||||||
cwToneFreq: Float = -1f // ignored in v3 (auto-detect via spectrogram)
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
private const val FFT_SIZE = 256
|
|
||||||
private const val HOP_SIZE = 64
|
|
||||||
}
|
|
||||||
|
|
||||||
private val spectrogram = CwSpectrogram(
|
|
||||||
fftSize = FFT_SIZE,
|
|
||||||
hopSize = HOP_SIZE,
|
|
||||||
sampleRate = sampleRate,
|
|
||||||
minBin = 6,
|
|
||||||
maxBin = 38,
|
|
||||||
historyCols = 40
|
|
||||||
)
|
|
||||||
private val channelTracker = CwChannelTracker(spectrogram, maxChannels = 3)
|
|
||||||
private val bayesianDecoder = CwBayesianDecoder()
|
|
||||||
|
|
||||||
// Timing state per channel
|
|
||||||
private data class ChannelTiming(
|
|
||||||
var isSignal: Boolean = false,
|
|
||||||
var toneTicks: Int = 0,
|
|
||||||
var gapTicks: Int = 0
|
|
||||||
)
|
|
||||||
private val timingStates = Array(3) { ChannelTiming() }
|
|
||||||
|
|
||||||
// Time per spectrogram column in milliseconds
|
|
||||||
private val tickMs = 1000f * HOP_SIZE / sampleRate
|
|
||||||
|
|
||||||
// Output flows
|
|
||||||
private val _decodedTextFlow = MutableStateFlow("")
|
|
||||||
val decodedTextFlow: StateFlow<String> = _decodedTextFlow
|
|
||||||
|
|
||||||
private val _signalStrength = MutableStateFlow(0f)
|
|
||||||
val signalStrength: StateFlow<Float> = _signalStrength
|
|
||||||
|
|
||||||
private val _estimatedPitch = MutableStateFlow<Float?>(null)
|
|
||||||
val estimatedPitch: StateFlow<Float?> = _estimatedPitch
|
|
||||||
|
|
||||||
private val _estimatedSpeed = MutableStateFlow<Float?>(null)
|
|
||||||
val estimatedSpeed: StateFlow<Float?> = _estimatedSpeed
|
|
||||||
|
|
||||||
private var frameCount = 0
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (cwToneFreq > 0f) {
|
|
||||||
_estimatedPitch.value = cwToneFreq
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun processBuffer(buffer: FloatArray) {
|
|
||||||
// 1. Feed samples to spectrogram
|
|
||||||
spectrogram.addSamples(buffer)
|
|
||||||
|
|
||||||
// 2. Get number of new columns generated
|
|
||||||
val newCols = spectrogram.getNewColumns()
|
|
||||||
if (newCols == 0) return
|
|
||||||
|
|
||||||
// 3. Update channel tracker (uses latest column for peak detection)
|
|
||||||
val activeChannels = channelTracker.update()
|
|
||||||
|
|
||||||
// 4. Process each new column for timing analysis
|
|
||||||
// Columns are indexed 0..historyCols-1, where historyCols-1 is the newest
|
|
||||||
val baseIdx = (spectrogram.historyCols - newCols).coerceAtLeast(0)
|
|
||||||
for (colOffset in 0 until newCols) {
|
|
||||||
val col = spectrogram.getColumn(baseIdx + colOffset)
|
|
||||||
|
|
||||||
for ((idx, channel) in activeChannels.withIndex()) {
|
|
||||||
if (idx >= timingStates.size) break
|
|
||||||
val state = timingStates[idx]
|
|
||||||
val energy = if (channel.bin in col.indices) col[channel.bin] else 0f
|
|
||||||
|
|
||||||
// Adaptive threshold
|
|
||||||
val threshold = 0.3f + (energy - 0.3f) * 0.3f
|
|
||||||
|
|
||||||
if (energy > threshold) {
|
|
||||||
if (!state.isSignal) {
|
|
||||||
if (state.gapTicks > 0) {
|
|
||||||
val gapMs = state.gapTicks * tickMs
|
|
||||||
bayesianDecoder.processGap(gapMs)
|
|
||||||
}
|
|
||||||
state.gapTicks = 0
|
|
||||||
state.isSignal = true
|
|
||||||
}
|
|
||||||
state.toneTicks++
|
|
||||||
} else {
|
|
||||||
if (state.isSignal) {
|
|
||||||
if (state.toneTicks > 0) {
|
|
||||||
val toneMs = state.toneTicks * tickMs
|
|
||||||
bayesianDecoder.processTone(toneMs)
|
|
||||||
}
|
|
||||||
state.toneTicks = 0
|
|
||||||
state.isSignal = false
|
|
||||||
}
|
|
||||||
state.gapTicks++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Update outputs
|
|
||||||
frameCount++
|
|
||||||
if (frameCount % 5 == 0) {
|
|
||||||
val bestChannel = channelTracker.getBestChannel()
|
|
||||||
if (bestChannel != null) {
|
|
||||||
_estimatedPitch.value = bestChannel.frequency
|
|
||||||
_signalStrength.value = bestChannel.confidence
|
|
||||||
_estimatedSpeed.value = bayesianDecoder.getSpeed()
|
|
||||||
}
|
|
||||||
_decodedTextFlow.value = bayesianDecoder.decodedText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetDecoder() {
|
|
||||||
spectrogram.reset()
|
|
||||||
channelTracker.reset()
|
|
||||||
bayesianDecoder.reset()
|
|
||||||
for (state in timingStates) {
|
|
||||||
state.isSignal = false
|
|
||||||
state.toneTicks = 0
|
|
||||||
state.gapTicks = 0
|
|
||||||
}
|
|
||||||
frameCount = 0
|
|
||||||
_decodedTextFlow.value = ""
|
|
||||||
_signalStrength.value = 0f
|
|
||||||
_estimatedPitch.value = null
|
|
||||||
_estimatedSpeed.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.math.cos
|
|
||||||
import kotlin.math.sin
|
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DSP utilities for CW (Morse code) decoding.
|
|
||||||
* Pure Kotlin, no NDK required.
|
|
||||||
*/
|
|
||||||
internal object CwDsp {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Design a simple bandpass FIR filter coefficients using windowed sinc method.
|
|
||||||
* @param lowCutoff lower cutoff frequency (Hz) as fraction of sampleRate
|
|
||||||
* @param highCutoff upper cutoff frequency (Hz) as fraction of sampleRate
|
|
||||||
* @param taps filter length (must be odd)
|
|
||||||
*/
|
|
||||||
fun bandpassFir(lowCutoff: Double, highCutoff: Double, taps: Int): FloatArray {
|
|
||||||
val n = if (taps % 2 == 0) taps + 1 else taps
|
|
||||||
val half = n / 2
|
|
||||||
val coeffs = FloatArray(n)
|
|
||||||
for (i in 0 until n) {
|
|
||||||
val idx = i - half
|
|
||||||
if (idx == 0) {
|
|
||||||
coeffs[i] = (2.0 * (highCutoff - lowCutoff)).toFloat()
|
|
||||||
} else {
|
|
||||||
val x = PI * idx
|
|
||||||
coeffs[i] = ((sin(2 * highCutoff * x) - sin(2 * lowCutoff * x)) / x).toFloat()
|
|
||||||
}
|
|
||||||
// Hamming window
|
|
||||||
coeffs[i] = (coeffs[i] * (0.54 - 0.46 * cos(2 * PI * i / (n - 1)))).toFloat()
|
|
||||||
}
|
|
||||||
// Normalize
|
|
||||||
val sum = coeffs.sum()
|
|
||||||
if (sum != 0f) for (i in 0 until n) coeffs[i] /= sum
|
|
||||||
return coeffs
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Apply FIR filter to a buffer. */
|
|
||||||
fun applyFir(buffer: FloatArray, coeffs: FloatArray): FloatArray {
|
|
||||||
val out = FloatArray(buffer.size)
|
|
||||||
for (i in buffer.indices) {
|
|
||||||
var sum = 0f
|
|
||||||
for (j in coeffs.indices) {
|
|
||||||
val idx = i - j
|
|
||||||
if (idx >= 0) sum += buffer[idx] * coeffs[j]
|
|
||||||
}
|
|
||||||
out[i] = sum
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Simple envelope detector: abs + low-pass smoothing. */
|
|
||||||
fun envelope(signal: FloatArray, alpha: Float = 0.1f): FloatArray {
|
|
||||||
val env = FloatArray(signal.size)
|
|
||||||
var s = 0f
|
|
||||||
for (i in signal.indices) {
|
|
||||||
s = alpha * kotlin.math.abs(signal[i]) + (1 - alpha) * s
|
|
||||||
env[i] = s
|
|
||||||
}
|
|
||||||
return env
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Estimate noise floor from envelope for adaptive thresholding. */
|
|
||||||
fun noiseFloor(env: FloatArray, fraction: Float = 0.3f): Float {
|
|
||||||
val sorted = env.sortedArray()
|
|
||||||
val median = sorted[sorted.size / 2]
|
|
||||||
return median + (sorted[sorted.size * 9 / 10] - median) * fraction
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Simple Goertzel to detect a specific tone frequency. */
|
|
||||||
fun goertzel(buffer: FloatArray, targetFreq: Float, sampleRate: Int): Float {
|
|
||||||
val omega = 2.0 * PI * targetFreq / sampleRate
|
|
||||||
val coeff = 2.0 * cos(omega)
|
|
||||||
var s0 = 0.0; var s1 = 0.0; var s2 = 0.0
|
|
||||||
for (sample in buffer) {
|
|
||||||
s0 = sample.toDouble() + coeff * s1 - s2
|
|
||||||
s2 = s1; s1 = s0
|
|
||||||
}
|
|
||||||
val power = s2 * s2 + s1 * s1 - coeff * s1 * s2
|
|
||||||
return sqrt(kotlin.math.abs(power)).toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
import kotlin.math.cos
|
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Radix-2 FFT for real-valued input.
|
|
||||||
* Produces magnitude spectrum for the first N/2+1 bins.
|
|
||||||
* Used by CwSpectrogram for time-frequency analysis.
|
|
||||||
*/
|
|
||||||
internal class CwFFT(private val n: Int) {
|
|
||||||
init {
|
|
||||||
require(n > 0 && n and (n - 1) == 0) { "FFT size must be power of 2, got $n" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val cosTable = FloatArray(n / 2)
|
|
||||||
private val sinTable = FloatArray(n / 2)
|
|
||||||
|
|
||||||
init {
|
|
||||||
for (i in 0 until n / 2) {
|
|
||||||
val angle = -2.0 * kotlin.math.PI * i / n
|
|
||||||
cosTable[i] = cos(angle).toFloat()
|
|
||||||
sinTable[i] = kotlin.math.sin(angle).toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Compute magnitude spectrum for real input. Returns array of size n/2+1. */
|
|
||||||
fun magnitudeSpectrum(input: FloatArray): FloatArray {
|
|
||||||
require(input.size == n) { "Input size must be $n, got ${input.size}" }
|
|
||||||
|
|
||||||
val real = input.copyOf()
|
|
||||||
val imag = FloatArray(n)
|
|
||||||
|
|
||||||
// Bit-reversal permutation
|
|
||||||
var j = 0
|
|
||||||
for (i in 1 until n) {
|
|
||||||
var bit = n shr 1
|
|
||||||
while (j and bit != 0) { j = j xor bit; bit = bit shr 1 }
|
|
||||||
j = j xor bit
|
|
||||||
if (i < j) {
|
|
||||||
var tmp = real[i]; real[i] = real[j]; real[j] = tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Radix-2 Cooley-Tukey FFT
|
|
||||||
var len = 2
|
|
||||||
while (len <= n) {
|
|
||||||
val half = len / 2
|
|
||||||
val step = n / len
|
|
||||||
for (i in 0 until n step len) {
|
|
||||||
for (k in 0 until half) {
|
|
||||||
val tReal = real[i + k + half] * cosTable[k * step] - imag[i + k + half] * sinTable[k * step]
|
|
||||||
val tImag = real[i + k + half] * sinTable[k * step] + imag[i + k + half] * cosTable[k * step]
|
|
||||||
real[i + k + half] = real[i + k] - tReal
|
|
||||||
imag[i + k + half] = imag[i + k] - tImag
|
|
||||||
real[i + k] += tReal
|
|
||||||
imag[i + k] += tImag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
len = len shl 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Magnitude spectrum (first N/2+1 bins)
|
|
||||||
val mag = FloatArray(n / 2 + 1)
|
|
||||||
for (i in 0..n / 2) {
|
|
||||||
mag[i] = sqrt(real[i] * real[i] + imag[i] * imag[i]) / n
|
|
||||||
}
|
|
||||||
return mag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
/**
|
|
||||||
* First-order IIR filters.
|
|
||||||
* Ported from ggmorse/src/filter.h
|
|
||||||
*/
|
|
||||||
internal class CwFilter {
|
|
||||||
private var z1 = 0f
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val PI_F = 3.141592653589793f
|
|
||||||
}
|
|
||||||
|
|
||||||
fun highPass(sample: Float, cutoffHz: Float, sampleRate: Float): Float {
|
|
||||||
val rc = 1.0f / (2f * PI_F * cutoffHz)
|
|
||||||
val dt = 1.0f / sampleRate
|
|
||||||
val alpha = dt / (rc + dt)
|
|
||||||
z1 = alpha * (z1 + sample - z1)
|
|
||||||
return sample - z1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun lowPass(sample: Float, cutoffHz: Float, sampleRate: Float): Float {
|
|
||||||
val rc = 1.0f / (2f * PI_F * cutoffHz)
|
|
||||||
val dt = 1.0f / sampleRate
|
|
||||||
val alpha = dt / (rc + dt)
|
|
||||||
z1 += alpha * (sample - z1)
|
|
||||||
return z1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() { z1 = 0f }
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
import kotlin.math.cos
|
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Running Goertzel filter for CW tone detection.
|
|
||||||
* Tracks a specific frequency over time with a sliding window.
|
|
||||||
* Ported from ggmorse/src/goertzel.h
|
|
||||||
*/
|
|
||||||
internal class CwGoertzel {
|
|
||||||
private var s1 = 0.0
|
|
||||||
private var s2 = 0.0
|
|
||||||
private var coeff = 0.0
|
|
||||||
|
|
||||||
fun init(sampleRate: Float, targetFreq: Float) {
|
|
||||||
val omega = 2.0 * kotlin.math.PI * targetFreq / sampleRate
|
|
||||||
coeff = 2.0 * cos(omega)
|
|
||||||
s1 = 0.0
|
|
||||||
s2 = 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun process(sample: Float) {
|
|
||||||
val s0 = sample.toDouble() + coeff * s1 - s2
|
|
||||||
s2 = s1
|
|
||||||
s1 = s0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPower(): Float {
|
|
||||||
return sqrt(s2 * s2 + s1 * s1 - coeff * s1 * s2).toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
s1 = 0.0
|
|
||||||
s2 = 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple linear resampler.
|
|
||||||
* Downsamples from input sample rate to output sample rate.
|
|
||||||
* Ported from ggmorse/src/resampler.h
|
|
||||||
*/
|
|
||||||
internal class CwResampler(private val inputRate: Float, private val outputRate: Float) {
|
|
||||||
private val ratio = inputRate / outputRate
|
|
||||||
private var lastSample = 0f
|
|
||||||
|
|
||||||
fun process(input: FloatArray): FloatArray {
|
|
||||||
if (ratio <= 0f || input.isEmpty()) return input
|
|
||||||
val outputLen = (input.size / ratio).toInt() + 1
|
|
||||||
val output = FloatArray(outputLen)
|
|
||||||
var idx = 0f
|
|
||||||
for (i in output.indices) {
|
|
||||||
val intIdx = idx.toInt()
|
|
||||||
val frac = idx - intIdx
|
|
||||||
if (intIdx + 1 < input.size) {
|
|
||||||
output[i] = input[intIdx] * (1 - frac) + input[intIdx + 1] * frac
|
|
||||||
} else if (intIdx < input.size) {
|
|
||||||
output[i] = input[intIdx] * (1 - frac) + lastSample * frac
|
|
||||||
} else {
|
|
||||||
output[i] = lastSample
|
|
||||||
}
|
|
||||||
idx += ratio
|
|
||||||
}
|
|
||||||
lastSample = input.lastOrNull() ?: lastSample
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
lastSample = 0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
import kotlin.math.cos
|
|
||||||
import kotlin.math.sin
|
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightweight pitch detector using DFT at specific frequency bins.
|
|
||||||
* Only scans [200, 1200] Hz in configurable steps — much faster than full FFT.
|
|
||||||
* Ported from ggmorse/src/stfft.h (simplified for CW use case).
|
|
||||||
*/
|
|
||||||
internal class CwPitchDetector(
|
|
||||||
private val sampleRate: Float,
|
|
||||||
private val minFreq: Float = 200f,
|
|
||||||
private val maxFreq: Float = 1200f,
|
|
||||||
private val stepHz: Float = 10f
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Find the dominant pitch frequency in the buffer.
|
|
||||||
* Returns null if no significant pitch found.
|
|
||||||
*/
|
|
||||||
fun findPitch(buffer: FloatArray): Float? {
|
|
||||||
if (buffer.isEmpty()) return null
|
|
||||||
var bestFreq = 0f
|
|
||||||
var bestPower = 0f
|
|
||||||
var freq = minFreq
|
|
||||||
while (freq <= maxFreq) {
|
|
||||||
var real = 0.0
|
|
||||||
var imag = 0.0
|
|
||||||
val omega = 2.0 * kotlin.math.PI * freq / sampleRate
|
|
||||||
for (i in buffer.indices) {
|
|
||||||
real += buffer[i] * cos(omega * i)
|
|
||||||
imag += buffer[i] * -sin(omega * i)
|
|
||||||
}
|
|
||||||
val power = (real * real + imag * imag).toFloat()
|
|
||||||
if (power > bestPower) {
|
|
||||||
bestPower = power
|
|
||||||
bestFreq = freq
|
|
||||||
}
|
|
||||||
freq += stepHz
|
|
||||||
}
|
|
||||||
return if (bestPower > 0.001f) bestFreq else null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sliding-window spectrogram for CW decoding.
|
|
||||||
* Maintains a time-frequency matrix updated with each audio frame.
|
|
||||||
*
|
|
||||||
* FFT size: 256, hop size: 64, sample rate: 4000 (or native)
|
|
||||||
* Frequency bins: 6..38 (187-1187 Hz, covers typical CW range)
|
|
||||||
* History: 40 columns (320 ms window)
|
|
||||||
* Time resolution: 64/4000 = 16 ms, Frequency resolution: 4000/256 = 15.625 Hz
|
|
||||||
*/
|
|
||||||
internal class CwSpectrogram(
|
|
||||||
private val fftSize: Int = 256,
|
|
||||||
private val hopSize: Int = 64,
|
|
||||||
private val sampleRate: Int = 4000,
|
|
||||||
private val minBin: Int = 6,
|
|
||||||
private val maxBin: Int = 38,
|
|
||||||
val historyCols: Int = 40
|
|
||||||
) {
|
|
||||||
private val fft = CwFFT(fftSize)
|
|
||||||
val numBins: Int get() = maxBin - minBin + 1
|
|
||||||
|
|
||||||
// Hanning window
|
|
||||||
private val hanning = FloatArray(fftSize) {
|
|
||||||
(0.5 - 0.5 * kotlin.math.cos(2.0 * kotlin.math.PI * it / (fftSize - 1))).toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spectrogram data: [timeCol][freqBin]
|
|
||||||
private val spectrogram = Array(historyCols) { FloatArray(numBins) }
|
|
||||||
private var currentCol = 0
|
|
||||||
private var samplesBuffered = 0
|
|
||||||
private val buffer = FloatArray(fftSize)
|
|
||||||
|
|
||||||
// Per-bin running energy for normalization
|
|
||||||
private val binEnergy = FloatArray(numBins) { 1f }
|
|
||||||
private val alpha = 0.95f
|
|
||||||
|
|
||||||
// Counter for new columns generated since last check
|
|
||||||
private var newColumnCount = 0
|
|
||||||
|
|
||||||
/** Add audio samples, compute FFTs for each complete hop. */
|
|
||||||
fun addSamples(samples: FloatArray) {
|
|
||||||
var offset = 0
|
|
||||||
while (offset < samples.size) {
|
|
||||||
val needed = fftSize - samplesBuffered
|
|
||||||
val copyLen = minOf(needed, samples.size - offset)
|
|
||||||
System.arraycopy(samples, offset, buffer, samplesBuffered, copyLen)
|
|
||||||
samplesBuffered += copyLen
|
|
||||||
offset += copyLen
|
|
||||||
|
|
||||||
if (samplesBuffered >= fftSize) {
|
|
||||||
processFrame()
|
|
||||||
newColumnCount++
|
|
||||||
// Shift buffer: keep last (fftSize - hopSize) samples
|
|
||||||
System.arraycopy(buffer, hopSize, buffer, 0, fftSize - hopSize)
|
|
||||||
samplesBuffered = fftSize - hopSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get number of new columns generated since the last call to this method. */
|
|
||||||
fun getNewColumns(): Int {
|
|
||||||
val count = newColumnCount
|
|
||||||
newColumnCount = 0
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processFrame() {
|
|
||||||
// Apply Hanning window
|
|
||||||
val windowed = FloatArray(fftSize) { buffer[it] * hanning[it] }
|
|
||||||
|
|
||||||
// Compute FFT magnitude spectrum
|
|
||||||
val mag = fft.magnitudeSpectrum(windowed)
|
|
||||||
|
|
||||||
// Update spectrogram column
|
|
||||||
val col = spectrogram[currentCol]
|
|
||||||
for (b in 0 until numBins) {
|
|
||||||
val binIdx = minBin + b
|
|
||||||
val rawMag = mag[binIdx]
|
|
||||||
// Running energy normalization
|
|
||||||
binEnergy[b] = alpha * binEnergy[b] + (1 - alpha) * rawMag
|
|
||||||
col[b] = if (binEnergy[b] > 1e-6f) rawMag / binEnergy[b] else 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCol = (currentCol + 1) % historyCols
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the current spectrogram as a 2D array in chronological order. */
|
|
||||||
fun getSpectrogram(): Array<FloatArray> {
|
|
||||||
val result = Array(historyCols) { i ->
|
|
||||||
val srcIdx = (currentCol + i) % historyCols
|
|
||||||
spectrogram[srcIdx].copyOf()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the most recent column (current energy across all frequencies). */
|
|
||||||
fun getCurrentColumn(): FloatArray {
|
|
||||||
val prevCol = (currentCol - 1 + historyCols) % historyCols
|
|
||||||
return spectrogram[prevCol].copyOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get a column by index from the history (0 = oldest, historyCols-1 = newest). */
|
|
||||||
fun getColumn(index: Int): FloatArray {
|
|
||||||
val clamped = index.coerceIn(0, historyCols - 1)
|
|
||||||
val srcIdx = (currentCol - historyCols + clamped + historyCols) % historyCols
|
|
||||||
return spectrogram[srcIdx].copyOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Find the frequency bin with peak energy. Returns -1 if no significant signal. */
|
|
||||||
fun findPeakBin(): Int {
|
|
||||||
val col = getCurrentColumn()
|
|
||||||
var maxBin = -1
|
|
||||||
var maxVal = 0f
|
|
||||||
for (i in col.indices) {
|
|
||||||
if (col[i] > maxVal) {
|
|
||||||
maxVal = col[i]
|
|
||||||
maxBin = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (maxVal > 0.3f) maxBin else -1
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get energy at a specific bin over the last N columns in chronological order. */
|
|
||||||
fun getBinEnergy(bin: Int, numCols: Int): FloatArray {
|
|
||||||
val clamped = minOf(numCols, historyCols)
|
|
||||||
val result = FloatArray(clamped)
|
|
||||||
for (i in 0 until clamped) {
|
|
||||||
val colIdx = (currentCol - clamped + i + historyCols) % historyCols
|
|
||||||
result[i] = spectrogram[colIdx][bin]
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the bin index for a frequency in Hz. */
|
|
||||||
fun freqToBin(freqHz: Float): Int {
|
|
||||||
val bin = (freqHz * fftSize / sampleRate).toInt()
|
|
||||||
return (bin - minBin).coerceIn(0, numBins - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the center frequency for a bin. */
|
|
||||||
fun binToFreq(bin: Int): Float {
|
|
||||||
return (minBin + bin).toFloat() * sampleRate / fftSize
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
for (col in spectrogram) col.fill(0f)
|
|
||||||
currentCol = 0
|
|
||||||
samplesBuffered = 0
|
|
||||||
buffer.fill(0f)
|
|
||||||
binEnergy.fill(1f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.rtbishop.look4sat.core.domain.model
|
package com.rtbishop.look4sat.core.domain.model
|
||||||
|
|
||||||
/** 单条卫星状态报告(AMSAT 网站 tooltip 数据) */
|
/** One satellite status report (AMSAT site tooltip data) */
|
||||||
data class SatReport(
|
data class SatReport(
|
||||||
val id: String, // 报告 ID(a885153)
|
val id: String, // 报告 ID(a885153)
|
||||||
val statusText: String, // Heard / Telemetry Only / Not Heard ...
|
val statusText: String, // Heard / Telemetry Only / Not Heard ...
|
||||||
@@ -10,26 +10,26 @@ data class SatReport(
|
|||||||
val timeUtc: String // 2:46-:59 UTC
|
val timeUtc: String // 2:46-:59 UTC
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 单个 2 小时槽的状态 */
|
/** State of one 2-hour slot */
|
||||||
data class SatSlot(
|
data class SatSlot(
|
||||||
val statusColor: Long, // ARGB 状态色(-1 = 无报告)
|
val statusColor: Long, // ARGB 状态色(-1 = 无报告)
|
||||||
val count: Int, // 报告数量(0 = 无)
|
val count: Int, // 报告数量(0 = 无)
|
||||||
val reportIds: List<String> = emptyList() // 该槽报告 ID 列表
|
val reportIds: List<String> = emptyList() // 该槽报告 ID 列表
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 卫星一天的状态(12 个 2 小时槽) */
|
/** One satellite day (12 two-hour slots) */
|
||||||
data class SatDay(
|
data class SatDay(
|
||||||
val dateLabel: String, // "Aug 4"
|
val dateLabel: String, // "Aug 4"
|
||||||
val slots: List<SatSlot> // 12 槽(00-02 ... 22-24)
|
val slots: List<SatSlot> // 12 槽(00-02 ... 22-24)
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 单个卫星 6 天状态 */
|
/** One satellite, 6 days of state */
|
||||||
data class SatStatus(
|
data class SatStatus(
|
||||||
val name: String, // "AO-123_[FM]"
|
val name: String, // "AO-123_[FM]"
|
||||||
val days: List<SatDay> // 6 天(新→旧)
|
val days: List<SatDay> // 6 天(新→旧)
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 页面整体解析结果 */
|
/** Overall page parse result */
|
||||||
data class SatStatusPage(
|
data class SatStatusPage(
|
||||||
val fetchedAtUtcMs: Long,
|
val fetchedAtUtcMs: Long,
|
||||||
val statuses: List<SatStatus>,
|
val statuses: List<SatStatus>,
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ data class PassesSettings(
|
|||||||
val minElevation: Double,
|
val minElevation: Double,
|
||||||
val aosStartMinute: Int = 0,
|
val aosStartMinute: Int = 0,
|
||||||
val aosEndMinute: Int = 23 * 60 + 59,
|
val aosEndMinute: Int = 23 * 60 + 59,
|
||||||
val invertAosTimeWindow: Boolean = false,
|
val invertAosTimeWindow: Boolean = false
|
||||||
val selectedModes: List<String>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class RCSettings(
|
data class RCSettings(
|
||||||
@@ -63,17 +62,8 @@ data class OtherSettings(
|
|||||||
val sstvMode: String = "Auto",
|
val sstvMode: String = "Auto",
|
||||||
val lowElevation: Double = 15.0,
|
val lowElevation: Double = 15.0,
|
||||||
val highElevation: Double = 45.0,
|
val highElevation: Double = 45.0,
|
||||||
// UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示)
|
val radarCompassOffset: Float = 0f,
|
||||||
val hiddenScreens: List<String> = emptyList(),
|
val radarCompassOffsetElev: Float = 0f
|
||||||
// UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/地图/设置)
|
|
||||||
val screenOrder: List<String> = emptyList(),
|
|
||||||
// UI 设置: 更多菜单顺序(空 = 默认: 匹配/漫游/CW解码)
|
|
||||||
val subMenuOrder: List<String> = emptyList(),
|
|
||||||
// WaveLog 日志系统(4.5.2): 服务器配置
|
|
||||||
val wavelogUrl: String = "",
|
|
||||||
val wavelogApiKey: String = "",
|
|
||||||
val wavelogStationId: String = "",
|
|
||||||
val wavelogAutoUpload: Boolean = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DataSourcesSettings(
|
data class DataSourcesSettings(
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@ package com.rtbishop.look4sat.core.domain.repository
|
|||||||
|
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
|
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
|
||||||
|
|
||||||
/** AMSAT 卫星状态数据源 */
|
/** AMSAT satellite status data source */
|
||||||
interface IAmSatRepository {
|
interface IAmSatRepository {
|
||||||
/** 抓取并解析 AMSAT 状态页; 失败返回 null */
|
/** Fetch and parse the AMSAT status page; null on failure */
|
||||||
suspend fun fetchStatus(): SatStatusPage?
|
suspend fun fetchStatus(): SatStatusPage?
|
||||||
}
|
}
|
||||||
+3
-27
@@ -16,13 +16,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat.core.domain.repository
|
package com.rtbishop.look4sat.core.domain.repository
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IAudioCapture
|
import com.rtbishop.look4sat.core.domain.usecase.IAudioCapture
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.ISaveImage
|
import com.rtbishop.look4sat.core.domain.usecase.ISaveImage
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
|
|
||||||
interface IMainContainer {
|
interface IMainContainer {
|
||||||
val appScope: CoroutineScope
|
val appScope: CoroutineScope
|
||||||
@@ -30,10 +29,8 @@ interface IMainContainer {
|
|||||||
val selectionRepo: ISelectionRepo
|
val selectionRepo: ISelectionRepo
|
||||||
val satelliteRepo: ISatelliteRepo
|
val satelliteRepo: ISatelliteRepo
|
||||||
val databaseRepo: IDatabaseRepo
|
val databaseRepo: IDatabaseRepo
|
||||||
val amSatRepo: IAmSatRepository
|
val amSatRepo: IAmSatRepository
|
||||||
val radioTrackingService: IRadioTrackingService
|
val radioTrackingService: IRadioTrackingService
|
||||||
val mutualPassData: StateFlow<MutualPassData>
|
|
||||||
fun setMutualPassData(data: MutualPassData)
|
|
||||||
fun provideAddToCalendar(): IAddToCalendar
|
fun provideAddToCalendar(): IAddToCalendar
|
||||||
fun provideShowToast(): IShowToast
|
fun provideShowToast(): IShowToast
|
||||||
fun provideBluetoothReporter(): IReporter
|
fun provideBluetoothReporter(): IReporter
|
||||||
@@ -43,30 +40,9 @@ interface IMainContainer {
|
|||||||
fun provideRxRadioController(): IRadioController
|
fun provideRxRadioController(): IRadioController
|
||||||
fun provideAudioCapture(): IAudioCapture
|
fun provideAudioCapture(): IAudioCapture
|
||||||
fun provideSaveImage(): ISaveImage
|
fun provideSaveImage(): ISaveImage
|
||||||
// WaveLog 日志(4.5.2)
|
fun providePairedBluetoothDevices(): List<Pair<String, String>>
|
||||||
val wavelogQueue: com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
|
||||||
fun provideWavelogUploader(): com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MutualPassData(
|
|
||||||
val samples: List<Pair<Long, Pair<Double, Double>>> = emptyList(),
|
|
||||||
val trackSamples: List<TrackSampleData> = emptyList(),
|
|
||||||
val startTime: Long = 0L,
|
|
||||||
val endTime: Long = 0L,
|
|
||||||
val maxElev: Double = 10.0,
|
|
||||||
val labelA: String = "你",
|
|
||||||
val labelB: String = "友台"
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Minimal track sample for cross-module sharing (angles in degrees). */
|
|
||||||
data class TrackSampleData(
|
|
||||||
val time: Long = 0L,
|
|
||||||
val azimuthA: Double,
|
|
||||||
val elevationA: Double,
|
|
||||||
val azimuthB: Double,
|
|
||||||
val elevationB: Double
|
|
||||||
)
|
|
||||||
|
|
||||||
interface IContainerProvider {
|
interface IContainerProvider {
|
||||||
fun getMainContainer(): IMainContainer
|
fun getMainContainer(): IMainContainer
|
||||||
}
|
}
|
||||||
+1
-1
@@ -61,7 +61,7 @@ interface ISatelliteRepo {
|
|||||||
suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos>
|
suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos>
|
||||||
|
|
||||||
/** Get Doppler-shifted radio frequencies for a satellite at the given time. */
|
/** Get Doppler-shifted radio frequencies for a satellite at the given time. */
|
||||||
suspend fun getRadios(satPos: OrbitalPos, radios: List<SatRadio>): List<SatRadio>
|
suspend fun getRadios(sat: OrbitalObject, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio>
|
||||||
|
|
||||||
/** Fetch radio transceivers for a satellite by its catalog number. */
|
/** Fetch radio transceivers for a satellite by its catalog number. */
|
||||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||||
|
|||||||
+3
-3
@@ -21,10 +21,10 @@ import com.rtbishop.look4sat.core.domain.model.SatItem
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface ISelectionRepo {
|
interface ISelectionRepo {
|
||||||
fun getCurrentTypes(): List<String>
|
fun getCurrentModes(): List<String>
|
||||||
fun getTypesList(): List<String>
|
fun getModesList(): List<String>
|
||||||
suspend fun getEntriesFlow(): Flow<List<SatItem>>
|
suspend fun getEntriesFlow(): Flow<List<SatItem>>
|
||||||
suspend fun setTypes(types: List<String>)
|
suspend fun setModes(modes: List<String>)
|
||||||
suspend fun setQuery(query: String)
|
suspend fun setQuery(query: String)
|
||||||
suspend fun setSelection(selectAll: Boolean)
|
suspend fun setSelection(selectAll: Boolean)
|
||||||
suspend fun setSelection(ids: List<Int>, isTicked: Boolean)
|
suspend fun setSelection(ids: List<Int>, isTicked: Boolean)
|
||||||
|
|||||||
+3
-6
@@ -32,9 +32,9 @@ interface ISettingsRepo {
|
|||||||
|
|
||||||
//region # Satellites selection settings
|
//region # Satellites selection settings
|
||||||
val selectedIds: StateFlow<List<Int>>
|
val selectedIds: StateFlow<List<Int>>
|
||||||
val selectedTypes: StateFlow<List<String>>
|
val selectedSatModes: StateFlow<List<String>>
|
||||||
fun setSelectedIds(ids: List<Int>)
|
fun setSelectedIds(ids: List<Int>)
|
||||||
fun setSelectedTypes(types: List<String>)
|
fun setSelectedSatModes(modes: List<String>)
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region # Passes filter settings
|
//region # Passes filter settings
|
||||||
@@ -45,15 +45,12 @@ interface ISettingsRepo {
|
|||||||
//region # Station position settings
|
//region # Station position settings
|
||||||
val stationPosition: StateFlow<GeoPos>
|
val stationPosition: StateFlow<GeoPos>
|
||||||
fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean
|
fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean
|
||||||
/** GPS 定位(挂起): 拿到位置才返回 true; 权限缺失/超时/无信号返回 false */
|
fun setStationPosition(): Boolean
|
||||||
suspend fun setStationPosition(): Boolean
|
|
||||||
fun setStationPosition(locator: String): Boolean
|
fun setStationPosition(locator: String): Boolean
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region # Database update settings
|
//region # Database update settings
|
||||||
val databaseState: StateFlow<DatabaseState>
|
val databaseState: StateFlow<DatabaseState>
|
||||||
fun getSatelliteTypesIds(types: List<String>): List<Int>
|
|
||||||
fun setSatelliteTypeIds(type: String, ids: List<Int>)
|
|
||||||
fun updateDatabaseState(state: DatabaseState)
|
fun updateDatabaseState(state: DatabaseState)
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import java.io.InputStream
|
|||||||
interface IRemoteSource {
|
interface IRemoteSource {
|
||||||
suspend fun getFileStream(uri: String): InputStream?
|
suspend fun getFileStream(uri: String): InputStream?
|
||||||
suspend fun getNetworkStream(url: String): InputStream?
|
suspend fun getNetworkStream(url: String): InputStream?
|
||||||
|
suspend fun getAmSatCatalog(): String?
|
||||||
/** 抓取 AMSAT 状态页 HTML(带 UA, 返回 null = 失败) */
|
suspend fun getAmSatReports(hours: Int, limit: Int): String?
|
||||||
suspend fun getStatusHtml(): String?
|
|
||||||
}
|
}
|
||||||
@@ -18,41 +18,26 @@
|
|||||||
package com.rtbishop.look4sat.core.domain.source
|
package com.rtbishop.look4sat.core.domain.source
|
||||||
|
|
||||||
object Sources {
|
object Sources {
|
||||||
// 在线更新的默认 URL(用户可通过"自定义URL"对话框修改/重置)
|
|
||||||
val defaultTleUrl = "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv"
|
|
||||||
val defaultTransceiversUrl = "https://db.satnogs.org/api/transmitters/?format=json&status=active"
|
|
||||||
|
|
||||||
val satelliteDataUrls = mapOf(
|
val satelliteDataUrls = mapOf(
|
||||||
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
|
"CelesTrak" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
|
||||||
"Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv",
|
"AMSAT" to "https://amsat.org/tle/current/nasabare.txt",
|
||||||
"Brightest" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=visual&FORMAT=csv",
|
"McCants Classified" to "https://www.mmccants.org/tles/classfd.zip",
|
||||||
"Cubesat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=csv",
|
"McCants Integrated" to "https://www.mmccants.org/tles/inttles.zip",
|
||||||
"Education" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=education&FORMAT=csv",
|
|
||||||
"Engineer" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=engineering&FORMAT=csv",
|
|
||||||
"Geostationary" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=geo&FORMAT=csv",
|
|
||||||
"Globalstar" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=globalstar&FORMAT=csv",
|
|
||||||
"GNSS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=csv",
|
|
||||||
"Intelsat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=intelsat&FORMAT=csv",
|
|
||||||
"Iridium" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=iridium-NEXT&FORMAT=csv",
|
|
||||||
"Military" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=csv",
|
|
||||||
"New" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=last-30-days&FORMAT=csv",
|
|
||||||
"OneWeb" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=csv",
|
|
||||||
"Orbcomm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=orbcomm&FORMAT=csv",
|
|
||||||
"Resource" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=resource&FORMAT=csv",
|
|
||||||
"SatNOGS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv",
|
|
||||||
"Science" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=science&FORMAT=csv",
|
|
||||||
"Spire" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=spire&FORMAT=csv",
|
|
||||||
"Starlink" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=csv",
|
|
||||||
"Swarm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv",
|
|
||||||
"Weather" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv",
|
|
||||||
"X-Comm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv",
|
|
||||||
"Amsat" to "https://amsat.org/tle/current/nasabare.txt",
|
|
||||||
"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",
|
"R4UAB" to "https://r4uab.ru/satonline.txt",
|
||||||
|
"SatNOGS" to "https://db.satnogs.org/api/tle/?format=3le",
|
||||||
|
"ARISS" to "https://live.ariss.org/iss.txt",
|
||||||
"Other" to "" // key for sats filter
|
"Other" to "" // key for sats filter
|
||||||
)
|
)
|
||||||
val transceiversDataUrls = mapOf(
|
val transceiversDataUrls = mapOf(
|
||||||
"SatNOGS" to "https://db.satnogs.org/api/transmitters/?format=json&status=active"
|
"SatNOGS" to "https://db.satnogs.org/api/transmitters/?format=json&status=active"
|
||||||
)
|
)
|
||||||
|
val satelliteModes = listOf(
|
||||||
|
"4FSK", "64-QAM", "AFSK", "AFSK TUBiX10", "AHRPT", "AM", "APT", "ASK", "BPSK",
|
||||||
|
"BPSK PMT-A3", "CERTO", "CW", "DATV", "DBPSK", "DOKA", "DPSK", "DQPSK", "DSB", "DSTAR",
|
||||||
|
"DUV", "DVB-S2", "FFSK", "FM", "FMN", "FSK", "FSK AX.100 Mode 5", "FSK AX.100 Mode 6",
|
||||||
|
"FSK AX.25 G3RUH", "FT8", "GENESIS FSK", "GFSK", "GFSK Pkst", "GFSK Rktr", "GFSK/BPSK",
|
||||||
|
"GMSK", "GMSK USP", "HRPT", "LoRa", "LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5",
|
||||||
|
"MSK AX.100 Mode 6", "OFDM", "OQPSK", "PPM", "PSK", "PSK31", "PSK63", "QPSK", "QPSK31",
|
||||||
|
"QPSK63", "SIDLOC", "SQPSK", "SSDV", "SSTV", "UNKNOWN", "USB", "WSJT"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,4 @@ package com.rtbishop.look4sat.core.domain.usecase
|
|||||||
|
|
||||||
interface IShowToast {
|
interface IShowToast {
|
||||||
operator fun invoke(message: String)
|
operator fun invoke(message: String)
|
||||||
|
|
||||||
/** 按资源 ID 显示(四语文案) */
|
|
||||||
operator fun invoke(resId: Int)
|
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ class DataParser(private val dispatcher: CoroutineDispatcher) {
|
|||||||
val line1 = tle[1]
|
val line1 = tle[1]
|
||||||
val line2 = tle[2]
|
val line2 = tle[2]
|
||||||
OrbitalData(
|
OrbitalData(
|
||||||
name = tle[0].trim(),
|
name = tle[0].trim().removePrefix("0 "),
|
||||||
epoch = line1.substring(18, 32).toDouble(),
|
epoch = line1.substring(18, 32).toDouble(),
|
||||||
meanmo = line2.substring(52, 63).toDouble(),
|
meanmo = line2.substring(52, 63).toDouble(),
|
||||||
eccn = line2.substring(26, 33).toDouble() / 1e7,
|
eccn = line2.substring(26, 33).toDouble() / 1e7,
|
||||||
|
|||||||
+3
-5
@@ -18,12 +18,10 @@ import java.util.Locale
|
|||||||
*
|
*
|
||||||
* For a linear (passband) transponder, uplink and downlink frequencies are
|
* For a linear (passband) transponder, uplink and downlink frequencies are
|
||||||
* related by a fixed passband offset. When the satellite moves, both are
|
* related by a fixed passband offset. When the satellite moves, both are
|
||||||
* Doppler-shifted. Given one, we compute the other:
|
* Doppler-shifted. Given one, this computes the other:
|
||||||
*
|
*
|
||||||
* downlink → uplink: mapDownlinkToUplink (passband) → getUplinkFreq (Doppler)
|
* downlink → uplink: mapDownlinkToUplink (passband) → getUplinkFreq (Doppler)
|
||||||
* uplink → downlink: mapUplinkToDownlink (passband) → getDownlinkFreq (Doppler)
|
* uplink → downlink: mapUplinkToDownlink (passband) → getDownlinkFreq (Doppler)
|
||||||
*
|
|
||||||
* Addresses GitHub issue #91 (Custom frequency Doppler correction).
|
|
||||||
*/
|
*/
|
||||||
object DopplerFrequencyCalculator {
|
object DopplerFrequencyCalculator {
|
||||||
|
|
||||||
@@ -104,8 +102,8 @@ object DopplerFrequencyCalculator {
|
|||||||
* True for the radio entry that should drive the standalone Calculator page.
|
* True for the radio entry that should drive the standalone Calculator page.
|
||||||
*
|
*
|
||||||
* A frequency range alone is not enough: some non-user-facing or drifting data entries
|
* A frequency range alone is not enough: some non-user-facing or drifting data entries
|
||||||
* can also have low/high frequencies. The calculator is meant for the named linear
|
* can also have low/high frequencies. The calculator is meant for named linear
|
||||||
* transponder entry, e.g. "Linear Transponder", "Linear Transp.", "SSB Transponder".
|
* transponders, e.g. "Linear Transponder", "Linear Transp.", "SSB Transponder".
|
||||||
*/
|
*/
|
||||||
fun isNamedLinearTransponder(transponder: SatRadio): Boolean {
|
fun isNamedLinearTransponder(transponder: SatRadio): Boolean {
|
||||||
if (!isLinearTransponder(transponder)) return false
|
if (!isLinearTransponder(transponder)) return false
|
||||||
|
|||||||
+15
-106
@@ -19,78 +19,31 @@ package com.rtbishop.look4sat.core.domain.utility
|
|||||||
|
|
||||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a Maidenhead locator (QTH grid square) to a GeoPos.
|
|
||||||
* Supports 6-char (3 pair), 8-char (4 pair) and 10-char (5 pair) locators.
|
|
||||||
* The returned position is the center of the finest cell encoded by the locator:
|
|
||||||
* - 6 char: 5' lon x 2.5' lat cell center
|
|
||||||
* - 8 char: 30" lon x 15" lat cell center
|
|
||||||
* - 10 char: 1.25" lon x 0.625" lat cell center
|
|
||||||
*/
|
|
||||||
fun qthToPosition(locator: String): GeoPos? {
|
fun qthToPosition(locator: String): GeoPos? {
|
||||||
val trimmedQth = locator.trim().uppercase()
|
val trimmedQth = locator.take(6)
|
||||||
if (!isValidLocator(trimmedQth)) return null
|
if (!isValidLocator(trimmedQth)) return null
|
||||||
val lonFirst = (trimmedQth[0].code - 65) * 20
|
val lonFirst = (trimmedQth[0].uppercaseChar().code - 65) * 20
|
||||||
val latFirst = (trimmedQth[1].code - 65) * 10
|
val latFirst = (trimmedQth[1].uppercaseChar().code - 65) * 10
|
||||||
val lonSecond = trimmedQth[2].toString().toInt() * 2
|
val lonSecond = trimmedQth[2].toString().toInt() * 2
|
||||||
val latSecond = trimmedQth[3].toString().toInt()
|
val latSecond = trimmedQth[3].toString().toInt()
|
||||||
val lonThird = (trimmedQth[4].lowercaseChar().code - 97) / 12.0
|
val lonThird = (((trimmedQth[4].lowercaseChar().code - 97) / 12.0) + (1.0 / 24.0)) - 180
|
||||||
val latThird = (trimmedQth[5].lowercaseChar().code - 97) / 24.0
|
val latThird = (((trimmedQth[5].lowercaseChar().code - 97) / 24.0) + (1.0 / 48.0)) - 90
|
||||||
var longitude = lonFirst + lonSecond + lonThird - 180
|
val longitude = (lonFirst + lonSecond + lonThird).round(4)
|
||||||
var latitude = latFirst + latSecond + latThird - 90
|
val latitude = (latFirst + latSecond + latThird).round(4)
|
||||||
// 8-char extension: 4th pair, digits, 30" lon x 15" lat cells
|
return GeoPos(latitude, longitude)
|
||||||
if (trimmedQth.length >= 8) {
|
|
||||||
longitude += trimmedQth[6].toString().toInt() / 120.0
|
|
||||||
latitude += trimmedQth[7].toString().toInt() / 240.0
|
|
||||||
}
|
|
||||||
// 10-char extension: 5th pair, letters, 1.25" lon x 0.625" lat cells
|
|
||||||
if (trimmedQth.length >= 10) {
|
|
||||||
longitude += (trimmedQth[8].lowercaseChar().code - 97) / 2880.0
|
|
||||||
latitude += (trimmedQth[9].lowercaseChar().code - 97) / 5760.0
|
|
||||||
}
|
|
||||||
// Offset to the center of the finest encoded cell
|
|
||||||
when (trimmedQth.length) {
|
|
||||||
8 -> {
|
|
||||||
longitude += 1.0 / 240.0
|
|
||||||
latitude += 1.0 / 480.0
|
|
||||||
}
|
|
||||||
10 -> {
|
|
||||||
longitude += 1.0 / 5760.0
|
|
||||||
latitude += 1.0 / 11520.0
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
longitude += 1.0 / 24.0
|
|
||||||
latitude += 1.0 / 48.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return GeoPos(latitude.round(6), longitude.round(6))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun positionToQth(latitude: Double, longitude: Double): String? {
|
||||||
* Converts a GeoPos to a Maidenhead locator (QTH grid square).
|
|
||||||
* Default precision is 8 characters (4 pairs) giving 30" lon x 15" lat resolution,
|
|
||||||
* matching common 8-char grid square tools. Pass precision = 6 for the classic
|
|
||||||
* 5' x 2.5' resolution, or precision = 10 for the finest 1.25" x 0.625" resolution.
|
|
||||||
*/
|
|
||||||
fun positionToQth(latitude: Double, longitude: Double, precision: Int = 8): String? {
|
|
||||||
if (!isValidPosition(latitude, longitude)) return null
|
if (!isValidPosition(latitude, longitude)) return null
|
||||||
val newLongitude = longitude + 180
|
val newLongitude = if (longitude > 180.0) longitude else longitude + 180
|
||||||
val newLatitude = latitude + 90
|
val newLatitude = latitude + 90
|
||||||
val lonFirst = (65 + (newLongitude / 20).toInt().coerceIn(0, 17)).toChar()
|
val lonFirst = (65 + (newLongitude / 20)).toInt().toChar()
|
||||||
val latFirst = (65 + (newLatitude / 10).toInt().coerceIn(0, 17)).toChar()
|
val latFirst = (65 + (newLatitude / 10)).toInt().toChar()
|
||||||
val lonSecond = ((newLongitude % 20) / 2).toInt()
|
val lonSecond = ((newLongitude / 2) % 10).toInt()
|
||||||
val latSecond = (newLatitude % 10).toInt()
|
val latSecond = (newLatitude % 10).toInt()
|
||||||
val lonThird = (65 + (newLongitude % 2) * 12).toInt().toChar().lowercaseChar()
|
val lonThird = (65 + (newLongitude % 2) * 12).toInt().toChar().lowercaseChar()
|
||||||
val latThird = (65 + (newLatitude % 1) * 24).toInt().toChar().lowercaseChar()
|
val latThird = (65 + (newLatitude % 1) * 24).toInt().toChar().lowercaseChar()
|
||||||
val qth = "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
|
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
|
||||||
if (precision < 8) return qth
|
|
||||||
val lonFourth = ((newLongitude % (1.0 / 12.0)) * 120).toInt()
|
|
||||||
val latFourth = ((newLatitude % (1.0 / 24.0)) * 240).toInt()
|
|
||||||
val qth8 = "$qth$lonFourth$latFourth"
|
|
||||||
if (precision < 10) return qth8
|
|
||||||
val lonFifth = (65 + (newLongitude % (1.0 / 120.0)) * 2880).toInt().toChar().lowercaseChar()
|
|
||||||
val latFifth = (65 + (newLatitude % (1.0 / 240.0)) * 5760).toInt().toChar().lowercaseChar()
|
|
||||||
return "$qth8$lonFifth$latFifth"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValidPosition(lat: Double, lon: Double): Boolean {
|
private fun isValidPosition(lat: Double, lon: Double): Boolean {
|
||||||
@@ -98,49 +51,5 @@ private fun isValidPosition(lat: Double, lon: Double): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isValidLocator(locator: String): Boolean {
|
private fun isValidLocator(locator: String): Boolean {
|
||||||
return locator.matches("[a-xA-X]{2}\\d{2}[a-xA-X]{2}(?:\\d{2}(?:[a-xA-X]{2})?)?".toRegex())
|
return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the 4-char field+square part of a locator, e.g. "OL42ih45" -> "OL42".
|
|
||||||
* A 4-char input is returned as-is when valid.
|
|
||||||
*/
|
|
||||||
fun qthToSquare(locator: String): String {
|
|
||||||
val upper = locator.trim().uppercase()
|
|
||||||
return when {
|
|
||||||
upper.length >= 4 && isValidLocator(upper) -> upper.take(4)
|
|
||||||
upper.length == 4 && upper.matches("[a-xA-X]{2}\\d{2}".toRegex()) -> upper
|
|
||||||
else -> "----"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the 3x3 grid of 4-char squares surrounding [square] (e.g. "OL42").
|
|
||||||
* Row 0 = north (lat +1), col 0 = west (lon -1). Handles field/square carry
|
|
||||||
* at boundaries (e.g. "AA00" wraps to "RR99" at the south-west corner).
|
|
||||||
* Mirrors the neighbor logic decompiled from the QTH定位器 app.
|
|
||||||
*/
|
|
||||||
fun qthNeighbors(square: String): List<String> {
|
|
||||||
if (square.length != 4) return emptyList()
|
|
||||||
val lonField = (square[0].uppercaseChar().code - 65).coerceIn(0, 17)
|
|
||||||
val latField = (square[1].uppercaseChar().code - 65).coerceIn(0, 17)
|
|
||||||
val lonSquare = square[2].digitToCharOrNull() ?: return emptyList()
|
|
||||||
val latSquare = square[3].digitToCharOrNull() ?: return emptyList()
|
|
||||||
val result = mutableListOf<String>()
|
|
||||||
for (dLat in 1 downTo -1) { // north -> south
|
|
||||||
for (dLon in -1..1) { // west -> east
|
|
||||||
var lf = lonField
|
|
||||||
var tf = latField
|
|
||||||
var ls = lonSquare + dLon
|
|
||||||
var ts = latSquare + dLat
|
|
||||||
if (ls < 0) { lf -= 1; ls = 9 } else if (ls > 9) { lf += 1; ls = 0 }
|
|
||||||
if (ts < 0) { tf -= 1; ts = 9 } else if (ts > 9) { tf += 1; ts = 0 }
|
|
||||||
lf = (lf + 18) % 18
|
|
||||||
tf = (tf + 18) % 18
|
|
||||||
result += "${('A' + lf).toChar()}${('A' + tf).toChar()}$ls$ts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Char.digitToCharOrNull(): Int? = digitToIntOrNull()
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
/*
|
|
||||||
* WaveLogApi.kt — WaveLog 日志服务器 API 客户端(4.5.2 覆盖修复 2)。
|
|
||||||
*
|
|
||||||
* 同时支持 v1 与 v2(用户服务器实测只有 v1, v2 返回 404):
|
|
||||||
* v2: POST {base}/api/v2/qso (Authorization: Bearer + JSON 字段)
|
|
||||||
* v1: POST {base}/index.php/api/qso (key 在 JSON body + ADIF 字符串)
|
|
||||||
* 策略: 优先 v2, 404 自动降级 v1。
|
|
||||||
* 测试连接: v2 GET api/v2/token → 404 时 v1 POST api/get_contacts_adif。
|
|
||||||
* 站点网格: 仅 v2 有 GET api/v2/station/{id}; v1 无此端点 → 降级用用户 QTH。
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.wavelog
|
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.io.OutputStreamWriter
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
/** 站点信息(GET /api/v2/station/{id} 结果) */
|
|
||||||
data class WavelogStation(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
val callsign: String,
|
|
||||||
val gridsquare: String
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed class WavelogResult {
|
|
||||||
data class Success(val message: String) : WavelogResult()
|
|
||||||
data class Failure(val message: String) : WavelogResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
object WaveLogApi {
|
|
||||||
|
|
||||||
private const val TIMEOUT_MS = 15000
|
|
||||||
|
|
||||||
/** 规范化服务器地址: 去尾斜杠/去尾部 index.php; 无协议前缀时补 https:// */
|
|
||||||
fun normalizeUrl(raw: String): String {
|
|
||||||
var u = raw.trim().trimEnd('/')
|
|
||||||
if (u.isBlank()) return ""
|
|
||||||
if (!u.startsWith("http://") && !u.startsWith("https://")) u = "https://$u"
|
|
||||||
if (u.endsWith("/index.php")) u = u.removeSuffix("/index.php")
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 测试连接: v2 GET api/v2/token → 404 时 v1 POST api/get_contacts_adif */
|
|
||||||
suspend fun testToken(url: String, apiKey: String, stationId: String = ""): WavelogResult = withContext(Dispatchers.IO) {
|
|
||||||
val base = normalizeUrl(url)
|
|
||||||
if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空")
|
|
||||||
|
|
||||||
// v2: GET /index.php/api/v2/token
|
|
||||||
val v2 = httpRequest("$base/index.php/api/v2/token", "GET", apiKey, null)
|
|
||||||
if (v2.first in 200..299) return@withContext WavelogResult.Success("连接成功 (API v2)")
|
|
||||||
|
|
||||||
// v1: POST /index.php/api/get_contacts_adif(key 在 body)
|
|
||||||
if (stationId.isNotBlank()) {
|
|
||||||
val body = JSONObject().apply {
|
|
||||||
put("key", apiKey)
|
|
||||||
put("station_id", stationId)
|
|
||||||
put("fetchfromid", 0)
|
|
||||||
}.toString()
|
|
||||||
val v1 = httpRequest("$base/index.php/api/get_contacts_adif", "POST", apiKey, body)
|
|
||||||
if (v1.first in 200..299) return@withContext WavelogResult.Success("连接成功 (API v1)")
|
|
||||||
if (v1.first == 401) return@withContext WavelogResult.Failure("API 密钥无效 (v1: 401)")
|
|
||||||
}
|
|
||||||
// 无 index.php 的 v1 尝试
|
|
||||||
val body = JSONObject().apply {
|
|
||||||
put("key", apiKey)
|
|
||||||
put("station_id", stationId)
|
|
||||||
put("fetchfromid", 0)
|
|
||||||
}.toString()
|
|
||||||
val v1b = httpRequest("$base/api/get_contacts_adif", "POST", apiKey, body)
|
|
||||||
if (v1b.first in 200..299) return@withContext WavelogResult.Success("连接成功 (API v1)")
|
|
||||||
if (v1b.first == 401) return@withContext WavelogResult.Failure("API 密钥无效 (v1: 401)")
|
|
||||||
|
|
||||||
WavelogResult.Failure("连接失败: v2 HTTP ${v2.first}, v1 HTTP ${v1b.first} — 请确认服务器地址/密钥正确")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 站点信息: 仅 v2; v1 无此端点返回 null 标记(网格检测降级用用户 QTH) */
|
|
||||||
suspend fun getStation(url: String, apiKey: String, stationId: String): WavelogResult = withContext(Dispatchers.IO) {
|
|
||||||
val base = normalizeUrl(url)
|
|
||||||
if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空")
|
|
||||||
val (code, resp) = httpRequest("$base/index.php/api/v2/station/$stationId", "GET", apiKey, null)
|
|
||||||
if (code in 200..299) {
|
|
||||||
return@withContext try {
|
|
||||||
val obj = JSONObject(resp)
|
|
||||||
val data = obj.optJSONObject("data") ?: obj
|
|
||||||
val station = WavelogStation(
|
|
||||||
id = data.optInt("id"),
|
|
||||||
name = data.optString("name"),
|
|
||||||
callsign = data.optString("callsign"),
|
|
||||||
gridsquare = data.optString("gridsquare")
|
|
||||||
)
|
|
||||||
WavelogResult.Success(JSONObject().apply {
|
|
||||||
put("id", station.id); put("name", station.name)
|
|
||||||
put("callsign", station.callsign); put("gridsquare", station.gridsquare)
|
|
||||||
}.toString())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
WavelogResult.Failure("解析失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// v1 无 station 端点 → 返回 Success 空(调用方降级用用户 QTH)
|
|
||||||
WavelogResult.Success("")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** LoTW 认可的卫星名: 取括号前主名 + 大写(ISS 特判) */
|
|
||||||
fun normalizeSatName(raw: String): String {
|
|
||||||
val main = raw.substringBefore('(').trim()
|
|
||||||
.ifBlank { raw.trim() }
|
|
||||||
.uppercase(Locale.ENGLISH)
|
|
||||||
// ISS 特判: 主名是 ISS/ZARYA/ARISS 变体 → ISS(LoTW 认可名)
|
|
||||||
return when {
|
|
||||||
main == "ZARYA" || main.startsWith("ISS") -> "ISS"
|
|
||||||
main == "ARISS" -> "ISS"
|
|
||||||
else -> main
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 创建 QSO: 优先 v2, 404 降级 v1(ADIF) */
|
|
||||||
suspend fun postQso(
|
|
||||||
url: String,
|
|
||||||
apiKey: String,
|
|
||||||
stationProfileId: String,
|
|
||||||
qso: WavelogQso,
|
|
||||||
gridsquare: String
|
|
||||||
): WavelogResult = withContext(Dispatchers.IO) {
|
|
||||||
val base = normalizeUrl(url)
|
|
||||||
if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空")
|
|
||||||
|
|
||||||
val satName = normalizeSatName(qso.satName)
|
|
||||||
|
|
||||||
// v2: POST /index.php/api/v2/qso(JSON 字段)
|
|
||||||
val v2Body = JSONObject().apply {
|
|
||||||
put("station_profile_id", stationProfileId.toIntOrNull() ?: 0)
|
|
||||||
put("call", qso.call)
|
|
||||||
put("band", "SAT")
|
|
||||||
put("mode", qso.mode)
|
|
||||||
put("qso_date", utcDate(qso.timeUtcMs))
|
|
||||||
put("time_on", utcTime(qso.timeUtcMs))
|
|
||||||
put("freq", qso.freqTxHz)
|
|
||||||
put("freq_rx", qso.freqRxHz)
|
|
||||||
put("gridsquare", gridsquare)
|
|
||||||
put("sat_name", satName)
|
|
||||||
}
|
|
||||||
val (code, resp) = httpRequest("$base/index.php/api/v2/qso", "POST", apiKey, v2Body.toString())
|
|
||||||
if (code in 200..299) return@withContext WavelogResult.Success("已上传 (v2)")
|
|
||||||
if (code == 409) return@withContext WavelogResult.Success("重复(已存在)")
|
|
||||||
|
|
||||||
// v1: POST /index.php/api/qso(key 在 body + ADIF)
|
|
||||||
val v1Body = JSONObject().apply {
|
|
||||||
put("key", apiKey)
|
|
||||||
put("station_profile_id", stationProfileId)
|
|
||||||
put("type", "adif")
|
|
||||||
put("string", toAdif(qso, gridsquare, satName))
|
|
||||||
}
|
|
||||||
val (code1, resp1) = httpRequest("$base/index.php/api/qso", "POST", apiKey, v1Body.toString())
|
|
||||||
if (code1 in 200..299) return@withContext WavelogResult.Success("已上传 (v1)")
|
|
||||||
|
|
||||||
// 无 index.php 的 v1
|
|
||||||
val (code1b, resp1b) = httpRequest("$base/api/qso", "POST", apiKey, v1Body.toString())
|
|
||||||
if (code1b in 200..299) return@withContext WavelogResult.Success("已上传 (v1)")
|
|
||||||
|
|
||||||
WavelogResult.Failure("上传失败: v2 HTTP $code, v1 HTTP $code1 — ${shortError(resp1.ifBlank { resp1b })}")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** v1 ADIF 字符串(频率 MHz, 长度=UTF-8 字节数, sat_name 已规范化) */
|
|
||||||
private fun toAdif(qso: WavelogQso, gridsquare: String, satName: String): String {
|
|
||||||
fun field(name: String, value: String): String {
|
|
||||||
val bytes = value.toByteArray(Charsets.UTF_8).size
|
|
||||||
return "<$name:$bytes>$value"
|
|
||||||
}
|
|
||||||
return buildString {
|
|
||||||
append(field("call", qso.call))
|
|
||||||
append(field("band", "SAT"))
|
|
||||||
append(field("mode", qso.mode))
|
|
||||||
append(field("freq", String.format(Locale.ENGLISH, "%.6f", qso.freqTxHz / 1_000_000.0)))
|
|
||||||
if (qso.freqRxHz > 0) {
|
|
||||||
append(field("freq_rx", String.format(Locale.ENGLISH, "%.6f", qso.freqRxHz / 1_000_000.0)))
|
|
||||||
}
|
|
||||||
append(field("qso_date", utcDateCompact(qso.timeUtcMs)))
|
|
||||||
append(field("time_on", utcTimeCompact(qso.timeUtcMs)))
|
|
||||||
if (gridsquare.isNotBlank()) append(field("gridsquare", gridsquare.take(4)))
|
|
||||||
if (satName.isNotBlank()) {
|
|
||||||
append(field("sat_name", satName))
|
|
||||||
append(field("prop_mode", "SAT"))
|
|
||||||
}
|
|
||||||
append("<eor>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 通用 HTTP 请求(返回 code + body) */
|
|
||||||
private fun httpRequest(url: String, method: String, apiKey: String, jsonBody: String?): Pair<Int, String> {
|
|
||||||
return try {
|
|
||||||
val conn = URL(url).openConnection() as HttpURLConnection
|
|
||||||
conn.requestMethod = method
|
|
||||||
conn.connectTimeout = TIMEOUT_MS
|
|
||||||
conn.readTimeout = TIMEOUT_MS
|
|
||||||
if (apiKey.isNotBlank()) conn.setRequestProperty("Authorization", "Bearer $apiKey")
|
|
||||||
if (jsonBody != null) {
|
|
||||||
conn.doOutput = true
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json")
|
|
||||||
conn.setRequestProperty("Accept", "application/json")
|
|
||||||
OutputStreamWriter(conn.outputStream, Charsets.UTF_8).use { it.write(jsonBody) }
|
|
||||||
}
|
|
||||||
val code = conn.responseCode
|
|
||||||
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
|
|
||||||
val body = if (stream != null) {
|
|
||||||
BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).use { it.readText() }
|
|
||||||
} else ""
|
|
||||||
code to body
|
|
||||||
} catch (e: Exception) {
|
|
||||||
-1 to (e.message ?: e.javaClass.simpleName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shortError(body: String): String {
|
|
||||||
if (body.startsWith("<")) return body.take(80) // HTML 错误页
|
|
||||||
return try {
|
|
||||||
val obj = JSONObject(body)
|
|
||||||
val err = obj.optJSONObject("error")
|
|
||||||
err?.optString("message")?.ifBlank { body.take(120) }
|
|
||||||
?: obj.optString("reason").ifBlank { obj.optString("message").ifBlank { body.take(120) } }
|
|
||||||
} catch (_: Exception) {
|
|
||||||
body.take(120)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun utcDate(ms: Long): String {
|
|
||||||
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
|
|
||||||
cal.timeInMillis = ms
|
|
||||||
return "%04d-%02d-%02d".format(
|
|
||||||
cal.get(java.util.Calendar.YEAR), cal.get(java.util.Calendar.MONTH) + 1,
|
|
||||||
cal.get(java.util.Calendar.DAY_OF_MONTH)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun utcTime(ms: Long): String {
|
|
||||||
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
|
|
||||||
cal.timeInMillis = ms
|
|
||||||
return "%02d:%02d:%02d".format(
|
|
||||||
cal.get(java.util.Calendar.HOUR_OF_DAY), cal.get(java.util.Calendar.MINUTE),
|
|
||||||
cal.get(java.util.Calendar.SECOND)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun utcDateCompact(ms: Long): String {
|
|
||||||
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
|
|
||||||
cal.timeInMillis = ms
|
|
||||||
return "%04d%02d%02d".format(
|
|
||||||
cal.get(java.util.Calendar.YEAR), cal.get(java.util.Calendar.MONTH) + 1,
|
|
||||||
cal.get(java.util.Calendar.DAY_OF_MONTH)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun utcTimeCompact(ms: Long): String {
|
|
||||||
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
|
|
||||||
cal.timeInMillis = ms
|
|
||||||
return "%02d%02d%02d".format(
|
|
||||||
cal.get(java.util.Calendar.HOUR_OF_DAY), cal.get(java.util.Calendar.MINUTE),
|
|
||||||
cal.get(java.util.Calendar.SECOND)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* WavelogQueue.kt — WaveLog 本地日志队列(4.5.2)。
|
|
||||||
*
|
|
||||||
* 纯 Kotlin(不依赖 Android): 存储走 IWavelogQueueStore 接口,
|
|
||||||
* 由 core/data 用 SharedPreferences 实现。
|
|
||||||
* 队列上限 500 条(超出丢最旧)。
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.wavelog
|
|
||||||
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
/** 存储抽象(SharedPreferences 实现见 core/data) */
|
|
||||||
interface IWavelogQueueStore {
|
|
||||||
fun load(): String
|
|
||||||
fun save(json: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 待上传的 QSO 条目(本地队列元素,与 POST /api/v2/qso 字段对应) */
|
|
||||||
data class WavelogQso(
|
|
||||||
val id: String, // 本地唯一 id(UUID)
|
|
||||||
val timeUtcMs: Long, // 回车时刻 UTC 毫秒(本地显示 + 组装 qso_date/time_on)
|
|
||||||
val call: String,
|
|
||||||
val mode: String,
|
|
||||||
val freqTxHz: Long, // 上行(回车那一秒多普勒修正)
|
|
||||||
val freqRxHz: Long, // 下行
|
|
||||||
val satName: String,
|
|
||||||
val sessionId: String = "", // 场次 ID: 卫星名-AOS 时间戳(过境仰角 0 秒), 空=未分组(旧数据)
|
|
||||||
val uploaded: Boolean = false // 是否已成功上传(4.5.2 修复: 成功后保留标记, 表格打勾)
|
|
||||||
)
|
|
||||||
|
|
||||||
class WavelogQueue(private val store: IWavelogQueueStore) {
|
|
||||||
|
|
||||||
private val key = "wavelog_queue"
|
|
||||||
|
|
||||||
fun all(): List<WavelogQso> {
|
|
||||||
val raw = store.load()
|
|
||||||
return try {
|
|
||||||
val arr = JSONArray(raw)
|
|
||||||
(0 until arr.length()).map { i ->
|
|
||||||
val o = arr.getJSONObject(i)
|
|
||||||
WavelogQso(
|
|
||||||
id = o.getString("id"),
|
|
||||||
timeUtcMs = o.getLong("timeUtcMs"),
|
|
||||||
call = o.optString("call"),
|
|
||||||
mode = o.optString("mode"),
|
|
||||||
freqTxHz = o.optLong("freqTxHz"),
|
|
||||||
freqRxHz = o.optLong("freqRxHz"),
|
|
||||||
satName = o.optString("satName"),
|
|
||||||
sessionId = o.optString("sessionId"),
|
|
||||||
uploaded = o.optBoolean("uploaded", false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { emptyList() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun add(qso: WavelogQso) {
|
|
||||||
val list = all().toMutableList()
|
|
||||||
list.add(0, qso) // 最新在前
|
|
||||||
if (list.size > 500) list.removeAt(list.size - 1)
|
|
||||||
save(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun remove(id: String) {
|
|
||||||
save(all().filter { it.id != id })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun removeAll(ids: Set<String>) {
|
|
||||||
save(all().filter { it.id !in ids })
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 标记为已上传(保留在队列, 表格打勾) */
|
|
||||||
@Synchronized
|
|
||||||
fun markUploaded(id: String) {
|
|
||||||
save(all().map { if (it.id == id) it.copy(uploaded = true) else it })
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 移除所有已上传条目(可选项, 保持队列精简) */
|
|
||||||
@Synchronized
|
|
||||||
fun removeUploaded() {
|
|
||||||
save(all().filter { !it.uploaded })
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun save(list: List<WavelogQso>) {
|
|
||||||
val arr = JSONArray()
|
|
||||||
list.forEach { q ->
|
|
||||||
arr.put(JSONObject().apply {
|
|
||||||
put("id", q.id); put("timeUtcMs", q.timeUtcMs); put("call", q.call)
|
|
||||||
put("mode", q.mode); put("freqTxHz", q.freqTxHz)
|
|
||||||
put("freqRxHz", q.freqRxHz); put("satName", q.satName)
|
|
||||||
put("sessionId", q.sessionId)
|
|
||||||
put("uploaded", q.uploaded)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
store.save(arr.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-91
@@ -1,91 +0,0 @@
|
|||||||
/*
|
|
||||||
* WavelogUploader.kt — WaveLog 队列上传调度(4.5.2)。
|
|
||||||
*
|
|
||||||
* 手动/周期上传共用: 逐条网格检测(用户 QTH 前 4 位 vs 台站网格前 4 位)
|
|
||||||
* → POST /api/v2/qso → 成功移出队列。
|
|
||||||
* 网格不一致: 返回 NeedConfirm(由 UI 弹窗「忽略并上传/取消」),
|
|
||||||
* 确认后带 force=true 重试本批。
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.wavelog
|
|
||||||
|
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
sealed class UploadOutcome {
|
|
||||||
data class NeedConfirm(val stationGrid: String, val userGrid: String) : UploadOutcome()
|
|
||||||
data class Done(
|
|
||||||
val successCount: Int,
|
|
||||||
val failedCount: Int,
|
|
||||||
val message: String,
|
|
||||||
val firstError: String = ""
|
|
||||||
) : UploadOutcome()
|
|
||||||
}
|
|
||||||
|
|
||||||
class WavelogUploader(
|
|
||||||
private val settingsRepo: ISettingsRepo,
|
|
||||||
private val queue: WavelogQueue
|
|
||||||
) {
|
|
||||||
|
|
||||||
// 台站网格缓存(每次上传前刷新; 失败用旧值)
|
|
||||||
private var cachedStationGrid: String? = null
|
|
||||||
|
|
||||||
/** 上传整个队列。force=true 跳过网格确认(用户已选「忽略并上传」) */
|
|
||||||
suspend fun uploadQueue(force: Boolean = false): UploadOutcome {
|
|
||||||
val settings = settingsRepo.otherSettings.value
|
|
||||||
val url = settings.wavelogUrl
|
|
||||||
val apiKey = settings.wavelogApiKey
|
|
||||||
val stationId = settings.wavelogStationId
|
|
||||||
if (url.isBlank() || apiKey.isBlank() || stationId.isBlank()) {
|
|
||||||
return UploadOutcome.Done(0, queue.all().size, "未配置 WaveLog 服务器")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 拉站点信息(拿台站网格); v1 无此端点时降级用用户 QTH
|
|
||||||
val stationGrid = getStationGrid(url, apiKey, stationId) ?: userQthGrid()
|
|
||||||
if (stationGrid.isNullOrBlank()) {
|
|
||||||
return UploadOutcome.Done(0, queue.all().size, "无法获取站点信息(检查站点 ID/密钥权限)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 网格检测: 用户当前 QTH 前 4 位 vs 台站网格前 4 位(v1 降级时网格相同, 跳过)
|
|
||||||
if (!force) {
|
|
||||||
val userGrid = userQthGrid()
|
|
||||||
if (userGrid != null && stationGrid.take(4).lowercase() != userGrid.take(4).lowercase()) {
|
|
||||||
return UploadOutcome.NeedConfirm(stationGrid, userGrid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 逐条上传(成功后标记 uploaded, 保留在本地供日志页打勾)
|
|
||||||
val entries = queue.all()
|
|
||||||
var ok = 0
|
|
||||||
var fail = 0
|
|
||||||
var firstError = ""
|
|
||||||
for (qso in entries) {
|
|
||||||
if (qso.uploaded) { ok++; continue }
|
|
||||||
val result = WaveLogApi.postQso(url, apiKey, stationId, qso, stationGrid)
|
|
||||||
if (result is WavelogResult.Success) {
|
|
||||||
ok++
|
|
||||||
queue.markUploaded(qso.id)
|
|
||||||
} else {
|
|
||||||
fail++
|
|
||||||
if (firstError.isBlank()) firstError = (result as? WavelogResult.Failure)?.message ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val message = if (fail == 0) "成功上传 $ok 条" else "成功 $ok 条, 失败 $fail 条(保留待重试)"
|
|
||||||
return UploadOutcome.Done(ok, fail, message, firstError)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getStationGrid(url: String, apiKey: String, stationId: String): String? {
|
|
||||||
val result = WaveLogApi.getStation(url, apiKey, stationId)
|
|
||||||
if (result is WavelogResult.Success) {
|
|
||||||
return try {
|
|
||||||
JSONObject(result.message).optString("gridsquare").takeIf { it.isNotBlank() }
|
|
||||||
?: cachedStationGrid
|
|
||||||
} catch (_: Exception) { cachedStationGrid }
|
|
||||||
}
|
|
||||||
return cachedStationGrid
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 用户当前 QTH 网格(前 4 位; 无 QTH 返回 null = 跳过检测) */
|
|
||||||
private fun userQthGrid(): String? {
|
|
||||||
return settingsRepo.stationPosition.value.qthLocator?.takeIf { it.length >= 4 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
-11
@@ -3,7 +3,11 @@ package com.rtbishop.look4sat.core.domain
|
|||||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||||
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
|
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DopplerFrequencyCalculatorTest {
|
class DopplerFrequencyCalculatorTest {
|
||||||
@@ -79,8 +83,6 @@ class DopplerFrequencyCalculatorTest {
|
|||||||
val orbitalPos = pos(0.0)
|
val orbitalPos = pos(0.0)
|
||||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
||||||
assertNotNull(uplink)
|
assertNotNull(uplink)
|
||||||
assertTrue(uplink!! > 0)
|
|
||||||
// With zero Doppler, result equals mapDownlinkToUplink output
|
|
||||||
assertEquals(145_200_000L, uplink)
|
assertEquals(145_200_000L, uplink)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,24 +97,23 @@ class DopplerFrequencyCalculatorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun computeUplinkFromDownlink_withDoppler_positiveRangeRate() {
|
fun computeUplinkFromDownlink_withDoppler_positiveRangeRate() {
|
||||||
// Satellite receding (positive range rate) → ground must transmit higher freq to compensate
|
// Satellite receding (positive range rate) → ground must transmit higher freq to compensate.
|
||||||
val xpdr = linearTransponder()
|
val xpdr = linearTransponder()
|
||||||
val orbitalPos = pos(7.0) // ~7 km/s receding
|
val orbitalPos = pos(7.0)
|
||||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
||||||
assertNotNull(uplink)
|
assertNotNull(uplink)
|
||||||
// Uplink freq should be Doppler shifted UP (compensating for receding)
|
|
||||||
assertTrue(uplink!! > 145_200_000L)
|
assertTrue(uplink!! > 145_200_000L)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun computeUplinkFromDownlink_fm_transponder_returnsNull() {
|
fun computeUplinkFromDownlink_fmTransponder_returnsNull() {
|
||||||
val orbitalPos = pos()
|
val orbitalPos = pos()
|
||||||
val result = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_600_000L, fmTransponder(), orbitalPos)
|
val result = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_600_000L, fmTransponder(), orbitalPos)
|
||||||
assertNull(result)
|
assertNull(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun computeDownlinkFromUplink_fm_transponder_returnsNull() {
|
fun computeDownlinkFromUplink_fmTransponder_returnsNull() {
|
||||||
val orbitalPos = pos()
|
val orbitalPos = pos()
|
||||||
val result = DopplerFrequencyCalculator.computeDownlinkFromUplink(145_900_000L, fmTransponder(), orbitalPos)
|
val result = DopplerFrequencyCalculator.computeDownlinkFromUplink(145_900_000L, fmTransponder(), orbitalPos)
|
||||||
assertNull(result)
|
assertNull(result)
|
||||||
@@ -171,13 +172,11 @@ class DopplerFrequencyCalculatorTest {
|
|||||||
val orbitalPos = pos(0.0)
|
val orbitalPos = pos(0.0)
|
||||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
||||||
assertNotNull(uplink)
|
assertNotNull(uplink)
|
||||||
// Inverted: offset from high end → maps to high end of uplink
|
|
||||||
assertEquals(145_300_000L, uplink)
|
assertEquals(145_300_000L, uplink)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun computeUplinkFromDownlink_roundTrip() {
|
fun computeUplinkFromDownlink_roundTrip() {
|
||||||
// downlink → uplink → downlink should round-trip
|
|
||||||
val xpdr = linearTransponder()
|
val xpdr = linearTransponder()
|
||||||
val orbitalPos = pos(3.5)
|
val orbitalPos = pos(3.5)
|
||||||
val originalDownlink = 435_250_000L
|
val originalDownlink = 435_250_000L
|
||||||
@@ -185,7 +184,6 @@ class DopplerFrequencyCalculatorTest {
|
|||||||
assertNotNull(uplink)
|
assertNotNull(uplink)
|
||||||
val roundTripDownlink = DopplerFrequencyCalculator.computeDownlinkFromUplink(uplink!!, xpdr, orbitalPos)
|
val roundTripDownlink = DopplerFrequencyCalculator.computeDownlinkFromUplink(uplink!!, xpdr, orbitalPos)
|
||||||
assertNotNull(roundTripDownlink)
|
assertNotNull(roundTripDownlink)
|
||||||
// Doppler round-trip: small residual due to freq-dependent Doppler
|
|
||||||
val error = kotlin.math.abs(roundTripDownlink!! - originalDownlink)
|
val error = kotlin.math.abs(roundTripDownlink!! - originalDownlink)
|
||||||
assertTrue("Round-trip error too large: $error", error < 10000)
|
assertTrue("Round-trip error too large: $error", error < 10000)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,7 @@
|
|||||||
package com.rtbishop.look4sat.core.domain
|
package com.rtbishop.look4sat.core.domain
|
||||||
|
|
||||||
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
||||||
import com.rtbishop.look4sat.core.domain.utility.qthNeighbors
|
|
||||||
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
|
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
|
||||||
import com.rtbishop.look4sat.core.domain.utility.qthToSquare
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class QthConverterTest {
|
class QthConverterTest {
|
||||||
@@ -28,36 +26,21 @@ class QthConverterTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Given valid QTH returns correct POS`() {
|
fun `Given valid QTH returns correct POS`() {
|
||||||
var result = qthToPosition("io91VL39FX")
|
var result = qthToPosition("io91VL39FX")
|
||||||
assert(result?.latitude == 51.499913 && result.longitude == -0.22309)
|
assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
|
||||||
result = qthToPosition("gf15vc")
|
result = qthToPosition("gf15vc")
|
||||||
assert(result?.latitude == -34.895833 && result.longitude == -56.208333)
|
assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
|
||||||
// 8-char locators: finer 30" x 15" cell center
|
|
||||||
result = qthToPosition("io91vl47")
|
|
||||||
assert(result?.latitude == 51.489583 && result.longitude == -0.2125)
|
|
||||||
result = qthToPosition("jn58td25")
|
|
||||||
assert(result?.latitude == 48.147917 && result.longitude == 11.604167)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Given invalid QTH returns null`() {
|
fun `Given invalid QTH returns null`() {
|
||||||
assert(qthToPosition("ZZ00zz") == null)
|
assert(qthToPosition("ZZ00zz") == null)
|
||||||
assert(qthToPosition("JN58") == null)
|
assert(qthToPosition("JN58") == null)
|
||||||
assert(qthToPosition("io9") == null)
|
|
||||||
assert(qthToPosition("IO91VL7") == null)
|
|
||||||
assert(qthToPosition("IO91VL4X") == null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Given valid POS returns correct QTH`() {
|
fun `Given valid POS returns correct QTH`() {
|
||||||
// default precision is 8 chars
|
assert(positionToQth(51.4878, -0.2146) == "IO91vl")
|
||||||
assert(positionToQth(51.4878, -0.2146) == "IO91vl47")
|
assert(positionToQth(48.1466, 11.6083) == "JN58td")
|
||||||
assert(positionToQth(48.1466, 11.6083) == "JN58td25")
|
|
||||||
// 6-char precision still available for backwards compatibility
|
|
||||||
assert(positionToQth(51.4878, -0.2146, 6) == "IO91vl")
|
|
||||||
assert(positionToQth(48.1466, 11.6083, 6) == "JN58td")
|
|
||||||
// 10-char precision
|
|
||||||
assert(positionToQth(51.4878, -0.2146, 10) == "IO91vl47fb")
|
|
||||||
assert(positionToQth(48.1466, 11.6083, 10) == "JN58td25xe")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -65,61 +48,4 @@ class QthConverterTest {
|
|||||||
assert(positionToQth(91.0542, -170.1142) == null)
|
assert(positionToQth(91.0542, -170.1142) == null)
|
||||||
assert(positionToQth(89.0542, -240.1142) == null)
|
assert(positionToQth(89.0542, -240.1142) == null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Given boundary POS stays in valid grid`() {
|
|
||||||
// antipodal / edge cases must not overflow the A-R / 0-9 / a-x alphabet
|
|
||||||
assert(positionToQth(-90.0, -180.0, 8) == "AA00aa00")
|
|
||||||
assert(positionToQth(90.0, 180.0, 8) == "RR00aa00")
|
|
||||||
assert(positionToQth(0.0, 0.0, 8) == "JJ00aa00")
|
|
||||||
// roundtrip stability: 8-char roundtrip is stable across a sample of positions
|
|
||||||
val positions = listOf(
|
|
||||||
Pair(51.4878, -0.2146), Pair(48.1466, 11.6083), Pair(-33.8688, 151.2093),
|
|
||||||
Pair(39.9042, 116.4074), Pair(35.6895, 139.6917), Pair(41.714, -72.727)
|
|
||||||
)
|
|
||||||
positions.forEach { (lat, lon) ->
|
|
||||||
val qth = positionToQth(lat, lon, 8)
|
|
||||||
val pos = qthToPosition(qth!!)
|
|
||||||
val qth2 = positionToQth(pos!!.latitude, pos.longitude, 8)
|
|
||||||
assert(qth == qth2) { "Roundtrip failed for ($lat, $lon): $qth -> $qth2" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Given square returns correct 3x3 neighbors`() {
|
|
||||||
// Reference grid from the QTH定位器 screenshot: OL42
|
|
||||||
val neighbors = qthNeighbors("OL42")
|
|
||||||
assert(neighbors == listOf(
|
|
||||||
"OL33", "OL43", "OL53",
|
|
||||||
"OL32", "OL42", "OL52",
|
|
||||||
"OL31", "OL41", "OL51"
|
|
||||||
)) { "OL42 grid mismatch: $neighbors" }
|
|
||||||
// Center cell must be the input itself
|
|
||||||
assert(neighbors[4] == "OL42")
|
|
||||||
// 9 cells, all distinct
|
|
||||||
assert(neighbors.size == 9 && neighbors.toSet().size == 9)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Given boundary square wraps fields correctly`() {
|
|
||||||
// South-west corner: AA00 neighbors wrap to RR99 / RA90 etc.
|
|
||||||
val sw = qthNeighbors("AA00")
|
|
||||||
assert(sw.size == 9 && sw.toSet().size == 9)
|
|
||||||
assert(sw[0] == "RA91" && sw[4] == "AA00" && sw[6] == "RR99" && sw[8] == "AR19")
|
|
||||||
// North-east corner: RR99 wraps to AA00
|
|
||||||
val ne = qthNeighbors("RR99")
|
|
||||||
assert(ne.size == 9 && ne.toSet().size == 9)
|
|
||||||
assert(ne[0] == "RA80" && ne[4] == "RR99" && ne[8] == "AR08")
|
|
||||||
// Field boundary: IO91's east neighbors cross into J field
|
|
||||||
val london = qthNeighbors("IO91")
|
|
||||||
assert(london[2] == "JO02" && london[5] == "JO01")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Given full locator returns square part`() {
|
|
||||||
assert(qthToSquare("OL42ih45") == "OL42")
|
|
||||||
assert(qthToSquare("io91VL39FX") == "IO91")
|
|
||||||
assert(qthToSquare("JN58") == "JN58")
|
|
||||||
assert(qthToSquare("garbage!!") == "----")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.core.domain.cw
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import org.junit.Test
|
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.math.sin
|
|
||||||
|
|
||||||
class CwDecoderTest {
|
|
||||||
|
|
||||||
// --- Morse table ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun morseToChar_basicLetters() {
|
|
||||||
assertEquals('A', CwBayesianDecoder.morseToChar("01"))
|
|
||||||
assertEquals('S', CwBayesianDecoder.morseToChar("000"))
|
|
||||||
assertEquals('O', CwBayesianDecoder.morseToChar("111"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun morseToChar_numbers() {
|
|
||||||
assertEquals('1', CwBayesianDecoder.morseToChar("01111"))
|
|
||||||
assertEquals('0', CwBayesianDecoder.morseToChar("11111"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun morseToChar_unknown_returnsNull() {
|
|
||||||
assertNull(CwBayesianDecoder.morseToChar("......."))
|
|
||||||
assertNull(CwBayesianDecoder.morseToChar(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- FFT ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun fft_magnitudeSpectrum_detectsTone() {
|
|
||||||
val fft = CwFFT(256)
|
|
||||||
val sampleRate = 8000f
|
|
||||||
val freq = 700f
|
|
||||||
val buffer = FloatArray(256) { (sin(2.0 * PI * freq * it / sampleRate)).toFloat() }
|
|
||||||
val mag = fft.magnitudeSpectrum(buffer)
|
|
||||||
// Peak should be at bin around 700 * 256 / 8000 ≈ 22.4
|
|
||||||
var maxBin = 0
|
|
||||||
var maxVal = 0f
|
|
||||||
for (i in mag.indices) {
|
|
||||||
if (mag[i] > maxVal) { maxVal = mag[i]; maxBin = i }
|
|
||||||
}
|
|
||||||
assertTrue("Peak bin $maxBin should be near 22", maxBin in 18..26)
|
|
||||||
assertTrue("Peak value $maxVal should be positive", maxVal > 0.01f)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun fft_magnitudeSpectrum_silence_isFlat() {
|
|
||||||
val fft = CwFFT(256)
|
|
||||||
val buffer = FloatArray(256) { 0f }
|
|
||||||
val mag = fft.magnitudeSpectrum(buffer)
|
|
||||||
for (v in mag) assertEquals("Silence spectrum should be 0, got $v", 0f, v, 1e-6f)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun fft_rejectsWrongSize() {
|
|
||||||
assertThrows(IllegalArgumentException::class.java) { CwFFT(100) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Spectrogram ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun spectrogram_addSamples_updatesEnergy() {
|
|
||||||
val spec = CwSpectrogram(sampleRate = 8000)
|
|
||||||
val freq = 700f
|
|
||||||
// Feed multiple frames to stabilize energy normalization
|
|
||||||
for (i in 0..5) {
|
|
||||||
val buffer = FloatArray(256) { (sin(2.0 * PI * freq * it / 8000.0)).toFloat() }
|
|
||||||
spec.addSamples(buffer)
|
|
||||||
}
|
|
||||||
val col = spec.getCurrentColumn()
|
|
||||||
val peakBin = spec.findPeakBin()
|
|
||||||
assertTrue("Peak bin $peakBin should be >= 0", peakBin >= 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun spectrogram_findPeakBin_returnsValidBin() {
|
|
||||||
val spec = CwSpectrogram(sampleRate = 8000)
|
|
||||||
// Add multiple frames of 700 Hz tone
|
|
||||||
for (i in 0..5) {
|
|
||||||
val buffer = FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() }
|
|
||||||
spec.addSamples(buffer)
|
|
||||||
}
|
|
||||||
val peakBin = spec.findPeakBin()
|
|
||||||
assertTrue("Peak bin should be >= 0, got $peakBin", peakBin >= 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun spectrogram_freqToBin_roundtrip() {
|
|
||||||
val spec = CwSpectrogram(sampleRate = 8000)
|
|
||||||
val freq = 700f
|
|
||||||
val bin = spec.freqToBin(freq)
|
|
||||||
val backFreq = spec.binToFreq(bin)
|
|
||||||
assertTrue("Freq $freq → bin $bin → freq $backFreq", backFreq > 600f && backFreq < 800f)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun spectrogram_getBinEnergy_returnsCorrectLength() {
|
|
||||||
val spec = CwSpectrogram(sampleRate = 8000)
|
|
||||||
val energy = spec.getBinEnergy(0, 10)
|
|
||||||
assertEquals(10, energy.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun spectrogram_reset() {
|
|
||||||
val spec = CwSpectrogram(sampleRate = 8000)
|
|
||||||
spec.addSamples(FloatArray(256) { 1f })
|
|
||||||
spec.reset()
|
|
||||||
assertEquals(-1, spec.findPeakBin())
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Bayesian decoder ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun bayesian_processTone_dit() {
|
|
||||||
val decoder = CwBayesianDecoder()
|
|
||||||
// At 20 WPM, dot = 60 ms
|
|
||||||
val result = decoder.processTone(60f)
|
|
||||||
assertEquals('0', result.symbol)
|
|
||||||
assertTrue("Dit probability should be positive", result.probability > 0.1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun bayesian_processTone_dash() {
|
|
||||||
val decoder = CwBayesianDecoder()
|
|
||||||
// Dash = 3 * dot = 180 ms
|
|
||||||
val result = decoder.processTone(180f)
|
|
||||||
assertEquals('1', result.symbol)
|
|
||||||
assertTrue("Dash probability should be positive", result.probability > 0.1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun bayesian_processTone_unknown_returnsNull() {
|
|
||||||
val decoder = CwBayesianDecoder()
|
|
||||||
// Very long tone — low probability for both dit and dash
|
|
||||||
val result = decoder.processTone(5000f)
|
|
||||||
assertNull(result.symbol)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun bayesian_processGap_interChar_returnsChar() {
|
|
||||||
val decoder = CwBayesianDecoder()
|
|
||||||
decoder.processTone(60f) // dit
|
|
||||||
decoder.processTone(60f) // dit
|
|
||||||
decoder.processTone(60f) // dit
|
|
||||||
// 3 dots = "000" = 'S'
|
|
||||||
val char = decoder.processGap(180f) // 3 * dot = inter-char gap
|
|
||||||
assertEquals('S', char)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun bayesian_processGap_wordGap_addsSpace() {
|
|
||||||
val decoder = CwBayesianDecoder()
|
|
||||||
decoder.processTone(60f) // dit = 'E'
|
|
||||||
decoder.processGap(180f) // inter-char gap
|
|
||||||
// Now word gap
|
|
||||||
val space = decoder.processGap(420f) // 7 * dot
|
|
||||||
assertEquals(' ', space)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun bayesian_decodedText_accumulates() {
|
|
||||||
val decoder = CwBayesianDecoder()
|
|
||||||
decoder.processTone(60f) // dit = 'E'
|
|
||||||
decoder.processGap(180f) // inter-char
|
|
||||||
assertTrue(decoder.decodedText.isNotEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun bayesian_reset() {
|
|
||||||
val decoder = CwBayesianDecoder()
|
|
||||||
decoder.processTone(60f)
|
|
||||||
decoder.reset()
|
|
||||||
assertEquals("", decoder.decodedText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun bayesian_getSpeed() {
|
|
||||||
val decoder = CwBayesianDecoder()
|
|
||||||
// Send 3 dits at 20 WPM (60 ms each)
|
|
||||||
decoder.processTone(60f)
|
|
||||||
decoder.processTone(60f)
|
|
||||||
decoder.processTone(60f)
|
|
||||||
val speed = decoder.getSpeed()
|
|
||||||
assertTrue("Speed should be ~20 WPM, got $speed", speed > 15f && speed < 30f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Channel tracker ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun channelTracker_initialState() {
|
|
||||||
val spec = CwSpectrogram(sampleRate = 8000)
|
|
||||||
val tracker = CwChannelTracker(spec)
|
|
||||||
val channels = tracker.update()
|
|
||||||
assertTrue("No channels should be active initially", channels.isEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun channelTracker_detectsTone() {
|
|
||||||
val spec = CwSpectrogram(sampleRate = 8000)
|
|
||||||
// Feed a tone
|
|
||||||
for (i in 0..5) {
|
|
||||||
spec.addSamples(FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() })
|
|
||||||
}
|
|
||||||
val tracker = CwChannelTracker(spec)
|
|
||||||
val channels = tracker.update()
|
|
||||||
assertTrue("Should detect at least 1 channel", channels.isNotEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun channelTracker_bestChannel() {
|
|
||||||
val spec = CwSpectrogram(sampleRate = 8000)
|
|
||||||
for (i in 0..5) {
|
|
||||||
spec.addSamples(FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() })
|
|
||||||
}
|
|
||||||
val tracker = CwChannelTracker(spec)
|
|
||||||
tracker.update()
|
|
||||||
val best = tracker.getBestChannel()
|
|
||||||
assertNotNull("Best channel should exist", best)
|
|
||||||
if (best != null) assertTrue(best.frequency in 600f..800f)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun channelTracker_reset() {
|
|
||||||
val spec = CwSpectrogram(sampleRate = 8000)
|
|
||||||
for (i in 0..5) {
|
|
||||||
spec.addSamples(FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() })
|
|
||||||
}
|
|
||||||
val tracker = CwChannelTracker(spec)
|
|
||||||
tracker.update()
|
|
||||||
tracker.reset()
|
|
||||||
assertNull(tracker.getBestChannel())
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Full decoder ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun decoder_initialState() {
|
|
||||||
val decoder = CwDecoder()
|
|
||||||
assertEquals("", decoder.decodedTextFlow.value)
|
|
||||||
assertEquals(0f, decoder.signalStrength.value, 0.001f)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun decoder_processSilence_doesNotCrash() {
|
|
||||||
val decoder = CwDecoder()
|
|
||||||
decoder.processBuffer(FloatArray(256) { 0f })
|
|
||||||
assertEquals("", decoder.decodedTextFlow.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun decoder_processNoise_doesNotCrash() {
|
|
||||||
val decoder = CwDecoder()
|
|
||||||
decoder.processBuffer(FloatArray(256) { (Math.random() * 2 - 1).toFloat() * 0.1f })
|
|
||||||
assertNotNull(decoder.decodedTextFlow.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun decoder_processTone_doesNotCrash() {
|
|
||||||
val decoder = CwDecoder()
|
|
||||||
for (i in 0..20) {
|
|
||||||
decoder.processBuffer(FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() })
|
|
||||||
}
|
|
||||||
assertNotNull(decoder.decodedTextFlow.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun decoder_reset() {
|
|
||||||
val decoder = CwDecoder()
|
|
||||||
decoder.processBuffer(FloatArray(256) { 1f })
|
|
||||||
decoder.resetDecoder()
|
|
||||||
assertEquals("", decoder.decodedTextFlow.value)
|
|
||||||
assertEquals(0f, decoder.signalStrength.value, 0.001f)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun decoder_withFixedPitch() {
|
|
||||||
val decoder = CwDecoder(sampleRate = 8000, cwToneFreq = 700f)
|
|
||||||
assertEquals(700f, decoder.estimatedPitch.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+23
-7
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -46,6 +47,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -143,7 +145,7 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier, isUtc
|
|||||||
if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
|
if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
|
||||||
}
|
}
|
||||||
val sdfTime = remember(isUtc) {
|
val sdfTime = remember(isUtc) {
|
||||||
SimpleDateFormat("HH:mm:ss", Locale.ENGLISH).also { it.timeZone = timeZone }
|
SimpleDateFormat("HH:mm:ss", displayLocale()).also { it.timeZone = timeZone }
|
||||||
}
|
}
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -157,13 +159,16 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier, isUtc
|
|||||||
.padding(start = 6.dp, top = 1.dp, end = 6.dp, bottom = 0.dp)
|
.padding(start = 6.dp, top = 1.dp, end = 6.dp, bottom = 0.dp)
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.pass_satId, pass.catNum)} - ",
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
text = pass.name,
|
text = pass.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(end = 6.dp)
|
.padding(end = 6.dp)
|
||||||
.infiniteMarquee(),
|
.infiniteMarquee(),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
@@ -209,6 +214,11 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier, isUtc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun displayLocale(): Locale {
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
return if (locale.language == Locale.CHINESE.language) locale else Locale.ENGLISH
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier) {
|
fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier) {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
@@ -299,7 +309,8 @@ fun SharedDialog(
|
|||||||
titleTextAlign: TextAlign = if (onCancel != null && onAccept != null) TextAlign.Center else TextAlign.Start,
|
titleTextAlign: TextAlign = if (onCancel != null && onAccept != null) TextAlign.Center else TextAlign.Start,
|
||||||
content: @Composable (padding: Dp) -> Unit
|
content: @Composable (padding: Dp) -> Unit
|
||||||
) {
|
) {
|
||||||
DialogShell(onDismissRequest = onDismissRequest) { padding ->
|
val dismissible = onCancel != null
|
||||||
|
DialogShell(onDismissRequest = onDismissRequest, dismissible = dismissible) { padding ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -333,10 +344,14 @@ fun SharedDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DialogShell(
|
private fun DialogShell(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
|
dismissible: Boolean = true,
|
||||||
content: @Composable (padding: Dp) -> Unit
|
content: @Composable (padding: Dp) -> Unit
|
||||||
) {
|
) {
|
||||||
val padding = LocalSpacing.current.large
|
val padding = LocalSpacing.current.large
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true,
|
||||||
|
confirmValueChange = { if (dismissible) true else it != SheetValue.Hidden }
|
||||||
|
)
|
||||||
val stopSheetFling = remember {
|
val stopSheetFling = remember {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
@@ -351,10 +366,10 @@ private fun DialogShell(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = if (dismissible) onDismissRequest else { {} },
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
dragHandle = null,
|
dragHandle = null,
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||||
scrimColor = Color.Black.copy(alpha = 0.64f)
|
scrimColor = Color.Black.copy(alpha = 0.64f)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -518,5 +533,6 @@ fun formatFrequency(frequencyHz: Long): String {
|
|||||||
if (frequencyHz <= 0) return "---"
|
if (frequencyHz <= 0) return "---"
|
||||||
val mhz = frequencyHz / 1_000_000
|
val mhz = frequencyHz / 1_000_000
|
||||||
val khz = (frequencyHz % 1_000_000) / 1_000
|
val khz = (frequencyHz % 1_000_000) / 1_000
|
||||||
return String.format(Locale.ENGLISH, "%d.%03d", mhz, khz)
|
val hz = frequencyHz % 1_000
|
||||||
|
return String.format(Locale.ENGLISH, "%d.%03d.%03d", mhz, khz, hz)
|
||||||
}
|
}
|
||||||
+8
-4
@@ -79,6 +79,10 @@ private val lightScheme = lightColorScheme(
|
|||||||
onSecondary = Color(0xFFFFFFFF),
|
onSecondary = Color(0xFFFFFFFF),
|
||||||
secondaryContainer = Color(0xFFF1E1BB),
|
secondaryContainer = Color(0xFFF1E1BB),
|
||||||
onSecondaryContainer = Color(0xFF221B04),
|
onSecondaryContainer = Color(0xFF221B04),
|
||||||
|
tertiary = Color(0xFF3C6FE0), // AMSAT Active (darker for light theme)
|
||||||
|
onTertiary = Color(0xFFFFFFFF),
|
||||||
|
tertiaryContainer = Color(0xFFE09800), // AMSAT Telemetry (darker)
|
||||||
|
onTertiaryContainer = Color(0xFF000000),
|
||||||
background = Color(0xFFFFF8F0),
|
background = Color(0xFFFFF8F0),
|
||||||
onBackground = Color(0xFF1E1B13),
|
onBackground = Color(0xFF1E1B13),
|
||||||
surface = Color(0xFFFFF8F0),
|
surface = Color(0xFFFFF8F0),
|
||||||
@@ -101,10 +105,10 @@ private val darkScheme = darkColorScheme(
|
|||||||
onSecondary = Color(0xFF000000),
|
onSecondary = Color(0xFF000000),
|
||||||
secondaryContainer = Color(0xFF404040), // navBar indicator,
|
secondaryContainer = Color(0xFF404040), // navBar indicator,
|
||||||
onSecondaryContainer = Color(0xFFE0E0E0), // navBar active icon
|
onSecondaryContainer = Color(0xFFE0E0E0), // navBar active icon
|
||||||
// tertiary = Color(0xFF121212),
|
tertiary = Color(0xFF648FFF), // AMSAT Active (from amsat.org/status)
|
||||||
// onTertiary = Color(0xFF121212),
|
onTertiary = Color(0xFF000000),
|
||||||
// tertiaryContainer = Color(0xFF121212),
|
tertiaryContainer = Color(0xFFFFB000), // AMSAT Telemetry
|
||||||
// onTertiaryContainer = Color(0xFF121212),
|
onTertiaryContainer = Color(0xFF000000),
|
||||||
background = Color(0xFF121212),
|
background = Color(0xFF121212),
|
||||||
onBackground = Color(0xFFE0E0E0),
|
onBackground = Color(0xFFE0E0E0),
|
||||||
surface = Color(0xFF202020), // card background
|
surface = Color(0xFF202020), // card background
|
||||||
|
|||||||
+7
-28
@@ -21,45 +21,24 @@ import androidx.navigation3.runtime.NavKey
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class Screen(val iconResId: Int, val titleResId: Int, val screenId: String) : NavKey {
|
sealed class Screen(val iconResId: Int, val titleResId: Int) : NavKey {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Satellites : Screen(R.drawable.ic_satellites, R.string.nav_sat, "Satellites")
|
data object Satellites : Screen(R.drawable.ic_satellites, R.string.nav_sat)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Passes : Screen(R.drawable.ic_passes, R.string.nav_pass, "Passes")
|
data object Passes : Screen(R.drawable.ic_passes, R.string.nav_pass)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Radar : Screen(R.drawable.ic_radar, R.string.nav_radar, "Radar")
|
data object Radar : Screen(R.drawable.ic_radar, R.string.nav_radar)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Map : Screen(R.drawable.ic_map, R.string.nav_map, "Map")
|
data object Map : Screen(R.drawable.ic_map, R.string.nav_map)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Mutual : Screen(R.drawable.ic_match, R.string.nav_mutual, "Mutual")
|
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data object Roaming : Screen(R.drawable.ic_roaming, R.string.nav_roaming, "Roaming")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data object CwDecode : Screen(R.drawable.ic_cw, R.string.nav_cw, "CwDecode")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data object WavelogLog : Screen(R.drawable.ic_log, R.string.nav_log, "WavelogLog")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data object AmSat : Screen(R.drawable.ic_satellites, R.string.nav_amsat, "AMSAT")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs, "Settings")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI 设置: 默认主菜单页面顺序(底部栏 5 槽)
|
|
||||||
val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Map", "Settings")
|
|
||||||
|
|
||||||
// UI 设置: 默认更多菜单顺序(进「更多」的页面)
|
|
||||||
val defaultSubMenuOrder = listOf("Mutual", "Roaming", "CwDecode", "WavelogLog", "AMSAT")
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object RadarDestination : NavKey
|
data object RadarDestination : NavKey
|
||||||
|
|
||||||
@@ -88,4 +67,4 @@ class DeeplinkResolver(private val fallbackDestination: NavKey = Screen.Passes)
|
|||||||
matchers.forEach { it.match(deeplink)?.let { match -> return match } }
|
matchers.forEach { it.match(deeplink)?.let { match -> return match } }
|
||||||
return fallbackDestination
|
return fallbackDestination
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<!-- Morse style icon: a dot above a dash -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M 8 4 L 16 4 L 16 8 L 8 8 Z" />
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M 3 16 L 21 16 L 21 20 L 3 20 Z" />
|
|
||||||
</vector>
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="15"
|
||||||
|
android:viewportHeight="15">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M13.91,6.75c-1.17,2.25 -4.3,5.31 -6.07,6.94c-0.19,0.172 -0.48,0.172 -0.67,0C5.39,12.06 2.26,9 1.09,6.75C-1.48,1.8 5,-1.5 7.5,3.45C10,-1.5 16.48,1.8 13.91,6.75z" />
|
||||||
|
</vector>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M6,2h9l5,5v13c0,1.1 -0.9,2 -2,2H6c-1.1,0 -2,-0.9 -2,-2V4c0,-1.1 0.9,-2 2,-2zM14,3.5V8h4.5L14,3.5zM8,12h8v2H8v-2zM8,16h8v2H8v-2z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="96"
|
|
||||||
android:viewportHeight="96">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/transparent"
|
|
||||||
android:strokeColor="@android:color/white"
|
|
||||||
android:strokeWidth="6"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:pathData="M 8 76 C 8 44, 21 24, 39 24 C 57 24, 70 44, 70 76" />
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/transparent"
|
|
||||||
android:strokeColor="@android:color/white"
|
|
||||||
android:strokeWidth="6"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:pathData="M 30 76 C 30 54, 44 42, 62 42 C 80 42, 94 54, 94 76" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M14,21c1.93,0 3.62,-1.17 4,-3l-1.75,-0.88C16,18.21 15.33,19 14,19l-4.9,0c0.83,-1 1.5,-2.34 1.5,-4c0,-0.35 -0.03,-0.69 -0.08,-1L14,14v-2l-4.18,0C9,10.42 8,9.6 8,8c0,-1.93 1.57,-3.5 3.5,-3.5c1.5,0 2.79,0.95 3.28,2.28L16.63,6c-0.8,-2.05 -2.79,-3.5 -5.13,-3.5C8.46,2.5 6,4.96 6,8c0,1.78 0.79,2.9 1.49,4L6,12v2l2.47,0c0.08,0.31 0.13,0.64 0.13,1c0,2.7 -2.6,4 -2.6,4v2H14z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="96"
|
|
||||||
android:viewportHeight="96">
|
|
||||||
|
|
||||||
<!-- Crosshair / grid-locator style icon: concentric rings + center dot -->
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/transparent"
|
|
||||||
android:strokeColor="@android:color/white"
|
|
||||||
android:strokeWidth="6"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:pathData="M 48 12 L 48 84 M 12 48 L 84 48" />
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/transparent"
|
|
||||||
android:strokeColor="@android:color/white"
|
|
||||||
android:strokeWidth="6"
|
|
||||||
android:pathData="M 48 26 A 22 22 0 1 0 48.1 26" />
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M 44 44 L 52 44 L 52 52 L 44 52 Z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M240,800L280,640L120,640L140,560L300,560L340,400L180,400L200,320L360,320L400,160L480,160L440,320L600,320L640,160L720,160L680,320L840,320L820,400L660,400L620,560L780,560L760,640L600,640L560,800L480,800L520,640L360,640L320,800L240,800ZM380,560L540,560L580,400L420,400L380,560Z" />
|
|
||||||
</vector>
|
|
||||||
@@ -13,12 +13,16 @@
|
|||||||
<string name="nav_prefs">Ajustes</string>
|
<string name="nav_prefs">Ajustes</string>
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">Tipo: %s</string>
|
<string name="sat_type_hint">Modos: %s</string>
|
||||||
<string name="sat_type_title">Seleccionar tipo de satélite</string>
|
<string name="sat_type_title">Seleccionar modos</string>
|
||||||
<string name="sat_search_hint">Id - Nombre</string>
|
<string name="sat_search_hint">Id - Nombre</string>
|
||||||
<string name="sat_search_clear">Vaciar</string>
|
<string name="sat_search_clear">Vaciar</string>
|
||||||
<string name="sat_clear_all">Vaciar todo</string>
|
<string name="sat_clear_all">Vaciar todo</string>
|
||||||
<string name="sat_select_all">Seleccionar todo</string>
|
<string name="sat_select_all">Seleccionar todo</string>
|
||||||
|
<string name="sat_group_selected">Seleccionados</string>
|
||||||
|
<string name="sat_group_available">Disponibles</string>
|
||||||
|
<string name="sat_group_selected_count">Seleccionados (%1$d)</string>
|
||||||
|
<string name="sat_group_available_count">Disponibles (%1$d)</string>
|
||||||
<string name="sat_empty_list_message">
|
<string name="sat_empty_list_message">
|
||||||
Asegúrese de que su consulta de búsqueda sea correcta y que la base de datos esté actualizada</string>
|
Asegúrese de que su consulta de búsqueda sea correcta y que la base de datos esté actualizada</string>
|
||||||
<string name="sat_warning_title">Alarma\!</string>
|
<string name="sat_warning_title">Alarma\!</string>
|
||||||
@@ -26,7 +30,7 @@
|
|||||||
Esta aplicación incluye más de 9000 satélites.
|
Esta aplicación incluye más de 9000 satélites.
|
||||||
No tiene sentido rastrearlos todos a la vez.
|
No tiene sentido rastrearlos todos a la vez.
|
||||||
\n\nIntenta siempre limitar la lista a los que te
|
\n\nIntenta siempre limitar la lista a los que te
|
||||||
interesen mediante la búsqueda y el selector de tipos.</string>
|
interesen mediante la búsqueda y el selector de modos.</string>
|
||||||
|
|
||||||
<!-- Passes screen -->
|
<!-- Passes screen -->
|
||||||
<string name="pass_filter_title">Filtrar pases</string>
|
<string name="pass_filter_title">Filtrar pases</string>
|
||||||
@@ -35,7 +39,7 @@
|
|||||||
<string name="pass_filter_aos_time">Ventana AOS</string>
|
<string name="pass_filter_aos_time">Ventana AOS</string>
|
||||||
<string name="pass_filter_invert_time">Invertir ventana AOS</string>
|
<string name="pass_filter_invert_time">Invertir ventana AOS</string>
|
||||||
<string name="pass_filter_deep_space">DeepSpace (período >225min)</string>
|
<string name="pass_filter_deep_space">DeepSpace (período >225min)</string>
|
||||||
<string name="pass_modes_title">Tipo de modulación</string>
|
<string name="pass_modes_title">Seleccionar modos</string>
|
||||||
<string name="pass_elevation">Elevación: %.1f°</string>
|
<string name="pass_elevation">Elevación: %.1f°</string>
|
||||||
<string name="pass_altitude">Altitud: %.0f km</string>
|
<string name="pass_altitude">Altitud: %.0f km</string>
|
||||||
<string name="pass_empty_list_message">
|
<string name="pass_empty_list_message">
|
||||||
@@ -96,11 +100,11 @@
|
|||||||
<string name="prefs_loc_qth_title">QTH</string>
|
<string name="prefs_loc_qth_title">QTH</string>
|
||||||
<string name="prefs_loc_qth_error">Ubicación QTH no válida</string>
|
<string name="prefs_loc_qth_error">Ubicación QTH no válida</string>
|
||||||
<string name="prefs_loc_success">Ubicación actualizada con éxito</string>
|
<string name="prefs_loc_success">Ubicación actualizada con éxito</string>
|
||||||
<string name="prefs_station_title">Ajustar ubicaicón de la estación</string>
|
<string name="prefs_station_title">Ubicación de la estación</string>
|
||||||
<string name="prefs_station_lat_text">Latitud de tu estación terrestre</string>
|
<string name="prefs_station_lat_text">Latitud de estación</string>
|
||||||
<string name="prefs_station_lon_text">Longitud de tu estación terrestre</string>
|
<string name="prefs_station_lon_text">Longitud de estación</string>
|
||||||
<string name="prefs_locator_title">Ajustes ubicación QTH</string>
|
<string name="prefs_locator_title">Localizador QTH</string>
|
||||||
<string name="prefs_locator_text">Ajustar posición de la estación con tu ubicación</string>
|
<string name="prefs_locator_text">Posición por localizador</string>
|
||||||
|
|
||||||
<string name="prefs_data_title">Datos satelitales</string>
|
<string name="prefs_data_title">Datos satelitales</string>
|
||||||
<string name="prefs_data_entries">Satélites: %s</string>
|
<string name="prefs_data_entries">Satélites: %s</string>
|
||||||
@@ -110,39 +114,52 @@
|
|||||||
<string name="prefs_data_clear">Vaciar</string>
|
<string name="prefs_data_clear">Vaciar</string>
|
||||||
<string name="prefs_data_clear_success">Datos eliminados con éxito</string>
|
<string name="prefs_data_clear_success">Datos eliminados con éxito</string>
|
||||||
<string name="prefs_data_update_success">Actualización completada con éxito</string>
|
<string name="prefs_data_update_success">Actualización completada con éxito</string>
|
||||||
|
<string name="prefs_data_import_satellites_error">No se importaron satélites. Usa un TLE/3LE (.txt) u OMM (.csv) válido.</string>
|
||||||
|
<string name="prefs_data_import_transceivers_error">No se importaron transceptores. Usa un SatNOGS (.json) válido.</string>
|
||||||
|
|
||||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
<string name="prefs_data_sources_title">Fuentes personalizadas</string>
|
||||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
<string name="prefs_data_sources_tle_switch">URL TLE</string>
|
||||||
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
|
<string name="prefs_data_sources_transceivers_switch">URL transceptores</string>
|
||||||
|
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
||||||
|
|
||||||
<string name="prefs_data_output_title">Data output</string>
|
<string name="prefs_data_output_title">Salida de datos</string>
|
||||||
<string name="prefs_net_output">Network</string>
|
<string name="prefs_net_output">Red</string>
|
||||||
<string name="prefs_bt_output">Bluetooth</string>
|
<string name="prefs_bt_output">Bluetooth</string>
|
||||||
|
<string name="prefs_cat_output">CAT</string>
|
||||||
|
|
||||||
<string name="prefs_net_title">Salida de datos de red</string>
|
<string name="nav_radiocontrol">Control de radio</string>
|
||||||
<string name="prefs_net_rotator_switch">Enable rotation output</string>
|
<string name="rc_settings_title">Control CAT</string>
|
||||||
|
<string name="rc_radio_model">Modelo de radio</string>
|
||||||
|
<string name="rc_tx_device_hint">Dirección BT TX</string>
|
||||||
|
<string name="rc_rx_device_hint">Dirección BT RX</string>
|
||||||
|
<string name="rc_tx_name_hint">Nombre radio TX</string>
|
||||||
|
<string name="rc_rx_name_hint">Nombre radio RX</string>
|
||||||
|
<string name="rc_enable_switch">Activar CAT</string>
|
||||||
|
|
||||||
|
<string name="prefs_net_title">Salida de red</string>
|
||||||
|
<string name="prefs_net_rotator_switch">Activar salida de rotación</string>
|
||||||
<string name="prefs_net_rotator_address_hint">IP:Puerto</string>
|
<string name="prefs_net_rotator_address_hint">IP:Puerto</string>
|
||||||
<string name="prefs_net_rotator_format_hint">Formato de datos</string>
|
<string name="prefs_net_rotator_format_hint">Formato de datos</string>
|
||||||
<string name="prefs_net_frequency_switch">Enable frequency output</string>
|
<string name="prefs_net_frequency_switch">Activar salida de frecuencia</string>
|
||||||
<string name="prefs_net_frequency_address_hint">IP:Puerto</string>
|
<string name="prefs_net_frequency_address_hint">IP:Puerto</string>
|
||||||
<string name="prefs_net_frequency_format_hint">Formato de datos</string>
|
<string name="prefs_net_frequency_format_hint">Formato de datos</string>
|
||||||
|
|
||||||
<string name="prefs_bt_title">Salida por Bluetooth</string>
|
<string name="prefs_bt_title">Salida Bluetooth</string>
|
||||||
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
|
<string name="prefs_bt_rotator_switch">Activar salida de rotación</string>
|
||||||
<string name="prefs_bt_rotator_device_hint">Id dispositivo</string>
|
<string name="prefs_bt_rotator_device_hint">ID dispositivo</string>
|
||||||
<string name="prefs_bt_rotator_output_hint">Formato de datos</string>
|
<string name="prefs_bt_rotator_output_hint">Formato</string>
|
||||||
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
|
<string name="prefs_bt_frequency_switch">Activar salida de frecuencia</string>
|
||||||
<string name="prefs_bt_frequency_device_hint">Id dispositivo</string>
|
<string name="prefs_bt_frequency_device_hint">ID dispositivo</string>
|
||||||
<string name="prefs_bt_frequency_output_hint">Formato de datos</string>
|
<string name="prefs_bt_frequency_output_hint">Formato</string>
|
||||||
<string name="prefs_bt_perm_error">Revisa los permisos de Bluetooth</string>
|
<string name="prefs_bt_perm_error">Revisa los permisos de Bluetooth</string>
|
||||||
<string name="prefs_net_perm_error">Revisa los permisos de red</string>
|
<string name="prefs_net_perm_error">Revisa los permisos de red</string>
|
||||||
|
|
||||||
<string name="prefs_other_title">Otros ajustes</string>
|
<string name="prefs_other_title">Otros</string>
|
||||||
<string name="prefs_other_switch_utc">Mostrar tiempos de pase en UTC</string>
|
<string name="prefs_other_switch_utc">Hora UTC</string>
|
||||||
<string name="prefs_other_switch_update">Habilitar auto actualización de datos</string>
|
<string name="prefs_other_switch_update">Actualización automática</string>
|
||||||
<string name="prefs_other_switch_sweep">Habilitar animaciones radar</string>
|
<string name="prefs_other_switch_sweep">Animación radar</string>
|
||||||
<string name="prefs_other_switch_sensors">Usar sensores para rotar vista de radar</string>
|
<string name="prefs_other_switch_sensors">Sensores</string>
|
||||||
<string name="prefs_other_switch_night_mode">Activar filtro nocturno rojo</string>
|
<string name="prefs_other_switch_night_mode">Filtro nocturno</string>
|
||||||
|
|
||||||
<string name="prefs_highlight_title">Resaltado de elevación</string>
|
<string name="prefs_highlight_title">Resaltado de elevación</string>
|
||||||
<string name="prefs_highlight_low">Baja < %1$d°</string>
|
<string name="prefs_highlight_low">Baja < %1$d°</string>
|
||||||
|
|||||||
@@ -1,283 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name" translatable="false">Look4Sat Pro</string>
|
|
||||||
<string name="btn_accept">Terima</string>
|
|
||||||
<string name="btn_cancel">Batal</string>
|
|
||||||
<string name="empty_list_message">Tidak ada data untuk ditampilkan</string>
|
|
||||||
<string name="nav_sat">Satelit</string>
|
|
||||||
<string name="nav_pass">Lintasan</string>
|
|
||||||
<string name="nav_radar">Radar</string>
|
|
||||||
<string name="nav_mutual">Pasangan</string>
|
|
||||||
<string name="nav_roaming">Jelajah</string>
|
|
||||||
<string name="nav_cw">Dekoder CW</string>
|
|
||||||
<string name="nav_map">Peta</string>
|
|
||||||
<string name="nav_prefs">Pengaturan</string>
|
|
||||||
<string name="prefs_aprs_title">Pelaporan APRS</string>
|
|
||||||
<string name="prefs_aprs_enable">Aktifkan pelaporan APRS</string>
|
|
||||||
<string name="prefs_aprs_summary">Server: %1$s:%2$d Panggilan: %3$s</string>
|
|
||||||
<string name="prefs_aprs_report_now">Lapor Sekarang</string>
|
|
||||||
<string name="prefs_aprs_settings_title">Pengaturan APRS</string>
|
|
||||||
<string name="prefs_aprs_server">Server</string>
|
|
||||||
<string name="prefs_aprs_port">Port</string>
|
|
||||||
<string name="prefs_aprs_callsign">Tanda panggilan</string>
|
|
||||||
<string name="prefs_aprs_ssid">SSID</string>
|
|
||||||
<string name="prefs_aprs_passcode">Kode sandi</string>
|
|
||||||
<string name="prefs_aprs_passcode_hint">Kosong = otomatis dari panggilan</string>
|
|
||||||
<string name="prefs_aprs_interval">Interval (menit)</string>
|
|
||||||
<string name="prefs_aprs_status">Teks status</string>
|
|
||||||
<string name="prefs_aprs_symbol_table">Tabel simbol</string>
|
|
||||||
<string name="prefs_aprs_symbol_code">Kode simbol</string>
|
|
||||||
<string name="prefs_aprs_save">Simpan</string>
|
|
||||||
<string name="prefs_aprs_cancel">Batal</string>
|
|
||||||
<string name="aprs_notif_channel">Layanan APRS</string>
|
|
||||||
<string name="aprs_notif_title">APRS: %1$s</string>
|
|
||||||
<string name="aprs_notif_running">Pelaporan berjalan</string>
|
|
||||||
<string name="aprs_notif_connected">Terhubung ke APRS-IS</string>
|
|
||||||
<string name="aprs_notif_error">Kesalahan koneksi</string>
|
|
||||||
<string name="aprs_notif_stop">Berhenti</string>
|
|
||||||
<string name="prefs_aprs_calc_passcode">Hitung kode sandi</string>
|
|
||||||
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
|
||||||
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
|
||||||
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
|
||||||
<string name="prefs_aprs_last_ok">OK</string>
|
|
||||||
<string name="prefs_aprs_last_fail">gagal - %1$s</string>
|
|
||||||
<string name="nav_more">Lainnya</string>
|
|
||||||
<string name="nav_log">Log</string>
|
|
||||||
<string name="nav_amsat">AMSAT</string>
|
|
||||||
<string name="amsat_title">Status Satelit AMSAT</string>
|
|
||||||
<string name="amsat_updated">Diperbarui:</string>
|
|
||||||
<string name="amsat_active">Aktif</string>
|
|
||||||
<string name="amsat_tlm">TLM/beacon</string>
|
|
||||||
<string name="amsat_not_heard">Tidak terdengar</string>
|
|
||||||
<string name="amsat_conflict">Konflik</string>
|
|
||||||
<string name="amsat_name">Name</string>
|
|
||||||
<string name="amsat_load_failed">Gagal memuat - periksa jaringan dan coba lagi</string>
|
|
||||||
<string name="amsat_no_reports">Tidak ada laporan untuk periode ini</string>
|
|
||||||
<string name="amsat_close">Tutup</string>
|
|
||||||
|
|
||||||
<string name="sat_type_hint">Jenis: %s</string>
|
|
||||||
<string name="sat_type_title">Pilih jenis satelit</string>
|
|
||||||
<string name="sat_search_hint">Id - Nama</string>
|
|
||||||
<string name="sat_search_clear">Bersihkan</string>
|
|
||||||
<string name="sat_clear_all">Bersihkan semua</string>
|
|
||||||
<string name="sat_select_all">Pilih semua</string>
|
|
||||||
<string name="sat_empty_list_message">Pastikan kata kunci pencarian Anda benar dan DB sudah diperbarui</string>
|
|
||||||
<string name="sat_warning_title">Peringatan\!</string>
|
|
||||||
<string name="sat_warning_message">
|
|
||||||
Ada lebih dari 9000 satelit yang terdaftar di aplikasi ini.
|
|
||||||
Tidak masuk akal untuk melacak semuanya sekaligus.
|
|
||||||
\n\nSelalu coba persempit daftar hanya ke satelit yang
|
|
||||||
Anda minati melalui pencarian dan pemilih jenis.</string>
|
|
||||||
<string name="pass_filter_title">Filter lintasan</string>
|
|
||||||
<string name="pass_filter_elev">Elevasi minimal</string>
|
|
||||||
<string name="pass_filter_hours">Waktu ke depan</string>
|
|
||||||
<string name="pass_filter_aos_time">Jendela AOS</string>
|
|
||||||
<string name="pass_filter_invert_time">Balik jendela AOS</string>
|
|
||||||
<string name="pass_filter_deep_space">DeepSpace (periode >225 mnt)</string>
|
|
||||||
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
|
||||||
<string name="pass_modes_title">Pilih jenis modulasi</string>
|
|
||||||
<string name="pass_satId" translatable="false">%05d</string>
|
|
||||||
<string name="pass_elevation">Elevasi: %.1f°</string>
|
|
||||||
<string name="pass_deep_space" translatable="false">DeepSpace</string>
|
|
||||||
<string name="pass_altitude">Ketinggian: %d km</string>
|
|
||||||
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
|
|
||||||
<string name="pass_empty_list_message">
|
|
||||||
Pastikan filter Anda benar dan Anda sudah memilih satelit</string>
|
|
||||||
<string name="pass_error_message">Tidak ada lintasan satelit mendatang untuk ditampilkan</string>
|
|
||||||
<string name="pass_welcome_title">Selamat datang di Look4Sat\!</string>
|
|
||||||
<string name="pass_welcome_message">
|
|
||||||
Pastikan Anda mengatur posisi melalui GPS, Lat/Lon atau QTH di Pengaturan.
|
|
||||||
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.</string>
|
|
||||||
<string name="pass_whatsnew_title" translatable="false">Apa yang baru di Look4Sat</string>
|
|
||||||
<string name="pass_whatsnew_message" translatable="false">
|
|
||||||
* BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari
|
|
||||||
* Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi
|
|
||||||
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
|
|
||||||
* Pesan kegagalan jelas saat database satelit gagal diunduh
|
|
||||||
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
|
|
||||||
* Perbaikan bahasa Indonesia (values-id)
|
|
||||||
* BARU: Pelaporan APRS (jaringan APRS-IS, layanan latar depan dengan notifikasi, interval dapat diatur)</string>
|
|
||||||
<string name="radar_back">Kembali</string>
|
|
||||||
<string name="radar_notify">Beri tahu</string>
|
|
||||||
<string name="radar_az_text">Azimuth</string>
|
|
||||||
<string name="radar_az_value" translatable="false">%.1f°</string>
|
|
||||||
<string name="radar_el_text">Elevasi</string>
|
|
||||||
<string name="radar_el_value" translatable="false">%.1f°</string>
|
|
||||||
<string name="radar_alt_text">Ketinggian</string>
|
|
||||||
<string name="radar_alt_value" translatable="false">%.0f km</string>
|
|
||||||
<string name="radar_dist_text">Jarak</string>
|
|
||||||
<string name="radar_dist_value" translatable="false">%.0f km</string>
|
|
||||||
<string name="radar_eclipsed">Gerhana</string>
|
|
||||||
<string name="radar_downlink">Downlink</string>
|
|
||||||
<string name="radar_uplink">Uplink</string>
|
|
||||||
<string name="radar_link_low" translatable="false">%.4f</string>
|
|
||||||
<string name="radar_link_lowHigh" translatable="false">%.4f - %.4f</string>
|
|
||||||
<string name="radar_no_link" translatable="false">- - . - -</string>
|
|
||||||
<string name="radar_no_data">Satelit ini tidak memiliki transceiver</string>
|
|
||||||
<string name="radar_add">Tambah</string>
|
|
||||||
<string name="radar_data">Transceiver satelit:</string>
|
|
||||||
<string name="radar_inverted">Terbalik: %s</string>
|
|
||||||
<string name="radar_mode">Mode: %s</string>
|
|
||||||
<string name="radar_string_no">Tidak</string>
|
|
||||||
<string name="radar_string_yes">Ya</string>
|
|
||||||
<string name="radar_visible">Terlihat</string>
|
|
||||||
<string name="radar_doppler_calc">Kalkulator Frekuensi Doppler</string>
|
|
||||||
<string name="radar_doppler_tx_hint">Masukkan frekuensi TX (MHz)</string>
|
|
||||||
<string name="radar_doppler_rx_hint">Masukkan frekuensi RX (MHz)</string>
|
|
||||||
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
|
|
||||||
<string name="radar_doppler_info">Untuk transponder linier, ketik satu frekuensi untuk melihat frekuensi lainnya</string>
|
|
||||||
<string name="radar_cw_decoder">Dekoder CW</string>
|
|
||||||
<string name="radar_cw_start">Mulai</string>
|
|
||||||
<string name="radar_cw_stop">Berhenti</string>
|
|
||||||
<string name="radar_cw_reset">Bersihkan</string>
|
|
||||||
<string name="map_prev">Sebelumnya</string>
|
|
||||||
<string name="map_next">Berikutnya</string>
|
|
||||||
<string name="map_copyright" translatable="false">© Kontributor OpenStreetMap</string>
|
|
||||||
<string name="map_azimuth">Azimuth: %.1f°</string>
|
|
||||||
<string name="map_elevation">Elevasi: %.1f°</string>
|
|
||||||
<string name="map_altitude">Ketinggian: %.0f km</string>
|
|
||||||
<string name="map_distance">Jarak: %.0f km</string>
|
|
||||||
<string name="map_latitude">Lintang: %.1f°</string>
|
|
||||||
<string name="map_longitude">Bujur: %.1f°</string>
|
|
||||||
<string name="map_qth" translatable="false">QTH: %s</string>
|
|
||||||
<string name="map_phase">Fase: %.1f°</string>
|
|
||||||
<string name="map_eclipsed">Gerhana</string>
|
|
||||||
<string name="map_period">Periode: %.0f mnt</string>
|
|
||||||
<string name="map_velocity">Kecepatan: %.2f km/d</string>
|
|
||||||
<string name="map_visibility">Visibilitas: %s</string>
|
|
||||||
<string name="map_visible">Terlihat</string>
|
|
||||||
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
|
|
||||||
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
|
||||||
<string name="prefs_donate_title">Donasi</string>
|
|
||||||
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
|
|
||||||
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
|
|
||||||
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
|
|
||||||
<string name="prefs_github_title" translatable="false">GitHub</string>
|
|
||||||
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
|
|
||||||
<string name="prefs_license_title" translatable="false">GPLv3</string>
|
|
||||||
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
|
|
||||||
<string name="prefs_privacy_title">Kebijakan Privasi</string>
|
|
||||||
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
|
|
||||||
<string name="prefs_updated_title">Diperbarui: %s</string>
|
|
||||||
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
|
||||||
<string name="prefs_loc_title">Posisi stasiun</string>
|
|
||||||
<string name="prefs_loc_gps_title">GPS</string>
|
|
||||||
<string name="prefs_loc_gps_error">Tidak dapat mengambil lokasi - periksa izin dan sinyal GPS/jaringan</string>
|
|
||||||
<string name="prefs_loc_input_title">Input</string>
|
|
||||||
<string name="prefs_loc_input_error">Lokasi yang dimasukkan tidak valid</string>
|
|
||||||
<string name="prefs_loc_qth_title">QTH</string>
|
|
||||||
<string name="prefs_loc_qth_error">Lokator QTH tidak valid</string>
|
|
||||||
<string name="prefs_loc_success">Posisi berhasil diperbarui</string>
|
|
||||||
<string name="prefs_station_title">Pengaturan posisi stasiun</string>
|
|
||||||
<string name="prefs_station_lat_text">Atur garis lintang stasiun Anda</string>
|
|
||||||
<string name="prefs_station_lon_text">Atur garis bujur stasiun Anda</string>
|
|
||||||
<string name="prefs_locator_title">Pengaturan lokator QTH</string>
|
|
||||||
<string name="prefs_locator_text">Atur posisi stasiun menggunakan lokator</string>
|
|
||||||
<string name="prefs_data_title">Data satelit</string>
|
|
||||||
<string name="prefs_data_entries">Satelit: %s</string>
|
|
||||||
<string name="prefs_data_radios">Transceiver: %s</string>
|
|
||||||
<string name="prefs_data_update">Perbarui</string>
|
|
||||||
<string name="prefs_data_import">URL Kustom</string>
|
|
||||||
<string name="prefs_data_clear">Bersihkan</string>
|
|
||||||
<string name="prefs_data_clear_success">Data berhasil dibersihkan</string>
|
|
||||||
<string name="prefs_data_update_success">Pembaruan berhasil diselesaikan</string>
|
|
||||||
<string name="prefs_data_update_failed">Pembaruan gagal - periksa jaringan Anda</string>
|
|
||||||
<string name="prefs_data_import_satellites_error">Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid.</string>
|
|
||||||
<string name="prefs_data_import_transceivers_error">Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid.</string>
|
|
||||||
<string name="prefs_data_sources_title">Sumber data kustom</string>
|
|
||||||
<string name="prefs_data_sources_tle_switch">URL TLE Kustom</string>
|
|
||||||
<string name="prefs_data_sources_transceivers_switch">URL transceiver kustom</string>
|
|
||||||
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
|
||||||
<string name="prefs_data_reset_url">Setel ulang ke URL bawaan</string>
|
|
||||||
<string name="prefs_data_output_title">Keluaran data</string>
|
|
||||||
<string name="prefs_net_output">Jaringan</string>
|
|
||||||
<string name="prefs_bt_output">Bluetooth</string>
|
|
||||||
<string name="prefs_cat_output">CAT</string>
|
|
||||||
<string name="nav_radiocontrol">Kontrol Radio</string>
|
|
||||||
<string name="rc_settings_title">Kontrol Radio CAT</string>
|
|
||||||
<string name="rc_radio_model">Model Radio</string>
|
|
||||||
<string name="rc_tx_device_hint">Alamat BT Radio TX</string>
|
|
||||||
<string name="rc_rx_device_hint">Alamat BT Radio RX</string>
|
|
||||||
<string name="rc_tx_name_hint">Nama Radio TX</string>
|
|
||||||
<string name="rc_rx_name_hint">Nama Radio RX</string>
|
|
||||||
<string name="rc_enable_switch">Aktifkan Kontrol CAT</string>
|
|
||||||
<string name="prefs_net_title">Keluaran data jaringan</string>
|
|
||||||
<string name="prefs_net_rotator_switch">Aktifkan keluaran rotator</string>
|
|
||||||
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
|
||||||
<string name="prefs_net_rotator_format_hint">Format</string>
|
|
||||||
<string name="prefs_net_frequency_switch">Aktifkan keluaran frekuensi</string>
|
|
||||||
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
|
||||||
<string name="prefs_net_frequency_format_hint">Format</string>
|
|
||||||
<string name="prefs_bt_title">Keluaran data Bluetooth</string>
|
|
||||||
<string name="prefs_bt_rotator_switch">Aktifkan keluaran rotator</string>
|
|
||||||
<string name="prefs_bt_rotator_device_hint">Id perangkat</string>
|
|
||||||
<string name="prefs_bt_rotator_output_hint">Format data</string>
|
|
||||||
<string name="prefs_bt_frequency_switch">Aktifkan keluaran frekuensi</string>
|
|
||||||
<string name="prefs_bt_frequency_device_hint">Id perangkat</string>
|
|
||||||
<string name="prefs_bt_frequency_output_hint">Format data</string>
|
|
||||||
<string name="prefs_bt_perm_error">Periksa izin bluetooth Anda</string>
|
|
||||||
<string name="prefs_net_perm_error">Periksa izin jaringan Anda</string>
|
|
||||||
<string name="tracking_status">Melacak: %s</string>
|
|
||||||
<string name="loc_lat_text">Lintang: %s°</string>
|
|
||||||
<string name="loc_lon_text">Bujur: %s°</string>
|
|
||||||
<string name="loc_qth_text">QTH: %s</string>
|
|
||||||
<string name="connect_radio">Hubungkan</string>
|
|
||||||
<string name="disconnect_radio">Putuskan</string>
|
|
||||||
<string name="track_radio">Lacak</string>
|
|
||||||
<string name="stop_radio">Berhenti</string>
|
|
||||||
<string name="prefs_wavelog_title">Server log WaveLog</string>
|
|
||||||
<string name="prefs_wavelog_url_hint">Alamat server</string>
|
|
||||||
<string name="prefs_wavelog_key_hint">Kunci API</string>
|
|
||||||
<string name="prefs_wavelog_station_hint">ID stasiun</string>
|
|
||||||
<string name="prefs_wavelog_auto">Unggah otomatis</string>
|
|
||||||
<string name="prefs_wavelog_test">Uji koneksi</string>
|
|
||||||
<string name="prefs_wavelog_upload">Unggah sekarang</string>
|
|
||||||
<string name="wavelog_title">Log</string>
|
|
||||||
<string name="wavelog_select_transponder">Pilih transponder</string>
|
|
||||||
<string name="wavelog_call_hint">Tanda panggil</string>
|
|
||||||
<string name="wavelog_mode_hint">Mode</string>
|
|
||||||
<string name="wavelog_saved">Tersimpan ke log lokal</string>
|
|
||||||
<string name="wavelog_need_config">Konfigurasikan WaveLog di Pengaturan terlebih dahulu</string>
|
|
||||||
<string name="wavelog_empty">Belum ada entri log</string>
|
|
||||||
<string name="wavelog_grid_mismatch">Grid QTH (%1$s) tidak cocok dengan grid stasiun (%2$s). Tetap unggah?</string>
|
|
||||||
<string name="wavelog_ignore">Abaikan dan unggah</string>
|
|
||||||
<string name="wavelog_cancel">Batal</string>
|
|
||||||
<string name="wavelog_copy">Salin</string>
|
|
||||||
<string name="wavelog_delete_undo">Urungkan</string>
|
|
||||||
<string name="wavelog_col_time">Waktu</string>
|
|
||||||
<string name="wavelog_col_freq">Frek.</string>
|
|
||||||
<string name="wavelog_col_sat">Satelit</string>
|
|
||||||
<string name="wavelog_col_call">Panggilan</string>
|
|
||||||
<string name="wavelog_col_uploaded">Terkirim</string>
|
|
||||||
<string name="prefs_other_title">Pengaturan lainnya</string>
|
|
||||||
<string name="prefs_ui_title">Pengaturan UI</string>
|
|
||||||
<string name="prefs_ui_order_title">Urutan halaman</string>
|
|
||||||
<string name="prefs_ui_main_title">Menu utama (bilah bawah)</string>
|
|
||||||
<string name="prefs_ui_sub_title">Menu lainnya</string>
|
|
||||||
<string name="prefs_ui_move_out">Pindah ke menu lainnya</string>
|
|
||||||
<string name="prefs_ui_move_back">Kembalikan ke menu utama</string>
|
|
||||||
<string name="prefs_ui_order_locked">Tetap (tidak dapat dipindahkan)</string>
|
|
||||||
<string name="prefs_ui_order_reset">Setel ulang urutan</string>
|
|
||||||
<string name="prefs_other_switch_utc">Tampilkan waktu lintasan dalam UTC</string>
|
|
||||||
<string name="prefs_other_switch_update">Aktifkan pembaruan data otomatis</string>
|
|
||||||
<string name="prefs_other_switch_sweep">Aktifkan animasi sapuan radar</string>
|
|
||||||
<string name="prefs_other_switch_sensors">Gunakan sensor untuk memutar tampilan radar</string>
|
|
||||||
<string name="prefs_other_switch_night_mode">Aktifkan filter mode malam merah</string>
|
|
||||||
<string name="prefs_highlight_title">Sorotan elevasi</string>
|
|
||||||
<string name="prefs_highlight_low">Rendah < %1$d°</string>
|
|
||||||
<string name="prefs_highlight_mid">Sedang %1$d–%2$d°</string>
|
|
||||||
<string name="prefs_highlight_high">Tinggi > %1$d°</string>
|
|
||||||
<string name="prefs_outro_title">BG7NTA & penulis asli mengucapkan terima kasih</string>
|
|
||||||
<string name="prefs_outro_thanks" translatable="false">
|
|
||||||
* Pengguna dan kontributor Look4Sat!
|
|
||||||
\n* David A. B. Johnson (predict4java)
|
|
||||||
\n* Dave Moten (predict4java)
|
|
||||||
\n* Alexandru Csete (Gpredict)
|
|
||||||
\n* Dr T.S. Kelso (Celestrak)
|
|
||||||
\n* Libre Space Foundation (SatNOGS)
|
|
||||||
\n* xdsopl dan kontributor Robot36!
|
|
||||||
\n* BA7OPF (fitur pencocokan lintasan)
|
|
||||||
\n* BG7NTA</string>
|
|
||||||
<string name="prefs_outro_license">Aplikasi ini hadir tanpa jaminan</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name" translatable="false">Look4Sat Pro</string>
|
|
||||||
<string name="btn_accept">Terima</string>
|
|
||||||
<string name="btn_cancel">Batal</string>
|
|
||||||
<string name="empty_list_message">Tidak ada data untuk ditampilkan</string>
|
|
||||||
<string name="nav_sat">Satelit</string>
|
|
||||||
<string name="nav_pass">Lintasan</string>
|
|
||||||
<string name="nav_radar">Radar</string>
|
|
||||||
<string name="nav_mutual">Pasangan</string>
|
|
||||||
<string name="nav_roaming">Jelajah</string>
|
|
||||||
<string name="nav_cw">Dekoder CW</string>
|
|
||||||
<string name="nav_map">Peta</string>
|
|
||||||
<string name="nav_prefs">Pengaturan</string>
|
|
||||||
<string name="prefs_aprs_title">Pelaporan APRS</string>
|
|
||||||
<string name="prefs_aprs_enable">Aktifkan pelaporan APRS</string>
|
|
||||||
<string name="prefs_aprs_summary">Server: %1$s:%2$d Panggilan: %3$s</string>
|
|
||||||
<string name="prefs_aprs_report_now">Lapor Sekarang</string>
|
|
||||||
<string name="prefs_aprs_settings_title">Pengaturan APRS</string>
|
|
||||||
<string name="prefs_aprs_server">Server</string>
|
|
||||||
<string name="prefs_aprs_port">Port</string>
|
|
||||||
<string name="prefs_aprs_callsign">Tanda panggilan</string>
|
|
||||||
<string name="prefs_aprs_ssid">SSID</string>
|
|
||||||
<string name="prefs_aprs_passcode">Kode sandi</string>
|
|
||||||
<string name="prefs_aprs_passcode_hint">Kosong = otomatis dari panggilan</string>
|
|
||||||
<string name="prefs_aprs_interval">Interval (menit)</string>
|
|
||||||
<string name="prefs_aprs_status">Teks status</string>
|
|
||||||
<string name="prefs_aprs_symbol_table">Tabel simbol</string>
|
|
||||||
<string name="prefs_aprs_symbol_code">Kode simbol</string>
|
|
||||||
<string name="prefs_aprs_save">Simpan</string>
|
|
||||||
<string name="prefs_aprs_cancel">Batal</string>
|
|
||||||
<string name="aprs_notif_channel">Layanan APRS</string>
|
|
||||||
<string name="aprs_notif_title">APRS: %1$s</string>
|
|
||||||
<string name="aprs_notif_running">Pelaporan berjalan</string>
|
|
||||||
<string name="aprs_notif_connected">Terhubung ke APRS-IS</string>
|
|
||||||
<string name="aprs_notif_error">Kesalahan koneksi</string>
|
|
||||||
<string name="aprs_notif_stop">Berhenti</string>
|
|
||||||
<string name="prefs_aprs_calc_passcode">Hitung kode sandi</string>
|
|
||||||
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
|
||||||
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
|
||||||
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
|
||||||
<string name="prefs_aprs_last_ok">OK</string>
|
|
||||||
<string name="prefs_aprs_last_fail">gagal - %1$s</string>
|
|
||||||
<string name="nav_more">Lainnya</string>
|
|
||||||
<string name="nav_log">Log</string>
|
|
||||||
<string name="nav_amsat">AMSAT</string>
|
|
||||||
<string name="amsat_title">Status Satelit AMSAT</string>
|
|
||||||
<string name="amsat_updated">Diperbarui:</string>
|
|
||||||
<string name="amsat_active">Aktif</string>
|
|
||||||
<string name="amsat_tlm">TLM/beacon</string>
|
|
||||||
<string name="amsat_not_heard">Tidak terdengar</string>
|
|
||||||
<string name="amsat_conflict">Konflik</string>
|
|
||||||
<string name="amsat_name">Name</string>
|
|
||||||
<string name="amsat_load_failed">Gagal memuat - periksa jaringan dan coba lagi</string>
|
|
||||||
<string name="amsat_no_reports">Tidak ada laporan untuk periode ini</string>
|
|
||||||
<string name="amsat_close">Tutup</string>
|
|
||||||
<string name="sat_type_hint">Jenis: %s</string>
|
|
||||||
<string name="sat_type_title">Pilih jenis satelit</string>
|
|
||||||
<string name="sat_search_hint">Id - Nama</string>
|
|
||||||
<string name="sat_search_clear">Bersihkan</string>
|
|
||||||
<string name="sat_clear_all">Bersihkan semua</string>
|
|
||||||
<string name="sat_select_all">Pilih semua</string>
|
|
||||||
<string name="sat_empty_list_message">Pastikan kata kunci pencarian Anda benar dan DB sudah diperbarui</string>
|
|
||||||
<string name="sat_warning_title">Peringatan\!</string>
|
|
||||||
<string name="sat_warning_message">
|
|
||||||
Ada lebih dari 9000 satelit yang terdaftar di aplikasi ini.
|
|
||||||
Tidak masuk akal untuk melacak semuanya sekaligus.
|
|
||||||
\n\nSelalu coba persempit daftar hanya ke satelit yang
|
|
||||||
Anda minati melalui pencarian dan pemilih jenis.</string>
|
|
||||||
<string name="pass_filter_title">Filter lintasan</string>
|
|
||||||
<string name="pass_filter_elev">Elevasi minimal</string>
|
|
||||||
<string name="pass_filter_hours">Waktu ke depan</string>
|
|
||||||
<string name="pass_filter_aos_time">Jendela AOS</string>
|
|
||||||
<string name="pass_filter_invert_time">Balik jendela AOS</string>
|
|
||||||
<string name="pass_filter_deep_space">DeepSpace (periode >225 mnt)</string>
|
|
||||||
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
|
||||||
<string name="pass_modes_title">Pilih jenis modulasi</string>
|
|
||||||
<string name="pass_satId" translatable="false">%05d</string>
|
|
||||||
<string name="pass_elevation">Elevasi: %.1f°</string>
|
|
||||||
<string name="pass_deep_space" translatable="false">DeepSpace</string>
|
|
||||||
<string name="pass_altitude">Ketinggian: %d km</string>
|
|
||||||
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
|
|
||||||
<string name="pass_empty_list_message">
|
|
||||||
Pastikan filter Anda benar dan Anda sudah memilih satelit</string>
|
|
||||||
<string name="pass_error_message">Tidak ada lintasan satelit mendatang untuk ditampilkan</string>
|
|
||||||
<string name="pass_welcome_title">Selamat datang di Look4Sat\!</string>
|
|
||||||
<string name="pass_welcome_message">
|
|
||||||
Pastikan Anda mengatur posisi melalui GPS, Lat/Lon atau QTH di Pengaturan.
|
|
||||||
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.</string>
|
|
||||||
<string name="pass_whatsnew_title" translatable="false">Apa yang baru di Look4Sat</string>
|
|
||||||
<string name="pass_whatsnew_message" translatable="false">
|
|
||||||
* BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari
|
|
||||||
* Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi
|
|
||||||
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
|
|
||||||
* Pesan kegagalan jelas saat database satelit gagal diunduh
|
|
||||||
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
|
|
||||||
* Perbaikan bahasa Indonesia (values-id)
|
|
||||||
* BARU: Pelaporan APRS (jaringan APRS-IS, layanan latar depan dengan notifikasi, interval dapat diatur)</string>
|
|
||||||
<string name="radar_back">Kembali</string>
|
|
||||||
<string name="radar_notify">Beri tahu</string>
|
|
||||||
<string name="radar_az_text">Azimuth</string>
|
|
||||||
<string name="radar_az_value" translatable="false">%.1f°</string>
|
|
||||||
<string name="radar_el_text">Elevasi</string>
|
|
||||||
<string name="radar_el_value" translatable="false">%.1f°</string>
|
|
||||||
<string name="radar_alt_text">Ketinggian</string>
|
|
||||||
<string name="radar_alt_value" translatable="false">%.0f km</string>
|
|
||||||
<string name="radar_dist_text">Jarak</string>
|
|
||||||
<string name="radar_dist_value" translatable="false">%.0f km</string>
|
|
||||||
<string name="radar_eclipsed">Gerhana</string>
|
|
||||||
<string name="radar_downlink">Downlink</string>
|
|
||||||
<string name="radar_uplink">Uplink</string>
|
|
||||||
<string name="radar_link_low" translatable="false">%.4f</string>
|
|
||||||
<string name="radar_link_lowHigh" translatable="false">%.4f - %.4f</string>
|
|
||||||
<string name="radar_no_link" translatable="false">- - . - -</string>
|
|
||||||
<string name="radar_no_data">Satelit ini tidak memiliki transceiver</string>
|
|
||||||
<string name="radar_add">Tambah</string>
|
|
||||||
<string name="radar_data">Transceiver satelit:</string>
|
|
||||||
<string name="radar_inverted">Terbalik: %s</string>
|
|
||||||
<string name="radar_mode">Mode: %s</string>
|
|
||||||
<string name="radar_string_no">Tidak</string>
|
|
||||||
<string name="radar_string_yes">Ya</string>
|
|
||||||
<string name="radar_visible">Terlihat</string>
|
|
||||||
<string name="radar_doppler_calc">Kalkulator Frekuensi Doppler</string>
|
|
||||||
<string name="radar_doppler_tx_hint">Masukkan frekuensi TX (MHz)</string>
|
|
||||||
<string name="radar_doppler_rx_hint">Masukkan frekuensi RX (MHz)</string>
|
|
||||||
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
|
|
||||||
<string name="radar_doppler_info">Untuk transponder linier, ketik satu frekuensi untuk melihat frekuensi lainnya</string>
|
|
||||||
<string name="radar_cw_decoder">Dekoder CW</string>
|
|
||||||
<string name="radar_cw_start">Mulai</string>
|
|
||||||
<string name="radar_cw_stop">Berhenti</string>
|
|
||||||
<string name="radar_cw_reset">Bersihkan</string>
|
|
||||||
<string name="map_prev">Sebelumnya</string>
|
|
||||||
<string name="map_next">Berikutnya</string>
|
|
||||||
<string name="map_copyright" translatable="false">© Kontributor OpenStreetMap</string>
|
|
||||||
<string name="map_azimuth">Azimuth: %.1f°</string>
|
|
||||||
<string name="map_elevation">Elevasi: %.1f°</string>
|
|
||||||
<string name="map_altitude">Ketinggian: %.0f km</string>
|
|
||||||
<string name="map_distance">Jarak: %.0f km</string>
|
|
||||||
<string name="map_latitude">Lintang: %.1f°</string>
|
|
||||||
<string name="map_longitude">Bujur: %.1f°</string>
|
|
||||||
<string name="map_qth" translatable="false">QTH: %s</string>
|
|
||||||
<string name="map_phase">Fase: %.1f°</string>
|
|
||||||
<string name="map_eclipsed">Gerhana</string>
|
|
||||||
<string name="map_period">Periode: %.0f mnt</string>
|
|
||||||
<string name="map_velocity">Kecepatan: %.2f km/d</string>
|
|
||||||
<string name="map_visibility">Visibilitas: %s</string>
|
|
||||||
<string name="map_visible">Terlihat</string>
|
|
||||||
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
|
|
||||||
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
|
||||||
<string name="prefs_donate_title">Donasi</string>
|
|
||||||
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
|
|
||||||
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
|
|
||||||
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
|
|
||||||
<string name="prefs_github_title" translatable="false">GitHub</string>
|
|
||||||
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
|
|
||||||
<string name="prefs_license_title" translatable="false">GPLv3</string>
|
|
||||||
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
|
|
||||||
<string name="prefs_privacy_title">Kebijakan Privasi</string>
|
|
||||||
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
|
|
||||||
<string name="prefs_updated_title">Diperbarui: %s</string>
|
|
||||||
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
|
||||||
<string name="prefs_loc_title">Posisi stasiun</string>
|
|
||||||
<string name="prefs_loc_gps_title">GPS</string>
|
|
||||||
<string name="prefs_loc_gps_error">Tidak dapat mengambil lokasi - periksa izin dan sinyal GPS/jaringan</string>
|
|
||||||
<string name="prefs_loc_input_title">Input</string>
|
|
||||||
<string name="prefs_loc_input_error">Lokasi yang dimasukkan tidak valid</string>
|
|
||||||
<string name="prefs_loc_qth_title">QTH</string>
|
|
||||||
<string name="prefs_loc_qth_error">Lokator QTH tidak valid</string>
|
|
||||||
<string name="prefs_loc_success">Posisi berhasil diperbarui</string>
|
|
||||||
<string name="prefs_station_title">Pengaturan posisi stasiun</string>
|
|
||||||
<string name="prefs_station_lat_text">Atur garis lintang stasiun Anda</string>
|
|
||||||
<string name="prefs_station_lon_text">Atur garis bujur stasiun Anda</string>
|
|
||||||
<string name="prefs_locator_title">Pengaturan lokator QTH</string>
|
|
||||||
<string name="prefs_locator_text">Atur posisi stasiun menggunakan lokator</string>
|
|
||||||
<string name="prefs_data_title">Data satelit</string>
|
|
||||||
<string name="prefs_data_entries">Satelit: %s</string>
|
|
||||||
<string name="prefs_data_radios">Transceiver: %s</string>
|
|
||||||
<string name="prefs_data_update">Perbarui</string>
|
|
||||||
<string name="prefs_data_import">URL Kustom</string>
|
|
||||||
<string name="prefs_data_clear">Bersihkan</string>
|
|
||||||
<string name="prefs_data_clear_success">Data berhasil dibersihkan</string>
|
|
||||||
<string name="prefs_data_update_success">Pembaruan berhasil diselesaikan</string>
|
|
||||||
<string name="prefs_data_update_failed">Pembaruan gagal - periksa jaringan Anda</string>
|
|
||||||
<string name="prefs_data_import_satellites_error">Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid.</string>
|
|
||||||
<string name="prefs_data_import_transceivers_error">Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid.</string>
|
|
||||||
<string name="prefs_data_sources_title">Sumber data kustom</string>
|
|
||||||
<string name="prefs_data_sources_tle_switch">URL TLE Kustom</string>
|
|
||||||
<string name="prefs_data_sources_transceivers_switch">URL transceiver kustom</string>
|
|
||||||
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
|
||||||
<string name="prefs_data_reset_url">Setel ulang ke URL bawaan</string>
|
|
||||||
<string name="prefs_data_output_title">Keluaran data</string>
|
|
||||||
<string name="prefs_net_output">Jaringan</string>
|
|
||||||
<string name="prefs_bt_output">Bluetooth</string>
|
|
||||||
<string name="prefs_cat_output">CAT</string>
|
|
||||||
<string name="nav_radiocontrol">Kontrol Radio</string>
|
|
||||||
<string name="rc_settings_title">Kontrol Radio CAT</string>
|
|
||||||
<string name="rc_radio_model">Model Radio</string>
|
|
||||||
<string name="rc_tx_device_hint">Alamat BT Radio TX</string>
|
|
||||||
<string name="rc_rx_device_hint">Alamat BT Radio RX</string>
|
|
||||||
<string name="rc_tx_name_hint">Nama Radio TX</string>
|
|
||||||
<string name="rc_rx_name_hint">Nama Radio RX</string>
|
|
||||||
<string name="rc_enable_switch">Aktifkan Kontrol CAT</string>
|
|
||||||
<string name="prefs_net_title">Keluaran data jaringan</string>
|
|
||||||
<string name="prefs_net_rotator_switch">Aktifkan keluaran rotator</string>
|
|
||||||
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
|
||||||
<string name="prefs_net_rotator_format_hint">Format</string>
|
|
||||||
<string name="prefs_net_frequency_switch">Aktifkan keluaran frekuensi</string>
|
|
||||||
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
|
||||||
<string name="prefs_net_frequency_format_hint">Format</string>
|
|
||||||
<string name="prefs_bt_title">Keluaran data Bluetooth</string>
|
|
||||||
<string name="prefs_bt_rotator_switch">Aktifkan keluaran rotator</string>
|
|
||||||
<string name="prefs_bt_rotator_device_hint">Id perangkat</string>
|
|
||||||
<string name="prefs_bt_rotator_output_hint">Format data</string>
|
|
||||||
<string name="prefs_bt_frequency_switch">Aktifkan keluaran frekuensi</string>
|
|
||||||
<string name="prefs_bt_frequency_device_hint">Id perangkat</string>
|
|
||||||
<string name="prefs_bt_frequency_output_hint">Format data</string>
|
|
||||||
<string name="prefs_bt_perm_error">Periksa izin bluetooth Anda</string>
|
|
||||||
<string name="prefs_net_perm_error">Periksa izin jaringan Anda</string>
|
|
||||||
<string name="tracking_status">Melacak: %s</string>
|
|
||||||
<string name="loc_lat_text">Lintang: %s°</string>
|
|
||||||
<string name="loc_lon_text">Bujur: %s°</string>
|
|
||||||
<string name="loc_qth_text">QTH: %s</string>
|
|
||||||
<string name="connect_radio">Hubungkan</string>
|
|
||||||
<string name="disconnect_radio">Putuskan</string>
|
|
||||||
<string name="track_radio">Lacak</string>
|
|
||||||
<string name="stop_radio">Berhenti</string>
|
|
||||||
<string name="prefs_wavelog_title">Server log WaveLog</string>
|
|
||||||
<string name="prefs_wavelog_url_hint">Alamat server</string>
|
|
||||||
<string name="prefs_wavelog_key_hint">Kunci API</string>
|
|
||||||
<string name="prefs_wavelog_station_hint">ID stasiun</string>
|
|
||||||
<string name="prefs_wavelog_auto">Unggah otomatis</string>
|
|
||||||
<string name="prefs_wavelog_test">Uji koneksi</string>
|
|
||||||
<string name="prefs_wavelog_upload">Unggah sekarang</string>
|
|
||||||
<string name="wavelog_title">Log</string>
|
|
||||||
<string name="wavelog_select_transponder">Pilih transponder</string>
|
|
||||||
<string name="wavelog_call_hint">Tanda panggil</string>
|
|
||||||
<string name="wavelog_mode_hint">Mode</string>
|
|
||||||
<string name="wavelog_saved">Tersimpan ke log lokal</string>
|
|
||||||
<string name="wavelog_need_config">Konfigurasikan WaveLog di Pengaturan terlebih dahulu</string>
|
|
||||||
<string name="wavelog_empty">Belum ada entri log</string>
|
|
||||||
<string name="wavelog_grid_mismatch">Grid QTH (%1$s) tidak cocok dengan grid stasiun (%2$s). Tetap unggah?</string>
|
|
||||||
<string name="wavelog_ignore">Abaikan dan unggah</string>
|
|
||||||
<string name="wavelog_cancel">Batal</string>
|
|
||||||
<string name="wavelog_copy">Salin</string>
|
|
||||||
<string name="wavelog_delete_undo">Urungkan</string>
|
|
||||||
<string name="wavelog_col_time">Waktu</string>
|
|
||||||
<string name="wavelog_col_freq">Frek.</string>
|
|
||||||
<string name="wavelog_col_sat">Satelit</string>
|
|
||||||
<string name="wavelog_col_call">Panggilan</string>
|
|
||||||
<string name="wavelog_col_uploaded">Terkirim</string>
|
|
||||||
<string name="wavelog_ungrouped">Tidak dikelompokkan</string>
|
|
||||||
<string name="prefs_other_title">Pengaturan lainnya</string>
|
|
||||||
<string name="prefs_ui_title">Pengaturan UI</string>
|
|
||||||
<string name="prefs_ui_order_title">Urutan halaman</string>
|
|
||||||
<string name="prefs_ui_main_title">Menu utama (bilah bawah)</string>
|
|
||||||
<string name="prefs_ui_sub_title">Menu lainnya</string>
|
|
||||||
<string name="prefs_ui_move_out">Pindah ke menu lainnya</string>
|
|
||||||
<string name="prefs_ui_move_back">Kembalikan ke menu utama</string>
|
|
||||||
<string name="prefs_ui_order_locked">Tetap (tidak dapat dipindahkan)</string>
|
|
||||||
<string name="prefs_ui_order_reset">Setel ulang urutan</string>
|
|
||||||
<string name="prefs_other_switch_utc">Tampilkan waktu lintasan dalam UTC</string>
|
|
||||||
<string name="prefs_other_switch_update">Aktifkan pembaruan data otomatis</string>
|
|
||||||
<string name="prefs_other_switch_sweep">Aktifkan animasi sapuan radar</string>
|
|
||||||
<string name="prefs_other_switch_sensors">Gunakan sensor untuk memutar tampilan radar</string>
|
|
||||||
<string name="prefs_other_switch_night_mode">Aktifkan filter mode malam merah</string>
|
|
||||||
<string name="prefs_highlight_title">Sorotan elevasi</string>
|
|
||||||
<string name="prefs_highlight_low">Rendah < %1$d°</string>
|
|
||||||
<string name="prefs_highlight_mid">Sedang %1$d–%2$d°</string>
|
|
||||||
<string name="prefs_highlight_high">Tinggi > %1$d°</string>
|
|
||||||
<string name="prefs_outro_title">BG7NTA & penulis asli mengucapkan terima kasih</string>
|
|
||||||
<string name="prefs_outro_thanks" translatable="false">
|
|
||||||
* Pengguna dan kontributor Look4Sat!
|
|
||||||
\n* David A. B. Johnson (predict4java)
|
|
||||||
\n* Dave Moten (predict4java)
|
|
||||||
\n* Alexandru Csete (Gpredict)
|
|
||||||
\n* Dr T.S. Kelso (Celestrak)
|
|
||||||
\n* Libre Space Foundation (SatNOGS)
|
|
||||||
\n* xdsopl dan kontributor Robot36!
|
|
||||||
\n* BA7OPF (fitur pencocokan lintasan)
|
|
||||||
\n* BG7NTA</string>
|
|
||||||
<string name="prefs_outro_license">Aplikasi ini hadir tanpa jaminan</string>
|
|
||||||
</resources>
|
|
||||||
@@ -13,19 +13,23 @@
|
|||||||
<string name="nav_prefs">Настройки</string>
|
<string name="nav_prefs">Настройки</string>
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">Тип: %s</string>
|
<string name="sat_type_hint">Режимы: %s</string>
|
||||||
<string name="sat_type_title">Выберите тип спутника</string>
|
<string name="sat_type_title">Выберите режимы</string>
|
||||||
<string name="sat_search_hint">Id - Название</string>
|
<string name="sat_search_hint">Id - Название</string>
|
||||||
<string name="sat_search_clear">Очистить</string>
|
<string name="sat_search_clear">Очистить</string>
|
||||||
<string name="sat_clear_all">Очистить</string>
|
<string name="sat_clear_all">Очистить</string>
|
||||||
<string name="sat_select_all">Выбрать</string>
|
<string name="sat_select_all">Выбрать</string>
|
||||||
|
<string name="sat_group_selected">Выбранные</string>
|
||||||
|
<string name="sat_group_available">Доступные</string>
|
||||||
|
<string name="sat_group_selected_count">Выбранные (%1$d)</string>
|
||||||
|
<string name="sat_group_available_count">Доступные (%1$d)</string>
|
||||||
<string name="sat_empty_list_message">Убедитесь, что ваш поисковый запрос верен и база данных обновлена</string>
|
<string name="sat_empty_list_message">Убедитесь, что ваш поисковый запрос верен и база данных обновлена</string>
|
||||||
<string name="sat_warning_title">Внимание\!</string>
|
<string name="sat_warning_title">Внимание\!</string>
|
||||||
<string name="sat_warning_message">
|
<string name="sat_warning_message">
|
||||||
В этом приложении перечислено более 9000 спутников.
|
В этом приложении перечислено более 9000 спутников.
|
||||||
Отслеживать их все одновременно бессмысленно.
|
Отслеживать их все одновременно бессмысленно.
|
||||||
\n\nВсегда старайтесь сузить список до тех,
|
\n\nВсегда старайтесь сузить список до тех,
|
||||||
которые вас интересуют, используя поиск и селектор типов.</string>
|
которые вас интересуют, используя поиск и выбор режимов.</string>
|
||||||
|
|
||||||
<!-- Passes screen -->
|
<!-- Passes screen -->
|
||||||
<string name="pass_filter_title">Фильтровать пролеты</string>
|
<string name="pass_filter_title">Фильтровать пролеты</string>
|
||||||
@@ -34,7 +38,7 @@
|
|||||||
<string name="pass_filter_aos_time">Окно AOS</string>
|
<string name="pass_filter_aos_time">Окно AOS</string>
|
||||||
<string name="pass_filter_invert_time">Инвертировать окно AOS</string>
|
<string name="pass_filter_invert_time">Инвертировать окно AOS</string>
|
||||||
<string name="pass_filter_deep_space">DeepSpace (период >225мин)</string>
|
<string name="pass_filter_deep_space">DeepSpace (период >225мин)</string>
|
||||||
<string name="pass_modes_title">Выберите тип модуляции</string>
|
<string name="pass_modes_title">Выберите режимы</string>
|
||||||
<string name="pass_elevation">Элевация: %.1f°</string>
|
<string name="pass_elevation">Элевация: %.1f°</string>
|
||||||
<string name="pass_altitude">Высота: %d км</string>
|
<string name="pass_altitude">Высота: %d км</string>
|
||||||
<string name="pass_empty_list_message">
|
<string name="pass_empty_list_message">
|
||||||
@@ -95,11 +99,11 @@
|
|||||||
<string name="prefs_loc_qth_title">QTH</string>
|
<string name="prefs_loc_qth_title">QTH</string>
|
||||||
<string name="prefs_loc_qth_error">Неправильный локатор QTH</string>
|
<string name="prefs_loc_qth_error">Неправильный локатор QTH</string>
|
||||||
<string name="prefs_loc_success">Позиция успешно обновлена</string>
|
<string name="prefs_loc_success">Позиция успешно обновлена</string>
|
||||||
<string name="prefs_station_title">Настройки местоположения</string>
|
<string name="prefs_station_title">Положение станции</string>
|
||||||
<string name="prefs_station_lat_text">Введите широту наземной станции</string>
|
<string name="prefs_station_lat_text">Широта станции</string>
|
||||||
<string name="prefs_station_lon_text">Введите долготу наземной станции</string>
|
<string name="prefs_station_lon_text">Долгота станции</string>
|
||||||
<string name="prefs_locator_title">Настройки локатора QTH</string>
|
<string name="prefs_locator_title">Локатор QTH</string>
|
||||||
<string name="prefs_locator_text">Введите позицию через локатор</string>
|
<string name="prefs_locator_text">Позиция по локатору</string>
|
||||||
|
|
||||||
<string name="prefs_data_title">Данные спутников</string>
|
<string name="prefs_data_title">Данные спутников</string>
|
||||||
<string name="prefs_data_entries">Спутников: %s</string>
|
<string name="prefs_data_entries">Спутников: %s</string>
|
||||||
@@ -109,16 +113,30 @@
|
|||||||
<string name="prefs_data_clear">Очистить</string>
|
<string name="prefs_data_clear">Очистить</string>
|
||||||
<string name="prefs_data_clear_success">Очистка прошла успешно</string>
|
<string name="prefs_data_clear_success">Очистка прошла успешно</string>
|
||||||
<string name="prefs_data_update_success">Обновление прошло успешно</string>
|
<string name="prefs_data_update_success">Обновление прошло успешно</string>
|
||||||
|
<string name="prefs_data_import_satellites_error">Спутники не импортированы. Нужен TLE/3LE (.txt) или OMM (.csv).</string>
|
||||||
|
<string name="prefs_data_import_transceivers_error">Трансиверы не импортированы. Нужен SatNOGS (.json).</string>
|
||||||
|
|
||||||
<string name="prefs_data_sources_title">Свои источники данных</string>
|
<string name="prefs_data_sources_title">Свои источники</string>
|
||||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
<string name="prefs_data_sources_tle_switch">URL TLE</string>
|
||||||
<string name="prefs_data_sources_transceivers_switch">Custom Transceivers URL</string>
|
<string name="prefs_data_sources_transceivers_switch">URL трансиверов</string>
|
||||||
|
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
||||||
|
|
||||||
<string name="prefs_data_output_title">Вывод данных</string>
|
<string name="prefs_data_output_title">Вывод</string>
|
||||||
<string name="prefs_net_output">Сеть</string>
|
<string name="prefs_net_output">Сеть</string>
|
||||||
<string name="prefs_bt_output">Bluetooth</string>
|
<string name="prefs_bt_output">Bluetooth</string>
|
||||||
|
<string name="prefs_cat_output">CAT</string>
|
||||||
|
|
||||||
<string name="prefs_net_title">Вывод сетевых данных</string>
|
<string name="nav_radiocontrol">Управление радио</string>
|
||||||
|
|
||||||
|
<string name="rc_settings_title">CAT управление</string>
|
||||||
|
<string name="rc_radio_model">Модель радио</string>
|
||||||
|
<string name="rc_tx_device_hint">BT адрес TX</string>
|
||||||
|
<string name="rc_rx_device_hint">BT адрес RX</string>
|
||||||
|
<string name="rc_tx_name_hint">Имя радио TX</string>
|
||||||
|
<string name="rc_rx_name_hint">Имя радио RX</string>
|
||||||
|
<string name="rc_enable_switch">Включить CAT</string>
|
||||||
|
|
||||||
|
<string name="prefs_net_title">Сетевой вывод</string>
|
||||||
<string name="prefs_net_rotator_switch">Включить вывод ротатора</string>
|
<string name="prefs_net_rotator_switch">Включить вывод ротатора</string>
|
||||||
<string name="prefs_net_rotator_address_hint">IP:Порт</string>
|
<string name="prefs_net_rotator_address_hint">IP:Порт</string>
|
||||||
<string name="prefs_net_rotator_format_hint">Формат</string>
|
<string name="prefs_net_rotator_format_hint">Формат</string>
|
||||||
@@ -126,22 +144,22 @@
|
|||||||
<string name="prefs_net_frequency_address_hint">IP:Порт</string>
|
<string name="prefs_net_frequency_address_hint">IP:Порт</string>
|
||||||
<string name="prefs_net_frequency_format_hint">Формат</string>
|
<string name="prefs_net_frequency_format_hint">Формат</string>
|
||||||
|
|
||||||
<string name="prefs_bt_title">Вывод данных Bluetooth</string>
|
<string name="prefs_bt_title">Вывод Bluetooth</string>
|
||||||
<string name="prefs_bt_rotator_switch">Включить вывод ротатора</string>
|
<string name="prefs_bt_rotator_switch">Включить вывод ротатора</string>
|
||||||
<string name="prefs_bt_rotator_device_hint">Id устройства</string>
|
<string name="prefs_bt_rotator_device_hint">ID устройства</string>
|
||||||
<string name="prefs_bt_rotator_output_hint">Формат</string>
|
<string name="prefs_bt_rotator_output_hint">Формат</string>
|
||||||
<string name="prefs_bt_frequency_switch">Включить вывод частоты</string>
|
<string name="prefs_bt_frequency_switch">Включить вывод частоты</string>
|
||||||
<string name="prefs_bt_frequency_device_hint">Id устройства</string>
|
<string name="prefs_bt_frequency_device_hint">ID устройства</string>
|
||||||
<string name="prefs_bt_frequency_output_hint">Формат</string>
|
<string name="prefs_bt_frequency_output_hint">Формат</string>
|
||||||
<string name="prefs_bt_perm_error">Нет разрешения использовать bluetooth</string>
|
<string name="prefs_bt_perm_error">Нет разрешения использовать bluetooth</string>
|
||||||
<string name="prefs_net_perm_error">Нет разрешения использовать сеть</string>
|
<string name="prefs_net_perm_error">Нет разрешения использовать сеть</string>
|
||||||
|
|
||||||
<string name="prefs_other_title">Другие настройки</string>
|
<string name="prefs_other_title">Прочее</string>
|
||||||
<string name="prefs_other_switch_utc">Показывать время по UTC</string>
|
<string name="prefs_other_switch_utc">Время в UTC</string>
|
||||||
<string name="prefs_other_switch_update">Обновлять данные автоматически</string>
|
<string name="prefs_other_switch_update">Автообновление данных</string>
|
||||||
<string name="prefs_other_switch_sweep">Показывать анимацию радара</string>
|
<string name="prefs_other_switch_sweep">Анимация радара</string>
|
||||||
<string name="prefs_other_switch_sensors">Использовать сенсоры устройства</string>
|
<string name="prefs_other_switch_sensors">Сенсоры</string>
|
||||||
<string name="prefs_other_switch_night_mode">Включить красный ночной фильтр</string>
|
<string name="prefs_other_switch_night_mode">Ночной фильтр</string>
|
||||||
|
|
||||||
<string name="prefs_highlight_title">Подсветка высоты</string>
|
<string name="prefs_highlight_title">Подсветка высоты</string>
|
||||||
<string name="prefs_highlight_low">Низко < %1$d°</string>
|
<string name="prefs_highlight_low">Низко < %1$d°</string>
|
||||||
|
|||||||
@@ -13,19 +13,23 @@
|
|||||||
<string name="nav_prefs">පසුතල</string>
|
<string name="nav_prefs">පසුතල</string>
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">වර්ගය: %s</string>
|
<string name="sat_type_hint">මාදිලි: %s</string>
|
||||||
<string name="sat_type_title">චන්ද්රිකා ආකාරය තෝරන්න</string>
|
<string name="sat_type_title">මාදිලි තෝරන්න</string>
|
||||||
<string name="sat_search_hint">Id - නම</string>
|
<string name="sat_search_hint">Id - නම</string>
|
||||||
<string name="sat_search_clear">පිරිසිදු ක°</string>
|
<string name="sat_search_clear">පිරිසිදු ක°</string>
|
||||||
<string name="sat_clear_all">සියල්ල පිරිසිදු ක°</string>
|
<string name="sat_clear_all">සියල්ල පිරිසිදු ක°</string>
|
||||||
<string name="sat_select_all">සියල්ල තෝරන්න</string>
|
<string name="sat_select_all">සියල්ල තෝරන්න</string>
|
||||||
|
<string name="sat_group_selected">තෝරාගත්</string>
|
||||||
|
<string name="sat_group_available">තිබෙන</string>
|
||||||
|
<string name="sat_group_selected_count">තෝරාගත් (%1$d)</string>
|
||||||
|
<string name="sat_group_available_count">තිබෙන (%1$d)</string>
|
||||||
<string name="sat_empty_list_message">
|
<string name="sat_empty_list_message">
|
||||||
ඔබගේ සෙවුම් විමසුම නිවැරදි බවත් දත්ත සමුදාය යාවත්කාලීන කර ඇති බවත් සහතික කර ගන්න</string>
|
ඔබගේ සෙවුම් විමසුම නිවැරදි බවත් දත්ත සමුදාය යාවත්කාලීන කර ඇති බවත් සහතික කර ගන්න</string>
|
||||||
<string name="sat_warning_title">අවවාදයයි\!</string>
|
<string name="sat_warning_title">අවවාදයයි\!</string>
|
||||||
<string name="sat_warning_message">
|
<string name="sat_warning_message">
|
||||||
මෙම යෙදුමේ ලැයිස්තුගත කර ඇති චන්ද්රිකා 9000 කට වඩා තිබේ.
|
මෙම යෙදුමේ ලැයිස්තුගත කර ඇති චන්ද්රිකා 9000 කට වඩා තිබේ.
|
||||||
ඒවා සියල්ලම එකවර නිරීක්ෂණය කිරීම තේරුමක් නැති දෙයක්.
|
ඒවා සියල්ලම එකවර නිරීක්ෂණය කිරීම තේරුමක් නැති දෙයක්.
|
||||||
\n\nසෙවීම සහ වර්ග තේරීම හරහා ඔබ උනන්දුවක් දක්වන
|
\n\nසෙවීම සහ මාදිලි තේරීම හරහා ඔබ උනන්දුවක් දක්වන
|
||||||
ඒවාට පමණක් ලැයිස්තුව පටු කිරීමට සැමවිටම උත්සාහ කරන්න.</string>
|
ඒවාට පමණක් ලැයිස්තුව පටු කිරීමට සැමවිටම උත්සාහ කරන්න.</string>
|
||||||
|
|
||||||
<!-- Passes screen -->
|
<!-- Passes screen -->
|
||||||
@@ -35,7 +39,7 @@
|
|||||||
<string name="pass_filter_aos_time">AOS කවුළුව</string>
|
<string name="pass_filter_aos_time">AOS කවුළුව</string>
|
||||||
<string name="pass_filter_invert_time">AOS කවුළුව ප්රතිවිරුද්ධ කරන්න</string>
|
<string name="pass_filter_invert_time">AOS කවුළුව ප්රතිවිරුද්ධ කරන්න</string>
|
||||||
<string name="pass_filter_deep_space">DeepSpace (කාලය >225min)</string>
|
<string name="pass_filter_deep_space">DeepSpace (කාලය >225min)</string>
|
||||||
<string name="pass_modes_title">මූර්ජන ආකාරය තෝරන්න</string>
|
<string name="pass_modes_title">මූර්ජන තෝරන්න</string>
|
||||||
<string name="pass_elevation">උත්තෝලනය: %.1f°</string>
|
<string name="pass_elevation">උත්තෝලනය: %.1f°</string>
|
||||||
<string name="pass_altitude">උන්නතාශය: %d km</string>
|
<string name="pass_altitude">උන්නතාශය: %d km</string>
|
||||||
<string name="pass_empty_list_message">
|
<string name="pass_empty_list_message">
|
||||||
@@ -96,11 +100,11 @@
|
|||||||
<string name="prefs_loc_qth_title">QTH</string>
|
<string name="prefs_loc_qth_title">QTH</string>
|
||||||
<string name="prefs_loc_qth_error">QTH locator වලංගු නොවේ</string>
|
<string name="prefs_loc_qth_error">QTH locator වලංගු නොවේ</string>
|
||||||
<string name="prefs_loc_success">පිහිටීම යාවත්කාලීනය සාර්ථකයි</string>
|
<string name="prefs_loc_success">පිහිටීම යාවත්කාලීනය සාර්ථකයි</string>
|
||||||
<string name="prefs_station_title">පොළෙහි පිහිටීම් පසුතල</string>
|
<string name="prefs_station_title">පොළ පිහිටීම</string>
|
||||||
<string name="prefs_station_lat_text">ඔබගේ පොළෙහි අක්ෂාංශය සකසන්න</string>
|
<string name="prefs_station_lat_text">පොළ අක්ෂාංශය</string>
|
||||||
<string name="prefs_station_lon_text">ඔබගේ පොළෙහි දේශාංශය සකසන්න</string>
|
<string name="prefs_station_lon_text">පොළ දේශාංශය</string>
|
||||||
<string name="prefs_locator_title">QTH locator පසුතල</string>
|
<string name="prefs_locator_title">QTH locator</string>
|
||||||
<string name="prefs_locator_text"> ඔබගේ locator භාවිතා කර පොළහි පිහිටීම සකසන්න</string>
|
<string name="prefs_locator_text">locator මඟින් පොළ පිහිටීම සකසන්න</string>
|
||||||
|
|
||||||
<string name="prefs_data_title">චන්ද්රිකා දත්ත</string>
|
<string name="prefs_data_title">චන්ද්රිකා දත්ත</string>
|
||||||
<string name="prefs_data_entries">චන්ද්රිකා: %s</string>
|
<string name="prefs_data_entries">චන්ද්රිකා: %s</string>
|
||||||
@@ -110,39 +114,52 @@
|
|||||||
<string name="prefs_data_clear">පිරිසිදු ක°</string>
|
<string name="prefs_data_clear">පිරිසිදු ක°</string>
|
||||||
<string name="prefs_data_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string>
|
<string name="prefs_data_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string>
|
||||||
<string name="prefs_data_update_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string>
|
<string name="prefs_data_update_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string>
|
||||||
|
<string name="prefs_data_import_satellites_error">චන්ද්රිකා ආයාත නොවීය. වලංගු TLE/3LE (.txt) හෝ OMM (.csv) ගොනුවක් තෝරන්න.</string>
|
||||||
|
<string name="prefs_data_import_transceivers_error">සම්ප්රේෂක ආයාත නොවීය. වලංගු SatNOGS (.json) ගොනුවක් තෝරන්න.</string>
|
||||||
|
|
||||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
<string name="prefs_data_sources_title">අභිරුචි මූලාශ්ර</string>
|
||||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
<string name="prefs_data_sources_tle_switch">TLE URL</string>
|
||||||
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
|
<string name="prefs_data_sources_transceivers_switch">Transceivers URL</string>
|
||||||
|
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
||||||
|
|
||||||
<string name="prefs_data_output_title">Data output</string>
|
<string name="prefs_data_output_title">දත්ත ප්රතිදානය</string>
|
||||||
<string name="prefs_net_output">Network</string>
|
<string name="prefs_net_output">ජාලය</string>
|
||||||
<string name="prefs_bt_output">Bluetooth</string>
|
<string name="prefs_bt_output">Bluetooth</string>
|
||||||
|
<string name="prefs_cat_output">CAT</string>
|
||||||
|
|
||||||
|
<string name="nav_radiocontrol">රේඩියෝ පාලනය</string>
|
||||||
|
<string name="rc_settings_title">CAT පාලනය</string>
|
||||||
|
<string name="rc_radio_model">රේඩියෝ මාදිලිය</string>
|
||||||
|
<string name="rc_tx_device_hint">TX BT ලිපිනය</string>
|
||||||
|
<string name="rc_rx_device_hint">RX BT ලිපිනය</string>
|
||||||
|
<string name="rc_tx_name_hint">TX රේඩියෝ නම</string>
|
||||||
|
<string name="rc_rx_name_hint">RX රේඩියෝ නම</string>
|
||||||
|
<string name="rc_enable_switch">CAT සබල කරන්න</string>
|
||||||
|
|
||||||
<string name="prefs_net_title">ජාල ප්රතිදානය</string>
|
<string name="prefs_net_title">ජාල ප්රතිදානය</string>
|
||||||
<string name="prefs_net_rotator_switch">Enable rotation output</string>
|
<string name="prefs_net_rotator_switch">Rotation ප්රතිදානය සබල කරන්න</string>
|
||||||
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
||||||
<string name="prefs_net_rotator_format_hint">දත්ත අකාරය</string>
|
<string name="prefs_net_rotator_format_hint">දත්ත ආකෘතිය</string>
|
||||||
<string name="prefs_net_frequency_switch">Enable frequency output</string>
|
<string name="prefs_net_frequency_switch">Frequency ප්රතිදානය සබල කරන්න</string>
|
||||||
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
||||||
<string name="prefs_net_frequency_format_hint">දත්ත අකාරය</string>
|
<string name="prefs_net_frequency_format_hint">දත්ත ආකෘතිය</string>
|
||||||
|
|
||||||
<string name="prefs_bt_title">Bluetooth දත්ත ප්රතිදානය</string>
|
<string name="prefs_bt_title">Bluetooth ප්රතිදානය</string>
|
||||||
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
|
<string name="prefs_bt_rotator_switch">Rotation ප්රතිදානය සබල කරන්න</string>
|
||||||
<string name="prefs_bt_rotator_device_hint">උපාංග id</string>
|
<string name="prefs_bt_rotator_device_hint">උපාංග ID</string>
|
||||||
<string name="prefs_bt_rotator_output_hint">දත්ත අකාරය</string>
|
<string name="prefs_bt_rotator_output_hint">දත්ත ආකෘතිය</string>
|
||||||
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
|
<string name="prefs_bt_frequency_switch">Frequency ප්රතිදානය සබල කරන්න</string>
|
||||||
<string name="prefs_bt_frequency_device_hint">උපාංග id</string>
|
<string name="prefs_bt_frequency_device_hint">උපාංග ID</string>
|
||||||
<string name="prefs_bt_frequency_output_hint">දත්ත අකාරය</string>
|
<string name="prefs_bt_frequency_output_hint">දත්ත ආකෘතිය</string>
|
||||||
<string name="prefs_bt_perm_error">Bluetooth අවසර පරික්ෂා ක°</string>
|
<string name="prefs_bt_perm_error">Bluetooth අවසර පරික්ෂා ක°</string>
|
||||||
<string name="prefs_net_perm_error">Network අවසර පරික්ෂා කරන්න</string>
|
<string name="prefs_net_perm_error">Network අවසර පරික්ෂා කරන්න</string>
|
||||||
|
|
||||||
<string name="prefs_other_title">වෙනත් සැකසුම්</string>
|
<string name="prefs_other_title">වෙනත්</string>
|
||||||
<string name="prefs_other_switch_utc">පසුකර වේලාවන් UTC මගින්</string>
|
<string name="prefs_other_switch_utc">UTC වේලාව</string>
|
||||||
<string name="prefs_other_switch_update">ස්වයං දත්ත යාවත්කාලීනය</string>
|
<string name="prefs_other_switch_update">දත්ත ස්වයං යාවත්කාලීන</string>
|
||||||
<string name="prefs_other_switch_sweep">radar sweep සජීවිකරණය සබල කරන්න</string>
|
<string name="prefs_other_switch_sweep">Radar sweep</string>
|
||||||
<string name="prefs_other_switch_sensors">Radar දර්ශනය කරකැවීමට සංවේදක භාවිතා කරන්න </string>
|
<string name="prefs_other_switch_sensors">සංවේදක</string>
|
||||||
<string name="prefs_other_switch_night_mode">රතු රාත්රී පෙරහන සබල කරන්න</string>
|
<string name="prefs_other_switch_night_mode">රාත්රී පෙරහන</string>
|
||||||
|
|
||||||
<string name="prefs_highlight_title">උස් උද්දීපනය</string>
|
<string name="prefs_highlight_title">උස් උද්දීපනය</string>
|
||||||
<string name="prefs_highlight_low">අඩු < %1$d°</string>
|
<string name="prefs_highlight_low">අඩු < %1$d°</string>
|
||||||
|
|||||||
@@ -10,67 +10,27 @@
|
|||||||
<string name="nav_sat">Uydular</string>
|
<string name="nav_sat">Uydular</string>
|
||||||
<string name="nav_pass">Geçişler</string>
|
<string name="nav_pass">Geçişler</string>
|
||||||
<string name="nav_radar">Radar</string>
|
<string name="nav_radar">Radar</string>
|
||||||
<string name="nav_cw">CW Dekoder</string>
|
|
||||||
<string name="nav_map">Harita</string>
|
<string name="nav_map">Harita</string>
|
||||||
<string name="nav_prefs">Ayarlar</string>
|
<string name="nav_prefs">Ayarlar</string>
|
||||||
<string name="prefs_aprs_title">APRS Raporlama</string>
|
|
||||||
<string name="prefs_aprs_enable">APRS raporlamayı etkinleştir</string>
|
|
||||||
<string name="prefs_aprs_summary">Sunucu: %1$s:%2$d Çağrı: %3$s</string>
|
|
||||||
<string name="prefs_aprs_report_now">Şimdi Raporla</string>
|
|
||||||
<string name="prefs_aprs_settings_title">APRS Ayarları</string>
|
|
||||||
<string name="prefs_aprs_server">Sunucu</string>
|
|
||||||
<string name="prefs_aprs_port">Port</string>
|
|
||||||
<string name="prefs_aprs_callsign">Çağrı işareti</string>
|
|
||||||
<string name="prefs_aprs_ssid">SSID</string>
|
|
||||||
<string name="prefs_aprs_passcode">Parola</string>
|
|
||||||
<string name="prefs_aprs_passcode_hint">Parola boş = çağrıdan otomatik hesaplanır</string>
|
|
||||||
<string name="prefs_aprs_interval">Aralık (dakika)</string>
|
|
||||||
<string name="prefs_aprs_status">Durum metni</string>
|
|
||||||
<string name="prefs_aprs_symbol_table">Sembol tablosu</string>
|
|
||||||
<string name="prefs_aprs_symbol_code">Sembol kodu</string>
|
|
||||||
<string name="prefs_aprs_save">Kaydet</string>
|
|
||||||
<string name="prefs_aprs_cancel">İptal</string>
|
|
||||||
<string name="aprs_notif_channel">APRS hizmeti</string>
|
|
||||||
<string name="aprs_notif_title">APRS: %1$s</string>
|
|
||||||
<string name="aprs_notif_running">Raporlama çalışıyor</string>
|
|
||||||
<string name="aprs_notif_connected">APRS-IS bağlı</string>
|
|
||||||
<string name="aprs_notif_error">Bağlantı hatası</string>
|
|
||||||
<string name="aprs_notif_stop">Durdur</string>
|
|
||||||
<string name="prefs_aprs_calc_passcode">Parolayı hesapla</string>
|
|
||||||
<string name="aprs_toast_ok">APRS: rapor gönderildi</string>
|
|
||||||
<string name="aprs_toast_fail">APRS: rapor başarısız - %1$s</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS yapılandırılmadı - önce çağrı girin</string>
|
|
||||||
<string name="prefs_aprs_last_report">Son rapor: %1$s %2$s</string>
|
|
||||||
<string name="prefs_aprs_last_ok">OK</string>
|
|
||||||
<string name="prefs_aprs_last_fail">başarısız - %1$s</string>
|
|
||||||
<string name="nav_more">Daha Fazla</string>
|
|
||||||
<string name="nav_log">Günlük</string>
|
|
||||||
<string name="nav_amsat">AMSAT</string>
|
|
||||||
<string name="amsat_title">AMSAT Uydu Durumu</string>
|
|
||||||
<string name="amsat_updated">Güncellendi:</string>
|
|
||||||
<string name="amsat_active">Aktif</string>
|
|
||||||
<string name="amsat_tlm">TLM/işaret</string>
|
|
||||||
<string name="amsat_not_heard">Duyulmadı</string>
|
|
||||||
<string name="amsat_conflict">Çelişkili</string>
|
|
||||||
<string name="amsat_name">Name</string>
|
|
||||||
<string name="amsat_load_failed">Yükleme başarısız - ağı kontrol edip tekrar deneyin</string>
|
|
||||||
<string name="amsat_no_reports">Bu dönem için rapor yok</string>
|
|
||||||
<string name="amsat_close">Kapat</string>
|
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">Uydu türü: %s</string>
|
<string name="sat_type_hint">Modlar: %s</string>
|
||||||
<string name="sat_type_title">Uydu türü seçin</string>
|
<string name="sat_type_title">Modları seçin</string>
|
||||||
<string name="sat_search_hint">Id - Ad</string>
|
<string name="sat_search_hint">Id - Ad</string>
|
||||||
<string name="sat_search_clear">Temizle</string>
|
<string name="sat_search_clear">Temizle</string>
|
||||||
<string name="sat_clear_all">Tümünü temizle</string>
|
<string name="sat_clear_all">Tümünü temizle</string>
|
||||||
<string name="sat_select_all">Tümünü seç</string>
|
<string name="sat_select_all">Tümünü seç</string>
|
||||||
|
<string name="sat_group_selected">Seçilenler</string>
|
||||||
|
<string name="sat_group_available">Kullanılabilir</string>
|
||||||
|
<string name="sat_group_selected_count">Seçilenler (%1$d)</string>
|
||||||
|
<string name="sat_group_available_count">Kullanılabilir (%1$d)</string>
|
||||||
<string name="sat_empty_list_message">"Arama sorgunuzun doğru olduğundan ve veritabanının güncellendiğinden emin olun"</string>
|
<string name="sat_empty_list_message">"Arama sorgunuzun doğru olduğundan ve veritabanının güncellendiğinden emin olun"</string>
|
||||||
<string name="sat_warning_title">Uyarı\!</string>
|
<string name="sat_warning_title">Uyarı\!</string>
|
||||||
<string name="sat_warning_message">
|
<string name="sat_warning_message">
|
||||||
Bu uygulamada 9000\'den fazla uydu listelenmiştir.
|
Bu uygulamada 9000\'den fazla uydu listelenmiştir.
|
||||||
Hepsini aynı anda takip etmek anlamsızdır.
|
Hepsini aynı anda takip etmek anlamsızdır.
|
||||||
\n\nListeyi yalnızca ilgilendiğiniz uydularla
|
\n\nListeyi yalnızca ilgilendiğiniz uydularla
|
||||||
daraltmak için arama ve tür seçiciyi kullanın.</string>
|
daraltmak için arama ve mod seçiciyi kullanın.</string>
|
||||||
|
|
||||||
<!-- Passes screen -->
|
<!-- Passes screen -->
|
||||||
<string name="pass_filter_title">Geçişleri filtrele</string>
|
<string name="pass_filter_title">Geçişleri filtrele</string>
|
||||||
@@ -80,7 +40,7 @@
|
|||||||
<string name="pass_filter_invert_time">AOS aralığını tersine çevir</string>
|
<string name="pass_filter_invert_time">AOS aralığını tersine çevir</string>
|
||||||
<string name="pass_filter_deep_space">DeepSpace (periyot >225dk)</string>
|
<string name="pass_filter_deep_space">DeepSpace (periyot >225dk)</string>
|
||||||
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
||||||
<string name="pass_modes_title">Modulasyon türü seçin</string>
|
<string name="pass_modes_title">Modları seçin</string>
|
||||||
<string name="pass_satId" translatable="false">%05d</string>
|
<string name="pass_satId" translatable="false">%05d</string>
|
||||||
<string name="pass_elevation">Yükseklik açısı: %.1f°</string>
|
<string name="pass_elevation">Yükseklik açısı: %.1f°</string>
|
||||||
<string name="pass_deep_space" translatable="false">DeepSpace</string>
|
<string name="pass_deep_space" translatable="false">DeepSpace</string>
|
||||||
@@ -94,14 +54,13 @@
|
|||||||
Ayarlar\'dan GPS, Enlem/Boylam veya QTH kullanarak konumunuzu belirlediğinizden emin olun.
|
Ayarlar\'dan GPS, Enlem/Boylam veya QTH kullanarak konumunuzu belirlediğinizden emin olun.
|
||||||
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.</string>
|
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.</string>
|
||||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||||
<string name="pass_whatsnew_message" translatable="false">
|
<string name="pass_whatsnew_message" translatable="false">
|
||||||
* YENİ: AMSAT uydu durum sayfası - canlı durum tablosu (resmi renkler), rapor sayıları, uydu/gün bazında detaylı raporlar
|
* Switched to short-form commands in NetworkReporter, by theojalba
|
||||||
* Günlükler uydu geçişine göre gruplanır (oturum = uydu + AOS zamanı), oturumlar arasında ayırıcı çizgi
|
\n* Added CAT radio control for full-duplex SAT operation, by jcmerg
|
||||||
* GPS artık yalnızca gerçek konum alındığında başarı gösterir (15 sn zaman aşımı, izin kontrolü)
|
\n* Added normalized time for passes AOS/LOS, fixed refresh state bug
|
||||||
* Uydu veritabanı indirilemediğinde net hata mesajı gösterilir
|
\n* Added multiple performance tweaks, improved passes calculation
|
||||||
* Günlük frekansları Doppler panelini gerçek zamanlı takip eder; kaydırarak silme görselleri düzeltildi
|
\n* Fixed UTC pass time problem mentioned in issue #201
|
||||||
* Endonezya dili düzeltmesi (values-id)
|
</string>
|
||||||
* YENİ: APRS raporlama (APRS-IS ağı, bildirimli ön plan hizmeti, ayarlanabilir aralık)</string>
|
|
||||||
|
|
||||||
<!-- Radar screen -->
|
<!-- Radar screen -->
|
||||||
<string name="radar_back">Geri</string>
|
<string name="radar_back">Geri</string>
|
||||||
@@ -148,7 +107,7 @@
|
|||||||
<string name="map_visible">Görünür</string>
|
<string name="map_visible">Görünür</string>
|
||||||
|
|
||||||
<!-- Settings screen -->
|
<!-- Settings screen -->
|
||||||
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
|
<string name="prefs_app_title" translatable="false">Look4Sat v%s</string>
|
||||||
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
||||||
<string name="prefs_donate_title">Bağış yap</string>
|
<string name="prefs_donate_title">Bağış yap</string>
|
||||||
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
|
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
|
||||||
@@ -166,50 +125,50 @@
|
|||||||
|
|
||||||
<string name="prefs_loc_title">İstasyon konumu</string>
|
<string name="prefs_loc_title">İstasyon konumu</string>
|
||||||
<string name="prefs_loc_gps_title">GPS</string>
|
<string name="prefs_loc_gps_title">GPS</string>
|
||||||
<string name="prefs_loc_gps_error">Konum alınamıyor - izinleri ve GPS/ağ sinyalini kontrol edin</string>
|
<string name="prefs_loc_gps_error">Konum izinlerinizi kontrol edin</string>
|
||||||
<string name="prefs_loc_input_title">Manuel giriş</string>
|
<string name="prefs_loc_input_title">Manuel giriş</string>
|
||||||
<string name="prefs_loc_input_error">Geçersiz konum girildi</string>
|
<string name="prefs_loc_input_error">Geçersiz konum girildi</string>
|
||||||
<string name="prefs_loc_qth_title">QTH</string>
|
<string name="prefs_loc_qth_title">QTH</string>
|
||||||
<string name="prefs_loc_qth_error">Geçersiz QTH konumlandırıcısı</string>
|
<string name="prefs_loc_qth_error">Geçersiz QTH konumlandırıcısı</string>
|
||||||
<string name="prefs_loc_success">Konum başarıyla güncellendi</string>
|
<string name="prefs_loc_success">Konum başarıyla güncellendi</string>
|
||||||
<string name="prefs_station_title">İstasyon konumu ayarları</string>
|
<string name="prefs_station_title">İstasyon konumu</string>
|
||||||
<string name="prefs_station_lat_text">Yer istasyonunuzun enlemini girin</string>
|
<string name="prefs_station_lat_text">İstasyon enlemi</string>
|
||||||
<string name="prefs_station_lon_text">Yer istasyonunuzun boylamını girin</string>
|
<string name="prefs_station_lon_text">İstasyon boylamı</string>
|
||||||
<string name="prefs_locator_title">QTH konumlandırıcı ayarları</string>
|
<string name="prefs_locator_title">QTH konumlandırıcı</string>
|
||||||
<string name="prefs_locator_text">İstasyon konumunu konumlandırıcı ile ayarlayın</string>
|
<string name="prefs_locator_text">Konumu lokatörle ayarla</string>
|
||||||
|
|
||||||
<string name="prefs_data_title">Uydu verisi</string>
|
<string name="prefs_data_title">Uydu verisi</string>
|
||||||
<string name="prefs_data_entries">Uydular: %s</string>
|
<string name="prefs_data_entries">Uydular: %s</string>
|
||||||
<string name="prefs_data_radios">Transceiver\'lar: %s</string>
|
<string name="prefs_data_radios">Transceiver\'lar: %s</string>
|
||||||
<string name="prefs_data_update">Güncelle</string>
|
<string name="prefs_data_update">Güncelle</string>
|
||||||
<string name="prefs_data_import">Özel URL</string>
|
<string name="prefs_data_import">İçe aktar</string>
|
||||||
<string name="prefs_data_clear">Temizle</string>
|
<string name="prefs_data_clear">Temizle</string>
|
||||||
<string name="prefs_data_clear_success">Veriler başarıyla temizlendi</string>
|
<string name="prefs_data_clear_success">Veriler başarıyla temizlendi</string>
|
||||||
<string name="prefs_data_update_success">Güncelleme başarıyla tamamlandı</string>
|
<string name="prefs_data_update_success">Güncelleme başarıyla tamamlandı</string>
|
||||||
<string name="prefs_data_update_failed">Güncelleme başarısız - ağınızı kontrol edin</string>
|
<string name="prefs_data_import_satellites_error">Uydu içe aktarılmadı. Geçerli bir TLE/3LE (.txt) veya OMM (.csv) dosyası seçin.</string>
|
||||||
|
<string name="prefs_data_import_transceivers_error">Transceiver içe aktarılmadı. Geçerli bir SatNOGS (.json) dosyası seçin.</string>
|
||||||
|
|
||||||
<string name="prefs_data_sources_title">Özel veri kaynakları</string>
|
<string name="prefs_data_sources_title">Özel kaynaklar</string>
|
||||||
<string name="prefs_data_sources_tle_switch">Özel TLE URL</string>
|
<string name="prefs_data_sources_tle_switch">TLE URL</string>
|
||||||
<string name="prefs_data_sources_transceivers_switch">Özel transceiver URL</string>
|
<string name="prefs_data_sources_transceivers_switch">Transceiver URL</string>
|
||||||
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
||||||
<string name="prefs_data_reset_url">Varsayılan URL\'ye sıfırla</string>
|
|
||||||
|
|
||||||
<string name="prefs_data_output_title">Veri aktarımı</string>
|
<string name="prefs_data_output_title">Veri çıkışı</string>
|
||||||
<string name="prefs_net_output">Ağ</string>
|
<string name="prefs_net_output">Ağ</string>
|
||||||
<string name="prefs_bt_output">Bluetooth</string>
|
<string name="prefs_bt_output">Bluetooth</string>
|
||||||
<string name="prefs_cat_output">CAT</string>
|
<string name="prefs_cat_output">CAT</string>
|
||||||
|
|
||||||
<!-- Radio Control -->
|
<!-- Radio Control -->
|
||||||
<string name="nav_radiocontrol">Radyo Kontrolü</string>
|
<string name="nav_radiocontrol">Radyo Kontrolü</string>
|
||||||
<string name="rc_settings_title">CAT Radyo Kontrolü</string>
|
<string name="rc_settings_title">CAT kontrolü</string>
|
||||||
<string name="rc_radio_model">Radyo Modeli</string>
|
<string name="rc_radio_model">Radyo Modeli</string>
|
||||||
<string name="rc_tx_device_hint">TX Radyo BT Adresi</string>
|
<string name="rc_tx_device_hint">TX BT adresi</string>
|
||||||
<string name="rc_rx_device_hint">RX Radyo BT Adresi</string>
|
<string name="rc_rx_device_hint">RX BT adresi</string>
|
||||||
<string name="rc_tx_name_hint">TX Radyo Adı</string>
|
<string name="rc_tx_name_hint">TX Radyo Adı</string>
|
||||||
<string name="rc_rx_name_hint">RX Radyo Adı</string>
|
<string name="rc_rx_name_hint">RX Radyo Adı</string>
|
||||||
<string name="rc_enable_switch">CAT Kontrolünü Etkinleştir</string>
|
<string name="rc_enable_switch">CAT\'i etkinleştir</string>
|
||||||
|
|
||||||
<string name="prefs_net_title">Ağ veri çıkışı</string>
|
<string name="prefs_net_title">Ağ çıkışı</string>
|
||||||
<string name="prefs_net_rotator_switch">Döndürme çıkışını etkinleştir</string>
|
<string name="prefs_net_rotator_switch">Döndürme çıkışını etkinleştir</string>
|
||||||
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
||||||
<string name="prefs_net_rotator_format_hint">Biçim</string>
|
<string name="prefs_net_rotator_format_hint">Biçim</string>
|
||||||
@@ -217,79 +176,36 @@
|
|||||||
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
||||||
<string name="prefs_net_frequency_format_hint">Biçim</string>
|
<string name="prefs_net_frequency_format_hint">Biçim</string>
|
||||||
|
|
||||||
<string name="prefs_bt_title">Bluetooth veri çıkışı</string>
|
<string name="prefs_bt_title">Bluetooth çıkışı</string>
|
||||||
<string name="prefs_bt_rotator_switch">Döndürme çıkışını etkinleştir</string>
|
<string name="prefs_bt_rotator_switch">Döndürme çıkışını etkinleştir</string>
|
||||||
<string name="prefs_bt_rotator_device_hint">Cihaz kimliği</string>
|
<string name="prefs_bt_rotator_device_hint">Cihaz kimliği</string>
|
||||||
<string name="prefs_bt_rotator_output_hint">Veri biçimi</string>
|
<string name="prefs_bt_rotator_output_hint">Biçim</string>
|
||||||
<string name="prefs_bt_frequency_switch">Frekans çıkışını etkinleştir</string>
|
<string name="prefs_bt_frequency_switch">Frekans çıkışını etkinleştir</string>
|
||||||
<string name="prefs_bt_frequency_device_hint">Cihaz kimliği</string>
|
<string name="prefs_bt_frequency_device_hint">Cihaz kimliği</string>
|
||||||
<string name="prefs_bt_frequency_output_hint">Veri biçimi</string>
|
<string name="prefs_bt_frequency_output_hint">Biçim</string>
|
||||||
<string name="prefs_bt_perm_error">Bluetooth izninizi kontrol edin</string>
|
<string name="prefs_bt_perm_error">Bluetooth izninizi kontrol edin</string>
|
||||||
<string name="prefs_net_perm_error">Ağ izninizi kontrol edin</string>
|
<string name="prefs_net_perm_error">Ağ izninizi kontrol edin</string>
|
||||||
|
|
||||||
<string name="tracking_status">İzleniyor: %s</string>
|
<string name="prefs_other_title">Diğer</string>
|
||||||
<string name="loc_lat_text">Enlem: %s°</string>
|
<string name="prefs_other_switch_utc">UTC saati</string>
|
||||||
<string name="loc_lon_text">Boylam: %s°</string>
|
<string name="prefs_other_switch_update">Veriyi otomatik güncelle</string>
|
||||||
<string name="loc_qth_text">QTH: %s</string>
|
<string name="prefs_other_switch_sweep">Radar taraması</string>
|
||||||
<string name="connect_radio">Bağlan</string>
|
<string name="prefs_other_switch_sensors">Sensör</string>
|
||||||
<string name="disconnect_radio">Bağlantıyı Kes</string>
|
<string name="prefs_other_switch_night_mode">Gece filtresi</string>
|
||||||
<string name="track_radio">Takip</string>
|
|
||||||
<string name="stop_radio">Dur</string>
|
|
||||||
<string name="prefs_wavelog_title">WaveLog günlük sunucusu</string>
|
|
||||||
<string name="prefs_wavelog_url_hint">Sunucu adresi</string>
|
|
||||||
<string name="prefs_wavelog_key_hint">API anahtarı</string>
|
|
||||||
<string name="prefs_wavelog_station_hint">İstasyon kimliği</string>
|
|
||||||
<string name="prefs_wavelog_auto">Otomatik yükleme</string>
|
|
||||||
<string name="prefs_wavelog_test">Bağlantıyı test et</string>
|
|
||||||
<string name="prefs_wavelog_upload">Şimdi yükle</string>
|
|
||||||
<string name="wavelog_title">Günlük</string>
|
|
||||||
<string name="wavelog_select_transponder">Transponder seç</string>
|
|
||||||
<string name="wavelog_call_hint">Çağrı işareti</string>
|
|
||||||
<string name="wavelog_mode_hint">Mod</string>
|
|
||||||
<string name="wavelog_saved">Yerel günlüğe kaydedildi</string>
|
|
||||||
<string name="wavelog_need_config">Önce Ayarlar\'da WaveLog\'u yapılandırın</string>
|
|
||||||
<string name="wavelog_empty">Henüz günlük kaydı yok</string>
|
|
||||||
<string name="wavelog_grid_mismatch">QTH ızgarası (%1$s) istasyon ızgarasıyla (%2$s) eşleşmiyor. Yine de yüklensin mi?</string>
|
|
||||||
<string name="wavelog_ignore">Yoksay ve yükle</string>
|
|
||||||
<string name="wavelog_cancel">İptal</string>
|
|
||||||
<string name="wavelog_copy">Kopyala</string>
|
|
||||||
<string name="wavelog_delete_undo">Geri al</string>
|
|
||||||
<string name="wavelog_col_time">Zaman</string>
|
|
||||||
<string name="wavelog_col_freq">Frekans</string>
|
|
||||||
<string name="wavelog_col_sat">Uydu</string>
|
|
||||||
<string name="wavelog_col_call">Çağrı</string>
|
|
||||||
<string name="wavelog_col_uploaded">Gönderildi</string>
|
|
||||||
<string name="wavelog_ungrouped">Gruplanmamış</string>
|
|
||||||
<string name="prefs_other_title">Diğer ayarlar</string>
|
|
||||||
<string name="prefs_ui_title">UI Ayarları</string>
|
|
||||||
<string name="prefs_ui_order_title">Sayfa sırası</string>
|
|
||||||
<string name="prefs_ui_main_title">Ana menü (alt çubuk)</string>
|
|
||||||
<string name="prefs_ui_sub_title">Daha fazla menü</string>
|
|
||||||
<string name="prefs_ui_move_out">Daha fazla menüye taşı</string>
|
|
||||||
<string name="prefs_ui_move_back">Ana menüye geri taşı</string>
|
|
||||||
<string name="prefs_ui_order_locked">Sabit (taşınamaz)</string>
|
|
||||||
<string name="prefs_ui_order_reset">Sıralamayı sıfırla</string>
|
|
||||||
<string name="prefs_other_switch_utc">Geçiş saatini UTC olarak göster</string>
|
|
||||||
<string name="prefs_other_switch_update">Otomatik veri güncellemeyi etkinleştir</string>
|
|
||||||
<string name="prefs_other_switch_sweep">Radar taramasını etkinleştir</string>
|
|
||||||
<string name="prefs_other_switch_sensors">Radar görünümünü döndürmek için sensörleri kullan</string>
|
|
||||||
<string name="prefs_other_switch_night_mode">Kırmızı gece filtresini etkinleştir</string>
|
|
||||||
|
|
||||||
<string name="prefs_highlight_title">Elevasyon vurgusu</string>
|
<string name="prefs_highlight_title">Elevasyon vurgusu</string>
|
||||||
<string name="prefs_highlight_low">Düşük < %1$d°</string>
|
<string name="prefs_highlight_low">Düşük < %1$d°</string>
|
||||||
<string name="prefs_highlight_mid">Orta %1$d-%2$d°</string>
|
<string name="prefs_highlight_mid">Orta %1$d-%2$d°</string>
|
||||||
<string name="prefs_highlight_high">Yüksek > %1$d°</string>
|
<string name="prefs_highlight_high">Yüksek > %1$d°</string>
|
||||||
|
|
||||||
<string name="prefs_outro_title">BG7NTA ve orijinal yazar teşekkür eder</string>
|
<string name="prefs_outro_title">Teşekkürler</string>
|
||||||
<string name="prefs_outro_thanks" translatable="false">
|
<string name="prefs_outro_thanks" translatable="false">
|
||||||
• Look4Sat users and contributors!
|
• Look4Sat users and contributors!
|
||||||
\n• David A. B. Johnson (predict4java)
|
\n• David A. B. Johnson (predict4java)
|
||||||
\n• Dave Moten (predict4java)
|
\n• Dave Moten (predict4java)
|
||||||
\n• Alexandru Csete (Gpredict)
|
\n• Alexandru Csete (Gpredict)
|
||||||
\n• Dr T.S. Kelso (Celestrak)
|
\n• Dr T.S. Kelso (Celestrak)
|
||||||
\n• Libre Space Foundation (SatNOGS)
|
\n• Libre Space Foundation (SatNOGS)</string>
|
||||||
\n• BA7OPF (pass matching feature)
|
|
||||||
\n• BG7NTA</string>
|
|
||||||
<string name="prefs_outro_license">Bu uygulama herhangi bir garanti sunmaz</string>
|
<string name="prefs_outro_license">Bu uygulama herhangi bir garanti sunmaz</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -13,12 +13,16 @@
|
|||||||
<string name="nav_prefs">Налаштування</string>
|
<string name="nav_prefs">Налаштування</string>
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">Тип: %s</string>
|
<string name="sat_type_hint">Режими: %s</string>
|
||||||
<string name="sat_type_title">Виберіть тип супутника</string>
|
<string name="sat_type_title">Виберіть режими</string>
|
||||||
<string name="sat_search_hint">Id - Назва</string>
|
<string name="sat_search_hint">Id - Назва</string>
|
||||||
<string name="sat_search_clear">Очистити</string>
|
<string name="sat_search_clear">Очистити</string>
|
||||||
<string name="sat_clear_all">Очистити всі</string>
|
<string name="sat_clear_all">Очистити всі</string>
|
||||||
<string name="sat_select_all">Вибрати всі</string>
|
<string name="sat_select_all">Вибрати всі</string>
|
||||||
|
<string name="sat_group_selected">Обрані</string>
|
||||||
|
<string name="sat_group_available">Доступні</string>
|
||||||
|
<string name="sat_group_selected_count">Обрані (%1$d)</string>
|
||||||
|
<string name="sat_group_available_count">Доступні (%1$d)</string>
|
||||||
<string name="sat_empty_list_message">
|
<string name="sat_empty_list_message">
|
||||||
Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string>
|
Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string>
|
||||||
<string name="sat_warning_title">Увага\!</string>
|
<string name="sat_warning_title">Увага\!</string>
|
||||||
@@ -26,7 +30,7 @@
|
|||||||
У цьому додатку перелічено понад 9000 супутників.
|
У цьому додатку перелічено понад 9000 супутників.
|
||||||
Немає сенсу відстежувати їх усі одночасно.
|
Немає сенсу відстежувати їх усі одночасно.
|
||||||
\n\nЗавжди намагайтеся звузити список лише до тих,
|
\n\nЗавжди намагайтеся звузити список лише до тих,
|
||||||
які вас цікавлять, за допомогою пошуку та селектора типів.</string>
|
які вас цікавлять, за допомогою пошуку та вибору режимів.</string>
|
||||||
|
|
||||||
<!-- Passes screen -->
|
<!-- Passes screen -->
|
||||||
<string name="pass_filter_title">Фільтр прольотів</string>
|
<string name="pass_filter_title">Фільтр прольотів</string>
|
||||||
@@ -35,7 +39,7 @@
|
|||||||
<string name="pass_filter_aos_time">Вікно AOS</string>
|
<string name="pass_filter_aos_time">Вікно AOS</string>
|
||||||
<string name="pass_filter_invert_time">Інвертувати вікно AOS</string>
|
<string name="pass_filter_invert_time">Інвертувати вікно AOS</string>
|
||||||
<string name="pass_filter_deep_space">DeepSpace (період >225хв)</string>
|
<string name="pass_filter_deep_space">DeepSpace (період >225хв)</string>
|
||||||
<string name="pass_modes_title">Виберіть тип модуляції</string>
|
<string name="pass_modes_title">Виберіть режими</string>
|
||||||
<string name="pass_elevation">Піднес.: %.1f°</string>
|
<string name="pass_elevation">Піднес.: %.1f°</string>
|
||||||
<string name="pass_altitude">Висота: %.0f км</string>
|
<string name="pass_altitude">Висота: %.0f км</string>
|
||||||
<string name="pass_empty_list_message">
|
<string name="pass_empty_list_message">
|
||||||
@@ -96,11 +100,11 @@
|
|||||||
<string name="prefs_loc_qth_title">QTH</string>
|
<string name="prefs_loc_qth_title">QTH</string>
|
||||||
<string name="prefs_loc_qth_error">Некоректний локатор QTH</string>
|
<string name="prefs_loc_qth_error">Некоректний локатор QTH</string>
|
||||||
<string name="prefs_loc_success">Позицію успішно оновлено</string>
|
<string name="prefs_loc_success">Позицію успішно оновлено</string>
|
||||||
<string name="prefs_station_title">Налаштування місцезнаходження станції</string>
|
<string name="prefs_station_title">Позиція станції</string>
|
||||||
<string name="prefs_station_lat_text">Широта місцезнаходження станції</string>
|
<string name="prefs_station_lat_text">Широта станції</string>
|
||||||
<string name="prefs_station_lon_text">Довгота місцезнаходження станції</string>
|
<string name="prefs_station_lon_text">Довгота станції</string>
|
||||||
<string name="prefs_locator_title">Налаштування QTH локатора</string>
|
<string name="prefs_locator_title">Локатор QTH</string>
|
||||||
<string name="prefs_locator_text">Задати позицію за QTH локатором</string>
|
<string name="prefs_locator_text">Позиція за локатором</string>
|
||||||
|
|
||||||
<string name="prefs_data_title">Супутникові дані</string>
|
<string name="prefs_data_title">Супутникові дані</string>
|
||||||
<string name="prefs_data_entries">Супутників: %s</string>
|
<string name="prefs_data_entries">Супутників: %s</string>
|
||||||
@@ -110,39 +114,52 @@
|
|||||||
<string name="prefs_data_clear">Очистити</string>
|
<string name="prefs_data_clear">Очистити</string>
|
||||||
<string name="prefs_data_clear_success">Дані успішно очищено</string>
|
<string name="prefs_data_clear_success">Дані успішно очищено</string>
|
||||||
<string name="prefs_data_update_success">Оновлення успішне</string>
|
<string name="prefs_data_update_success">Оновлення успішне</string>
|
||||||
|
<string name="prefs_data_import_satellites_error">Супутники не імпортовано. Потрібен TLE/3LE (.txt) або OMM (.csv).</string>
|
||||||
|
<string name="prefs_data_import_transceivers_error">Трансивери не імпортовано. Потрібен SatNOGS (.json).</string>
|
||||||
|
|
||||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
<string name="prefs_data_sources_title">Власні джерела</string>
|
||||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
<string name="prefs_data_sources_tle_switch">URL TLE</string>
|
||||||
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
|
<string name="prefs_data_sources_transceivers_switch">URL трансиверів</string>
|
||||||
|
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
||||||
|
|
||||||
<string name="prefs_data_output_title">Data output</string>
|
<string name="prefs_data_output_title">Вивід даних</string>
|
||||||
<string name="prefs_net_output">Network</string>
|
<string name="prefs_net_output">Мережа</string>
|
||||||
<string name="prefs_bt_output">Bluetooth</string>
|
<string name="prefs_bt_output">Bluetooth</string>
|
||||||
|
<string name="prefs_cat_output">CAT</string>
|
||||||
|
|
||||||
<string name="prefs_net_title">Вивід мережевих даних</string>
|
<string name="nav_radiocontrol">Керування радіо</string>
|
||||||
<string name="prefs_net_rotator_switch">Enable rotation output</string>
|
<string name="rc_settings_title">CAT керування</string>
|
||||||
|
<string name="rc_radio_model">Модель радіо</string>
|
||||||
|
<string name="rc_tx_device_hint">BT адреса TX</string>
|
||||||
|
<string name="rc_rx_device_hint">BT адреса RX</string>
|
||||||
|
<string name="rc_tx_name_hint">Назва радіо TX</string>
|
||||||
|
<string name="rc_rx_name_hint">Назва радіо RX</string>
|
||||||
|
<string name="rc_enable_switch">Увімкнути CAT</string>
|
||||||
|
|
||||||
|
<string name="prefs_net_title">Мережевий вивід</string>
|
||||||
|
<string name="prefs_net_rotator_switch">Увімкнути вивід повороту</string>
|
||||||
<string name="prefs_net_rotator_address_hint">IP:Порт</string>
|
<string name="prefs_net_rotator_address_hint">IP:Порт</string>
|
||||||
<string name="prefs_net_rotator_format_hint">Формат даних</string>
|
<string name="prefs_net_rotator_format_hint">Формат даних</string>
|
||||||
<string name="prefs_net_frequency_switch">Enable frequency output</string>
|
<string name="prefs_net_frequency_switch">Увімкнути вивід частоти</string>
|
||||||
<string name="prefs_net_frequency_address_hint">IP:Порт</string>
|
<string name="prefs_net_frequency_address_hint">IP:Порт</string>
|
||||||
<string name="prefs_net_frequency_format_hint">Формат даних</string>
|
<string name="prefs_net_frequency_format_hint">Формат даних</string>
|
||||||
|
|
||||||
<string name="prefs_bt_title">Вивід даних Bluetooth</string>
|
<string name="prefs_bt_title">Вивід Bluetooth</string>
|
||||||
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
|
<string name="prefs_bt_rotator_switch">Увімкнути вивід повороту</string>
|
||||||
<string name="prefs_bt_rotator_device_hint">Id пристрою</string>
|
<string name="prefs_bt_rotator_device_hint">ID пристрою</string>
|
||||||
<string name="prefs_bt_rotator_output_hint">Формат даних</string>
|
<string name="prefs_bt_rotator_output_hint">Формат даних</string>
|
||||||
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
|
<string name="prefs_bt_frequency_switch">Увімкнути вивід частоти</string>
|
||||||
<string name="prefs_bt_frequency_device_hint">Id пристрою</string>
|
<string name="prefs_bt_frequency_device_hint">ID пристрою</string>
|
||||||
<string name="prefs_bt_frequency_output_hint">Формат даних</string>
|
<string name="prefs_bt_frequency_output_hint">Формат даних</string>
|
||||||
<string name="prefs_bt_perm_error">Перевірте дозвіл Bluetooth</string>
|
<string name="prefs_bt_perm_error">Перевірте дозвіл Bluetooth</string>
|
||||||
<string name="prefs_net_perm_error">Перевірте дозвіл мережі</string>
|
<string name="prefs_net_perm_error">Перевірте дозвіл мережі</string>
|
||||||
|
|
||||||
<string name="prefs_other_title">Інші налаштування</string>
|
<string name="prefs_other_title">Інше</string>
|
||||||
<string name="prefs_other_switch_utc">Показувати час в UTC</string>
|
<string name="prefs_other_switch_utc">Час в UTC</string>
|
||||||
<string name="prefs_other_switch_update">Автоматичне оновлення даних</string>
|
<string name="prefs_other_switch_update">Автооновлення даних</string>
|
||||||
<string name="prefs_other_switch_sweep">Показувати анімацію радара</string>
|
<string name="prefs_other_switch_sweep">Анімація радара</string>
|
||||||
<string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string>
|
<string name="prefs_other_switch_sensors">Сенсори</string>
|
||||||
<string name="prefs_other_switch_night_mode">Увімкнути червоний нічний фільтр</string>
|
<string name="prefs_other_switch_night_mode">Нічний фільтр</string>
|
||||||
|
|
||||||
<string name="prefs_highlight_title">Підсвічування висоти</string>
|
<string name="prefs_highlight_title">Підсвічування висоти</string>
|
||||||
<string name="prefs_highlight_low">Низько < %1$d°</string>
|
<string name="prefs_highlight_low">Низько < %1$d°</string>
|
||||||
|
|||||||
@@ -9,65 +9,23 @@
|
|||||||
<string name="nav_sat">卫星</string>
|
<string name="nav_sat">卫星</string>
|
||||||
<string name="nav_pass">过境</string>
|
<string name="nav_pass">过境</string>
|
||||||
<string name="nav_radar">雷达</string>
|
<string name="nav_radar">雷达</string>
|
||||||
<string name="nav_mutual">匹配</string>
|
|
||||||
<string name="nav_roaming">漫游</string>
|
|
||||||
<string name="nav_cw">CW解码</string>
|
|
||||||
<string name="nav_map">地图</string>
|
<string name="nav_map">地图</string>
|
||||||
<string name="nav_prefs">设置</string>
|
<string name="nav_prefs">设置</string>
|
||||||
<string name="prefs_aprs_title">APRS 上报</string>
|
|
||||||
<string name="prefs_aprs_enable">启用 APRS 上报</string>
|
|
||||||
<string name="prefs_aprs_summary">服务器: %1$s:%2$d 呼号: %3$s</string>
|
|
||||||
<string name="prefs_aprs_report_now">立即上报</string>
|
|
||||||
<string name="prefs_aprs_settings_title">APRS 设置</string>
|
|
||||||
<string name="prefs_aprs_server">服务器</string>
|
|
||||||
<string name="prefs_aprs_port">端口</string>
|
|
||||||
<string name="prefs_aprs_callsign">呼号</string>
|
|
||||||
<string name="prefs_aprs_ssid">SSID</string>
|
|
||||||
<string name="prefs_aprs_passcode">验证码</string>
|
|
||||||
<string name="prefs_aprs_passcode_hint">验证码留空 = 按呼号自动计算</string>
|
|
||||||
<string name="prefs_aprs_interval">周期(分钟)</string>
|
|
||||||
<string name="prefs_aprs_status">状态文本</string>
|
|
||||||
<string name="prefs_aprs_symbol_table">符号表</string>
|
|
||||||
<string name="prefs_aprs_symbol_code">符号码</string>
|
|
||||||
<string name="prefs_aprs_save">保存</string>
|
|
||||||
<string name="prefs_aprs_cancel">取消</string>
|
|
||||||
<string name="aprs_notif_channel">APRS 服务</string>
|
|
||||||
<string name="aprs_notif_title">APRS: %1$s</string>
|
|
||||||
<string name="aprs_notif_running">上报运行中</string>
|
|
||||||
<string name="aprs_notif_connected">已连接 APRS-IS</string>
|
|
||||||
<string name="aprs_notif_error">连接错误</string>
|
|
||||||
<string name="aprs_notif_stop">停止</string>
|
|
||||||
<string name="prefs_aprs_calc_passcode">计算验证码</string>
|
|
||||||
<string name="aprs_toast_ok">APRS: 上报成功</string>
|
|
||||||
<string name="aprs_toast_fail">APRS: 上报失败 - %1$s</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS 未配置,请先填呼号</string>
|
|
||||||
<string name="prefs_aprs_last_report">上次上报: %1$s %2$s</string>
|
|
||||||
<string name="prefs_aprs_last_ok">成功</string>
|
|
||||||
<string name="prefs_aprs_last_fail">失败 - %1$s</string>
|
|
||||||
<string name="nav_more">更多</string>
|
|
||||||
<string name="nav_log">日志</string>
|
|
||||||
<string name="nav_amsat">AMSAT</string>
|
|
||||||
<string name="amsat_title">AMSAT 卫星状态</string>
|
|
||||||
<string name="amsat_updated">更新:</string>
|
|
||||||
<string name="amsat_active">活跃</string>
|
|
||||||
<string name="amsat_tlm">仅遥测</string>
|
|
||||||
<string name="amsat_not_heard">未听到</string>
|
|
||||||
<string name="amsat_conflict">矛盾</string>
|
|
||||||
<string name="amsat_name">Name</string>
|
|
||||||
<string name="amsat_load_failed">加载失败,请检查网络后重试</string>
|
|
||||||
<string name="amsat_no_reports">该时段无报告</string>
|
|
||||||
<string name="amsat_close">关闭</string>
|
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">类型:%s</string>
|
<string name="sat_type_hint">模式:%s</string>
|
||||||
<string name="sat_type_title">选择卫星类型</string>
|
<string name="sat_type_title">选择模式</string>
|
||||||
<string name="sat_search_hint">Id - 名称</string>
|
<string name="sat_search_hint">Id - 名称</string>
|
||||||
<string name="sat_search_clear">清空</string>
|
<string name="sat_search_clear">清空</string>
|
||||||
<string name="sat_clear_all">全部清空</string>
|
<string name="sat_clear_all">全部清空</string>
|
||||||
<string name="sat_select_all">全选</string>
|
<string name="sat_select_all">全选</string>
|
||||||
|
<string name="sat_group_selected">已选择</string>
|
||||||
|
<string name="sat_group_available">可用</string>
|
||||||
|
<string name="sat_group_selected_count">已选择(%1$d)</string>
|
||||||
|
<string name="sat_group_available_count">可用(%1$d)</string>
|
||||||
<string name="sat_empty_list_message">请确保您的搜索查询正确且数据库已更新</string>
|
<string name="sat_empty_list_message">请确保您的搜索查询正确且数据库已更新</string>
|
||||||
<string name="sat_warning_title">警告\!</string>
|
<string name="sat_warning_title">警告\!</string>
|
||||||
<string name="sat_warning_message">此应用收录了超过9000颗卫星信息\n同时追踪所有卫星并不现实\n建议始终通过搜索和分类筛选功能仅勾选您感兴趣的卫星</string>
|
<string name="sat_warning_message">此应用收录了超过9000颗卫星信息\n同时追踪所有卫星并不现实\n建议始终通过搜索和模式筛选功能仅勾选您感兴趣的卫星</string>
|
||||||
|
|
||||||
<!-- Passes screen -->
|
<!-- Passes screen -->
|
||||||
<string name="pass_filter_title">筛选过境</string>
|
<string name="pass_filter_title">筛选过境</string>
|
||||||
@@ -76,22 +34,13 @@
|
|||||||
<string name="pass_filter_aos_time">AOS 时间段</string>
|
<string name="pass_filter_aos_time">AOS 时间段</string>
|
||||||
<string name="pass_filter_invert_time">反选 AOS 时间段</string>
|
<string name="pass_filter_invert_time">反选 AOS 时间段</string>
|
||||||
<string name="pass_filter_deep_space">DeepSpace(周期 >225分钟)</string>
|
<string name="pass_filter_deep_space">DeepSpace(周期 >225分钟)</string>
|
||||||
<string name="pass_modes_title">选择调制类型</string>
|
<string name="pass_modes_title">选择模式</string>
|
||||||
<string name="pass_elevation">仰角:%.1f°</string>
|
<string name="pass_elevation">仰角:%.1f°</string>
|
||||||
<string name="pass_altitude">高度:%d km</string>
|
<string name="pass_altitude">高度:%d km</string>
|
||||||
<string name="pass_empty_list_message">请确保您的过滤器设置正确,并且已选择卫星</string>
|
<string name="pass_empty_list_message">请确保您的过滤器设置正确,并且已选择卫星</string>
|
||||||
<string name="pass_error_message">没有即将过境的卫星</string>
|
<string name="pass_error_message">没有即将过境的卫星</string>
|
||||||
<string name="pass_welcome_title">欢迎来到 Look4Sat\!</string>
|
<string name="pass_welcome_title">欢迎来到 Look4Sat\!</string>
|
||||||
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
|
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
|
||||||
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
|
|
||||||
<string name="pass_whatsnew_message" translatable="false">
|
|
||||||
* 新增: AMSAT 卫星状态页 - 实时状态表格(官网配色)、报告数量、逐卫星/逐日详细报告
|
|
||||||
* 日志按场次分组(场次 = 卫星 + AOS 时间), 组间横线分隔
|
|
||||||
* GPS 真正定位成功才会提示成功(15 秒超时, 权限检查)
|
|
||||||
* 卫星数据库下载失败时有明确失败提示
|
|
||||||
* 日志频率实时跟随多普勒面板; 左滑删除视觉恢复
|
|
||||||
* 印尼语识别修复(values-id)
|
|
||||||
* 新增: APRS 网络上报(APRS-IS,前台服务+通知保活,周期可调)</string>
|
|
||||||
|
|
||||||
<!-- Radar screen -->
|
<!-- Radar screen -->
|
||||||
<string name="radar_back">后退</string>
|
<string name="radar_back">后退</string>
|
||||||
@@ -111,17 +60,9 @@
|
|||||||
<string name="radar_string_no">否</string>
|
<string name="radar_string_no">否</string>
|
||||||
<string name="radar_string_yes">是</string>
|
<string name="radar_string_yes">是</string>
|
||||||
<string name="radar_visible">日照中</string>
|
<string name="radar_visible">日照中</string>
|
||||||
|
|
||||||
<string name="radar_doppler_calc">多普勒频率计算器</string>
|
|
||||||
<string name="radar_doppler_tx_hint">输入上行频率 (MHz)</string>
|
<string name="radar_doppler_tx_hint">输入上行频率 (MHz)</string>
|
||||||
<string name="radar_doppler_rx_hint">输入下行频率 (MHz)</string>
|
<string name="radar_doppler_rx_hint">输入下行频率 (MHz)</string>
|
||||||
<string name="radar_doppler_offset_hint">偏移 (kHz)</string>
|
<string name="radar_doppler_offset_hint">偏移 (kHz)</string>
|
||||||
<string name="radar_doppler_info">线性转发器:输入一个频率即可算出另一个</string>
|
|
||||||
|
|
||||||
<string name="radar_cw_decoder">CW 解码器</string>
|
|
||||||
<string name="radar_cw_start">开始</string>
|
|
||||||
<string name="radar_cw_stop">停止</string>
|
|
||||||
<string name="radar_cw_reset">清空</string>
|
|
||||||
|
|
||||||
<!-- Map screen -->
|
<!-- Map screen -->
|
||||||
<string name="map_prev">上一个</string>
|
<string name="map_prev">上一个</string>
|
||||||
@@ -146,43 +87,51 @@
|
|||||||
<string name="prefs_updated_title">更新:%s</string>
|
<string name="prefs_updated_title">更新:%s</string>
|
||||||
<string name="prefs_updated_time">yyyy MMM d - HH:mm:ss</string>
|
<string name="prefs_updated_time">yyyy MMM d - HH:mm:ss</string>
|
||||||
|
|
||||||
<string name="prefs_loc_title">站点位置</string>
|
<string name="prefs_loc_title">站位</string>
|
||||||
<string name="prefs_loc_gps_title">GPS 定位</string>
|
<string name="prefs_loc_gps_title">GPS 定位</string>
|
||||||
<string name="prefs_loc_gps_error">无法获取位置,请检查定位权限和GPS/网络信号</string>
|
<string name="prefs_loc_gps_error">检查您的位置权限</string>
|
||||||
<string name="prefs_loc_input_title">输入位置</string>
|
<string name="prefs_loc_input_title">输入位置</string>
|
||||||
<string name="prefs_loc_input_error">输入的位置无效</string>
|
<string name="prefs_loc_input_error">输入的位置无效</string>
|
||||||
<string name="prefs_loc_qth_title">QTH 网格</string>
|
<string name="prefs_loc_qth_title">QTH 网格</string>
|
||||||
<string name="prefs_loc_qth_error">无效的 QTH 网格</string>
|
<string name="prefs_loc_qth_error">无效的 QTH 网格</string>
|
||||||
<string name="prefs_loc_success">位置更新成功</string>
|
<string name="prefs_loc_success">位置更新成功</string>
|
||||||
<string name="prefs_station_title">站点位置设置</string>
|
<string name="prefs_station_title">站位</string>
|
||||||
<string name="prefs_station_lat_text">输入站点的纬度</string>
|
<string name="prefs_station_lat_text">站位纬度</string>
|
||||||
<string name="prefs_station_lon_text">输入站点的经度</string>
|
<string name="prefs_station_lon_text">站位经度</string>
|
||||||
<string name="prefs_locator_title">QTH 网格</string>
|
<string name="prefs_locator_title">QTH 定位</string>
|
||||||
<string name="prefs_locator_text">使用梅登黑德网格设置站点位置</string>
|
<string name="prefs_locator_text">通过 QTH 设置站位</string>
|
||||||
|
|
||||||
<string name="prefs_data_title">卫星数据更新</string>
|
<string name="prefs_data_title">卫星数据更新</string>
|
||||||
<string name="prefs_data_entries">卫星:%s</string>
|
<string name="prefs_data_entries">卫星:%s</string>
|
||||||
<string name="prefs_data_radios">收发器:%s</string>
|
<string name="prefs_data_radios">收发器:%s</string>
|
||||||
<string name="prefs_data_update">在线更新</string>
|
<string name="prefs_data_update">在线更新</string>
|
||||||
<string name="prefs_data_import">自定义URL</string>
|
<string name="prefs_data_import">导入文件</string>
|
||||||
<string name="prefs_data_clear">清空</string>
|
<string name="prefs_data_clear">清空</string>
|
||||||
<string name="prefs_data_clear_success">数据清空成功</string>
|
<string name="prefs_data_clear_success">数据清空成功</string>
|
||||||
<string name="prefs_data_update_success">更新成功</string>
|
<string name="prefs_data_update_success">更新成功</string>
|
||||||
<string name="prefs_data_update_failed">更新失败,请检查网络</string>
|
|
||||||
|
|
||||||
<string name="prefs_data_import_satellites_error">未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。</string>
|
<string name="prefs_data_import_satellites_error">未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。</string>
|
||||||
<string name="prefs_data_import_transceivers_error">未导入收发器。请选择有效的 SatNOGS (.json) 文件。</string>
|
<string name="prefs_data_import_transceivers_error">未导入收发器。请选择有效的 SatNOGS (.json) 文件。</string>
|
||||||
|
|
||||||
<string name="prefs_data_sources_title">自定义数据源</string>
|
<string name="prefs_data_sources_title">自定义源</string>
|
||||||
<string name="prefs_data_sources_tle_switch">自定义TLE URL</string>
|
<string name="prefs_data_sources_tle_switch">TLE URL</string>
|
||||||
<string name="prefs_data_sources_transceivers_switch">自定义收发器URL</string>
|
<string name="prefs_data_sources_transceivers_switch">收发器 URL</string>
|
||||||
|
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
||||||
|
|
||||||
<string name="prefs_data_output_title">数据输出</string>
|
<string name="prefs_data_output_title">数据输出</string>
|
||||||
<string name="prefs_net_output">网络</string>
|
<string name="prefs_net_output">网络</string>
|
||||||
<string name="prefs_bt_output">蓝牙</string>
|
<string name="prefs_bt_output">蓝牙</string>
|
||||||
<string name="prefs_cat_output">CAT</string>
|
<string name="prefs_cat_output">CAT</string>
|
||||||
|
|
||||||
<string name="prefs_net_title">网络数据输出</string>
|
<string name="nav_radiocontrol">电台控制</string>
|
||||||
|
<string name="rc_settings_title">CAT 控制</string>
|
||||||
|
<string name="rc_radio_model">电台型号</string>
|
||||||
|
<string name="rc_tx_device_hint">TX 电台蓝牙地址</string>
|
||||||
|
<string name="rc_rx_device_hint">RX 电台蓝牙地址</string>
|
||||||
|
<string name="rc_tx_name_hint">TX 电台名称</string>
|
||||||
|
<string name="rc_rx_name_hint">RX 电台名称</string>
|
||||||
|
<string name="rc_enable_switch">启用 CAT</string>
|
||||||
|
|
||||||
|
<string name="prefs_net_title">网络输出</string>
|
||||||
<string name="prefs_net_rotator_switch">启用方位俯仰角输出</string>
|
<string name="prefs_net_rotator_switch">启用方位俯仰角输出</string>
|
||||||
<string name="prefs_net_rotator_address_hint">IP:端口</string>
|
<string name="prefs_net_rotator_address_hint">IP:端口</string>
|
||||||
<string name="prefs_net_rotator_format_hint">数据格式</string>
|
<string name="prefs_net_rotator_format_hint">数据格式</string>
|
||||||
@@ -190,7 +139,7 @@
|
|||||||
<string name="prefs_net_frequency_address_hint">IP:端口</string>
|
<string name="prefs_net_frequency_address_hint">IP:端口</string>
|
||||||
<string name="prefs_net_frequency_format_hint">数据格式</string>
|
<string name="prefs_net_frequency_format_hint">数据格式</string>
|
||||||
|
|
||||||
<string name="prefs_bt_title">蓝牙数据输出</string>
|
<string name="prefs_bt_title">蓝牙输出</string>
|
||||||
<string name="prefs_bt_rotator_switch">启用方位俯仰角输出</string>
|
<string name="prefs_bt_rotator_switch">启用方位俯仰角输出</string>
|
||||||
<string name="prefs_bt_rotator_device_hint">蓝牙设备 ID</string>
|
<string name="prefs_bt_rotator_device_hint">蓝牙设备 ID</string>
|
||||||
<string name="prefs_bt_rotator_output_hint">数据格式</string>
|
<string name="prefs_bt_rotator_output_hint">数据格式</string>
|
||||||
@@ -200,80 +149,26 @@
|
|||||||
<string name="prefs_bt_perm_error">请检查蓝牙权限</string>
|
<string name="prefs_bt_perm_error">请检查蓝牙权限</string>
|
||||||
<string name="prefs_net_perm_error">请检查网络权限</string>
|
<string name="prefs_net_perm_error">请检查网络权限</string>
|
||||||
|
|
||||||
<!-- Radio Control -->
|
<string name="prefs_other_title">其他</string>
|
||||||
<string name="nav_radiocontrol">电台控制</string>
|
<string name="prefs_other_switch_utc">UTC 时间</string>
|
||||||
<string name="rc_settings_title">CAT 电台控制</string>
|
<string name="prefs_other_switch_update">自动更新数据</string>
|
||||||
<string name="rc_radio_model">电台型号</string>
|
<string name="prefs_other_switch_sweep">雷达扫描动画</string>
|
||||||
<string name="rc_tx_device_hint">发射电台蓝牙地址</string>
|
<string name="prefs_other_switch_sensors">传感器控制雷达</string>
|
||||||
<string name="rc_rx_device_hint">接收电台蓝牙地址</string>
|
<string name="prefs_other_switch_night_mode">红色夜间模式</string>
|
||||||
<string name="rc_tx_name_hint">发射电台名称</string>
|
|
||||||
<string name="rc_rx_name_hint">接收电台名称</string>
|
|
||||||
<string name="rc_enable_switch">启用 CAT 控制</string>
|
|
||||||
|
|
||||||
<string name="tracking_status">跟踪: %s</string>
|
|
||||||
<string name="loc_lat_text">纬度: %s°</string>
|
|
||||||
<string name="loc_lon_text">经度: %s°</string>
|
|
||||||
<string name="loc_qth_text">QTH: %s</string>
|
|
||||||
<string name="connect_radio">连接</string>
|
|
||||||
<string name="disconnect_radio">断开</string>
|
|
||||||
<string name="track_radio">跟踪</string>
|
|
||||||
<string name="stop_radio">停止</string>
|
|
||||||
<string name="prefs_wavelog_title">WaveLog 日志服务器</string>
|
|
||||||
<string name="prefs_wavelog_url_hint">服务器地址</string>
|
|
||||||
<string name="prefs_wavelog_key_hint">API 密钥</string>
|
|
||||||
<string name="prefs_wavelog_station_hint">站点 ID</string>
|
|
||||||
<string name="prefs_wavelog_auto">自动上传</string>
|
|
||||||
<string name="prefs_wavelog_test">测试连接</string>
|
|
||||||
<string name="prefs_wavelog_upload">手动上传</string>
|
|
||||||
<string name="wavelog_title">日志</string>
|
|
||||||
<string name="wavelog_select_transponder">选择转发器</string>
|
|
||||||
<string name="wavelog_call_hint">对方呼号</string>
|
|
||||||
<string name="wavelog_mode_hint">模式</string>
|
|
||||||
<string name="wavelog_saved">已存入本地日志</string>
|
|
||||||
<string name="wavelog_need_config">请先在设置中配置 WaveLog</string>
|
|
||||||
<string name="wavelog_empty">暂无日志记录</string>
|
|
||||||
<string name="wavelog_grid_mismatch">当前 QTH 网格(%1$s)与站点网格(%2$s)不一致,仍要上传?</string>
|
|
||||||
<string name="wavelog_ignore">忽略并上传</string>
|
|
||||||
<string name="wavelog_cancel">取消</string>
|
|
||||||
<string name="wavelog_copy">复制</string>
|
|
||||||
<string name="wavelog_delete_undo">撤销</string>
|
|
||||||
<string name="wavelog_col_time">时间</string>
|
|
||||||
<string name="wavelog_col_freq">频率</string>
|
|
||||||
<string name="wavelog_col_sat">卫星</string>
|
|
||||||
<string name="wavelog_col_call">呼号</string>
|
|
||||||
<string name="wavelog_col_uploaded">已传</string>
|
|
||||||
<string name="wavelog_ungrouped">未分组</string>
|
|
||||||
<string name="prefs_other_title">其他设置</string>
|
|
||||||
<string name="prefs_ui_title">UI设置</string>
|
|
||||||
<string name="prefs_ui_order_title">页面顺序</string>
|
|
||||||
<string name="prefs_ui_main_title">主菜单(底部栏)</string>
|
|
||||||
<string name="prefs_ui_sub_title">更多菜单</string>
|
|
||||||
<string name="prefs_ui_move_out">移入更多菜单</string>
|
|
||||||
<string name="prefs_ui_move_back">移回主菜单</string>
|
|
||||||
<string name="prefs_ui_order_locked">固定(不可移动)</string>
|
|
||||||
<string name="prefs_ui_order_reset">重置顺序</string>
|
|
||||||
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
|
|
||||||
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
|
|
||||||
<string name="prefs_other_switch_sweep">启用雷达扫描动画</string>
|
|
||||||
<string name="prefs_other_switch_sensors">使用传感器旋转雷达视图</string>
|
|
||||||
<string name="prefs_other_switch_night_mode">启用红色夜间模式</string>
|
|
||||||
|
|
||||||
<string name="prefs_highlight_title">仰角高亮</string>
|
<string name="prefs_highlight_title">仰角高亮</string>
|
||||||
<string name="prefs_highlight_low">低 < %1$d°</string>
|
<string name="prefs_highlight_low">低 < %1$d°</string>
|
||||||
<string name="prefs_highlight_mid">中 %1$d-%2$d°</string>
|
<string name="prefs_highlight_mid">中 %1$d-%2$d°</string>
|
||||||
<string name="prefs_highlight_high">高 > %1$d°</string>
|
<string name="prefs_highlight_high">高 > %1$d°</string>
|
||||||
|
|
||||||
<string name="prefs_outro_title">BG7NTA 与原作者感谢:</string>
|
<string name="prefs_outro_title">我要感谢:</string>
|
||||||
<string name="prefs_outro_thanks" translatable="false">
|
<string name="prefs_outro_thanks" translatable="false">
|
||||||
• Look4Sat 所有用户及贡献者!
|
• Look4Sat 所有用户及贡献者!
|
||||||
\n• David A. B. Johnson (predict4java)
|
\n• David A. B. Johnson (predict4java)
|
||||||
\n• Dave Moten (predict4java)
|
\n• Dave Moten (predict4java)
|
||||||
\n• Alexandru Csete (Gpredict)
|
\n• Alexandru Csete (Gpredict)
|
||||||
\n• Dr T.S. Kelso (Celestrak)
|
\n• Dr T.S. Kelso (Celestrak)
|
||||||
\n• Libre Space Foundation (SatNOGS)
|
\n• Libre Space Foundation (SatNOGS)</string>
|
||||||
\n• xdsopl 和 Robot36 贡献者!
|
|
||||||
\n• BA7OPF(过境匹配功能)
|
|
||||||
\n• BG7NTA</string>
|
|
||||||
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
|
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Commmon -->
|
<!-- Commmon -->
|
||||||
<string name="app_name" translatable="false">Look4Sat Pro</string>
|
<string name="app_name" translatable="false">Look4Sat</string>
|
||||||
<string name="btn_accept">Accept</string>
|
<string name="btn_accept">Accept</string>
|
||||||
<string name="btn_cancel">Cancel</string>
|
<string name="btn_cancel">Cancel</string>
|
||||||
<string name="empty_list_message">No data to show</string>
|
<string name="empty_list_message">No data to show</string>
|
||||||
@@ -10,69 +10,27 @@
|
|||||||
<string name="nav_sat">Satellites</string>
|
<string name="nav_sat">Satellites</string>
|
||||||
<string name="nav_pass">Passes</string>
|
<string name="nav_pass">Passes</string>
|
||||||
<string name="nav_radar">Radar</string>
|
<string name="nav_radar">Radar</string>
|
||||||
<string name="nav_mutual">Match</string>
|
|
||||||
<string name="nav_roaming">Roaming</string>
|
|
||||||
<string name="nav_cw">CW Decoder</string>
|
|
||||||
<string name="nav_map">Map</string>
|
<string name="nav_map">Map</string>
|
||||||
<string name="nav_prefs">Settings</string>
|
<string name="nav_prefs">Settings</string>
|
||||||
<string name="prefs_aprs_title">APRS Reporting</string>
|
|
||||||
<string name="prefs_aprs_enable">Enable APRS reporting</string>
|
|
||||||
<string name="prefs_aprs_summary">Server: %1$s:%2$d Callsign: %3$s</string>
|
|
||||||
<string name="prefs_aprs_report_now">Report Now</string>
|
|
||||||
<string name="prefs_aprs_settings_title">APRS Settings</string>
|
|
||||||
<string name="prefs_aprs_server">Server</string>
|
|
||||||
<string name="prefs_aprs_port">Port</string>
|
|
||||||
<string name="prefs_aprs_callsign">Callsign</string>
|
|
||||||
<string name="prefs_aprs_ssid">SSID</string>
|
|
||||||
<string name="prefs_aprs_passcode">Passcode</string>
|
|
||||||
<string name="prefs_aprs_passcode_hint">Passcode empty = auto-computed from callsign</string>
|
|
||||||
<string name="prefs_aprs_interval">Interval (min)</string>
|
|
||||||
<string name="prefs_aprs_status">Status text</string>
|
|
||||||
<string name="prefs_aprs_symbol_table">Symbol table</string>
|
|
||||||
<string name="prefs_aprs_symbol_code">Symbol code</string>
|
|
||||||
<string name="prefs_aprs_save">Save</string>
|
|
||||||
<string name="prefs_aprs_cancel">Cancel</string>
|
|
||||||
<string name="aprs_notif_channel">APRS service</string>
|
|
||||||
<string name="aprs_notif_title">APRS: %1$s</string>
|
|
||||||
<string name="aprs_notif_running">Reporting running</string>
|
|
||||||
<string name="aprs_notif_connected">Connected to APRS-IS</string>
|
|
||||||
<string name="aprs_notif_error">Connection error</string>
|
|
||||||
<string name="aprs_notif_stop">Stop</string>
|
|
||||||
<string name="prefs_aprs_calc_passcode">Compute passcode</string>
|
|
||||||
<string name="aprs_toast_ok">APRS: report sent OK</string>
|
|
||||||
<string name="aprs_toast_fail">APRS: report failed - %1$s</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS not configured - set callsign first</string>
|
|
||||||
<string name="prefs_aprs_last_report">Last report: %1$s %2$s</string>
|
|
||||||
<string name="prefs_aprs_last_ok">OK</string>
|
|
||||||
<string name="prefs_aprs_last_fail">failed - %1$s</string>
|
|
||||||
<string name="nav_more">More</string>
|
|
||||||
<string name="nav_log">Log</string>
|
|
||||||
<string name="nav_amsat">AMSAT</string>
|
|
||||||
<string name="amsat_title">AMSAT Satellite Status</string>
|
|
||||||
<string name="amsat_updated">Updated:</string>
|
|
||||||
<string name="amsat_active">Active</string>
|
|
||||||
<string name="amsat_tlm">TLM/Beacon</string>
|
|
||||||
<string name="amsat_not_heard">Not Heard</string>
|
|
||||||
<string name="amsat_conflict">Conflicting</string>
|
|
||||||
<string name="amsat_name">Name</string>
|
|
||||||
<string name="amsat_load_failed">Load failed - check network and retry</string>
|
|
||||||
<string name="amsat_no_reports">No reports for this period</string>
|
|
||||||
<string name="amsat_close">Close</string>
|
|
||||||
|
|
||||||
<!-- Satellites screen -->
|
<!-- Satellites screen -->
|
||||||
<string name="sat_type_hint">Type: %s</string>
|
<string name="sat_type_hint">Modes: %s</string>
|
||||||
<string name="sat_type_title">Select satellite type</string>
|
<string name="sat_type_title">Select modes</string>
|
||||||
<string name="sat_search_hint">Id - Name</string>
|
<string name="sat_search_hint">Id - Name</string>
|
||||||
<string name="sat_search_clear">Clear</string>
|
<string name="sat_search_clear">Clear</string>
|
||||||
<string name="sat_clear_all">Clear all</string>
|
<string name="sat_clear_all">Clear all</string>
|
||||||
<string name="sat_select_all">Select all</string>
|
<string name="sat_select_all">Select all</string>
|
||||||
|
<string name="sat_group_selected">Selected</string>
|
||||||
|
<string name="sat_group_available">Available</string>
|
||||||
|
<string name="sat_group_selected_count">Selected (%1$d)</string>
|
||||||
|
<string name="sat_group_available_count">Available (%1$d)</string>
|
||||||
<string name="sat_empty_list_message">"Make sure your search query is correct and the DB is updated"</string>
|
<string name="sat_empty_list_message">"Make sure your search query is correct and the DB is updated"</string>
|
||||||
<string name="sat_warning_title">Warning\!</string>
|
<string name="sat_warning_title">Warning\!</string>
|
||||||
<string name="sat_warning_message">
|
<string name="sat_warning_message">
|
||||||
There are over 9000 satellites listed in this app.
|
There are over 9000 satellites listed in this app.
|
||||||
It makes no sense to track them all at the same time.
|
It makes no sense to track them all at the same time.
|
||||||
\n\nAlways try to narrow down the list to only the ones
|
\n\nAlways try to narrow down the list to only the ones
|
||||||
you\'re interested in via search and types selector.</string>
|
you\'re interested in via search and the modes selector.</string>
|
||||||
|
|
||||||
<!-- Passes screen -->
|
<!-- Passes screen -->
|
||||||
<string name="pass_filter_title">Filter passes</string>
|
<string name="pass_filter_title">Filter passes</string>
|
||||||
@@ -82,7 +40,7 @@
|
|||||||
<string name="pass_filter_invert_time">Invert AOS window</string>
|
<string name="pass_filter_invert_time">Invert AOS window</string>
|
||||||
<string name="pass_filter_deep_space">DeepSpace (period >225min)</string>
|
<string name="pass_filter_deep_space">DeepSpace (period >225min)</string>
|
||||||
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
||||||
<string name="pass_modes_title">Select modulation type</string>
|
<string name="pass_modes_title">Select modes</string>
|
||||||
<string name="pass_satId" translatable="false">%05d</string>
|
<string name="pass_satId" translatable="false">%05d</string>
|
||||||
<string name="pass_elevation">Elevation: %.1f°</string>
|
<string name="pass_elevation">Elevation: %.1f°</string>
|
||||||
<string name="pass_deep_space" translatable="false">DeepSpace</string>
|
<string name="pass_deep_space" translatable="false">DeepSpace</string>
|
||||||
@@ -96,14 +54,33 @@
|
|||||||
Make sure to set your position via GPS, Lat/Lon or QTH in Settings.
|
Make sure to set your position via GPS, Lat/Lon or QTH in Settings.
|
||||||
\n\nPlease update the database at least weekly to get accurate predictions.</string>
|
\n\nPlease update the database at least weekly to get accurate predictions.</string>
|
||||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||||
<string name="pass_whatsnew_message" translatable="false">
|
<string name="pass_whatsnew_message" translatable="false">
|
||||||
* NEW: AMSAT satellite status page - live status grid (official colors), report counts, detailed reports per satellite/day
|
* Added small tweaks to Sources and strings. Added official ISS source (#225)
|
||||||
* Log entries grouped by satellite pass (session ID = satellite + AOS time), divider lines between sessions
|
\n* Implemented AOS window and elevation highlight filters (#226)
|
||||||
* GPS now only reports success when a real fix is acquired (15s timeout, permission check)
|
\n* Added cleartext traffic support for custom TLE URLs (#227)
|
||||||
* Data update shows a clear failure message when the satellite database cannot be downloaded
|
\n* Fixed manual OMM (.csv) data import, tweaked error messaging (#228)
|
||||||
* Log frequencies follow the Doppler panel in real time; swipe-to-delete visuals restored
|
\n* Added support for Icom IC-705 CAT by ruilvo (#229)
|
||||||
* Indonesian locale fix (values-id)
|
\n* Added configurable Radar offset to the sensors output by qwqtoday (#230)
|
||||||
* NEW: APRS reporting (APRS-IS network, foreground service with notification, configurable interval)</string>
|
\n* Localized pass date and time formats for Chinese by atsunatsu (#231)
|
||||||
|
\n* Added linear transponder Doppler calculator by atsunatsu (#232)
|
||||||
|
\n* Added a few tweaks to SSTV sensitivity and reception, similar to Robot36
|
||||||
|
\n* Replaced the types selection dialog with modes selection to avoid confusion
|
||||||
|
</string>
|
||||||
|
|
||||||
|
<!-- AMSAT Status screen -->
|
||||||
|
<string name="nav_amsat">AMSAT</string>
|
||||||
|
<string name="amsat_title">AMSAT Satellite Status</string>
|
||||||
|
<string name="amsat_refresh">Refresh</string>
|
||||||
|
<string name="amsat_retry">Retry</string>
|
||||||
|
<string name="amsat_updated">Updated:</string>
|
||||||
|
<string name="amsat_active">Active</string>
|
||||||
|
<string name="amsat_tlm">TLM/Beacon</string>
|
||||||
|
<string name="amsat_not_heard">Not Heard</string>
|
||||||
|
<string name="amsat_conflict">Conflicting</string>
|
||||||
|
<string name="amsat_name">Name</string>
|
||||||
|
<string name="amsat_load_failed">Load failed - check network and retry</string>
|
||||||
|
<string name="amsat_no_reports">No reports for this period</string>
|
||||||
|
<string name="amsat_close">Close</string>
|
||||||
|
|
||||||
<!-- Radar screen -->
|
<!-- Radar screen -->
|
||||||
<string name="radar_back">Back</string>
|
<string name="radar_back">Back</string>
|
||||||
@@ -130,16 +107,9 @@
|
|||||||
<string name="radar_string_no">No</string>
|
<string name="radar_string_no">No</string>
|
||||||
<string name="radar_string_yes">Yes</string>
|
<string name="radar_string_yes">Yes</string>
|
||||||
<string name="radar_visible">Visible</string>
|
<string name="radar_visible">Visible</string>
|
||||||
<string name="radar_doppler_calc">Doppler Frequency Calculator</string>
|
|
||||||
<string name="radar_doppler_tx_hint">Enter TX freq (MHz)</string>
|
<string name="radar_doppler_tx_hint">Enter TX freq (MHz)</string>
|
||||||
<string name="radar_doppler_rx_hint">Enter RX freq (MHz)</string>
|
<string name="radar_doppler_rx_hint">Enter RX freq (MHz)</string>
|
||||||
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
|
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
|
||||||
<string name="radar_doppler_info">For linear transponders, type one frequency to see the other</string>
|
|
||||||
|
|
||||||
<string name="radar_cw_decoder">CW Decoder</string>
|
|
||||||
<string name="radar_cw_start">Start</string>
|
|
||||||
<string name="radar_cw_stop">Stop</string>
|
|
||||||
<string name="radar_cw_reset">Clear</string>
|
|
||||||
|
|
||||||
<!-- Map screen -->
|
<!-- Map screen -->
|
||||||
<string name="map_prev">Prev</string>
|
<string name="map_prev">Prev</string>
|
||||||
@@ -160,7 +130,7 @@
|
|||||||
<string name="map_visible">Visible</string>
|
<string name="map_visible">Visible</string>
|
||||||
|
|
||||||
<!-- Settings screen -->
|
<!-- Settings screen -->
|
||||||
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
|
<string name="prefs_app_title" translatable="false">Look4Sat v%s</string>
|
||||||
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
||||||
<string name="prefs_donate_title">Donate</string>
|
<string name="prefs_donate_title">Donate</string>
|
||||||
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
|
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
|
||||||
@@ -178,35 +148,33 @@
|
|||||||
|
|
||||||
<string name="prefs_loc_title">Station position</string>
|
<string name="prefs_loc_title">Station position</string>
|
||||||
<string name="prefs_loc_gps_title">GPS</string>
|
<string name="prefs_loc_gps_title">GPS</string>
|
||||||
<string name="prefs_loc_gps_error">Unable to get location - check permission and GPS/network signal</string>
|
<string name="prefs_loc_gps_error">Check your location permissions</string>
|
||||||
<string name="prefs_loc_input_title">Input</string>
|
<string name="prefs_loc_input_title">Input</string>
|
||||||
<string name="prefs_loc_input_error">Invalid location entered</string>
|
<string name="prefs_loc_input_error">Invalid location entered</string>
|
||||||
<string name="prefs_loc_qth_title">QTH</string>
|
<string name="prefs_loc_qth_title">QTH</string>
|
||||||
<string name="prefs_loc_qth_error">Invalid QTH locator</string>
|
<string name="prefs_loc_qth_error">Invalid QTH locator</string>
|
||||||
<string name="prefs_loc_success">Position updated successfully</string>
|
<string name="prefs_loc_success">Position updated successfully</string>
|
||||||
<string name="prefs_station_title">Station position settings</string>
|
<string name="prefs_station_title">Station position</string>
|
||||||
<string name="prefs_station_lat_text">Set your ground station\'s latitude</string>
|
<string name="prefs_station_lat_text">Station latitude</string>
|
||||||
<string name="prefs_station_lon_text">Set your ground station\'s longitude</string>
|
<string name="prefs_station_lon_text">Station longitude</string>
|
||||||
<string name="prefs_locator_title">QTH locator settings</string>
|
<string name="prefs_locator_title">QTH locator</string>
|
||||||
<string name="prefs_locator_text">Set station\'s position using locator</string>
|
<string name="prefs_locator_text">Set position via locator</string>
|
||||||
|
|
||||||
<string name="prefs_data_title">Satellite data</string>
|
<string name="prefs_data_title">Satellite data</string>
|
||||||
<string name="prefs_data_entries">Satellites: %s</string>
|
<string name="prefs_data_entries">Satellites: %s</string>
|
||||||
<string name="prefs_data_radios">Transceivers: %s</string>
|
<string name="prefs_data_radios">Transceivers: %s</string>
|
||||||
<string name="prefs_data_update">Update</string>
|
<string name="prefs_data_update">Update</string>
|
||||||
<string name="prefs_data_import">Custom URL</string>
|
<string name="prefs_data_import">Import</string>
|
||||||
<string name="prefs_data_clear">Clear</string>
|
<string name="prefs_data_clear">Clear</string>
|
||||||
<string name="prefs_data_clear_success">Data was cleared successfully</string>
|
<string name="prefs_data_clear_success">Data was cleared successfully</string>
|
||||||
<string name="prefs_data_update_success">Update completed successfully</string>
|
<string name="prefs_data_update_success">Update completed successfully</string>
|
||||||
<string name="prefs_data_update_failed">Update failed - check your network</string>
|
|
||||||
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
||||||
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
||||||
|
|
||||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
<string name="prefs_data_sources_title">Custom sources</string>
|
||||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
<string name="prefs_data_sources_tle_switch">TLE URL</string>
|
||||||
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
|
<string name="prefs_data_sources_transceivers_switch">Transceivers URL</string>
|
||||||
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
||||||
<string name="prefs_data_reset_url">Reset to default URL</string>
|
|
||||||
|
|
||||||
<string name="prefs_data_output_title">Data output</string>
|
<string name="prefs_data_output_title">Data output</string>
|
||||||
<string name="prefs_net_output">Network</string>
|
<string name="prefs_net_output">Network</string>
|
||||||
@@ -215,15 +183,15 @@
|
|||||||
|
|
||||||
<!-- Radio Control -->
|
<!-- Radio Control -->
|
||||||
<string name="nav_radiocontrol">Radio Control</string>
|
<string name="nav_radiocontrol">Radio Control</string>
|
||||||
<string name="rc_settings_title">CAT Radio Control</string>
|
<string name="rc_settings_title">CAT control</string>
|
||||||
<string name="rc_radio_model">Radio Model</string>
|
<string name="rc_radio_model">Radio Model</string>
|
||||||
<string name="rc_tx_device_hint">TX Radio BT Address</string>
|
<string name="rc_tx_device_hint">TX BT address</string>
|
||||||
<string name="rc_rx_device_hint">RX Radio BT Address</string>
|
<string name="rc_rx_device_hint">RX BT address</string>
|
||||||
<string name="rc_tx_name_hint">TX Radio Name</string>
|
<string name="rc_tx_name_hint">TX Radio Name</string>
|
||||||
<string name="rc_rx_name_hint">RX Radio Name</string>
|
<string name="rc_rx_name_hint">RX Radio Name</string>
|
||||||
<string name="rc_enable_switch">Enable CAT Control</string>
|
<string name="rc_enable_switch">Enable CAT</string>
|
||||||
|
|
||||||
<string name="prefs_net_title">Network data output</string>
|
<string name="prefs_net_title">Network output</string>
|
||||||
<string name="prefs_net_rotator_switch">Enable rotation output</string>
|
<string name="prefs_net_rotator_switch">Enable rotation output</string>
|
||||||
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
||||||
<string name="prefs_net_rotator_format_hint">Format</string>
|
<string name="prefs_net_rotator_format_hint">Format</string>
|
||||||
@@ -231,70 +199,31 @@
|
|||||||
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
||||||
<string name="prefs_net_frequency_format_hint">Format</string>
|
<string name="prefs_net_frequency_format_hint">Format</string>
|
||||||
|
|
||||||
<string name="prefs_bt_title">Bluetooth data output</string>
|
<string name="prefs_bt_title">Bluetooth output</string>
|
||||||
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
|
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
|
||||||
<string name="prefs_bt_rotator_device_hint">Device id</string>
|
<string name="prefs_bt_rotator_device_hint">Device ID</string>
|
||||||
<string name="prefs_bt_rotator_output_hint">Data format</string>
|
<string name="prefs_bt_rotator_output_hint">Format</string>
|
||||||
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
|
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
|
||||||
<string name="prefs_bt_frequency_device_hint">Device id</string>
|
<string name="prefs_bt_frequency_device_hint">Device ID</string>
|
||||||
<string name="prefs_bt_frequency_output_hint">Data format</string>
|
<string name="prefs_bt_frequency_output_hint">Format</string>
|
||||||
<string name="prefs_bt_perm_error">Check your bluetooth permission</string>
|
<string name="prefs_bt_perm_error">Check your bluetooth permission</string>
|
||||||
<string name="prefs_net_perm_error">Check your network permission</string>
|
<string name="prefs_net_perm_error">Check your network permission</string>
|
||||||
|
|
||||||
<string name="tracking_status">Tracking: %s</string>
|
<string name="prefs_other_title">Other</string>
|
||||||
<string name="loc_lat_text">Lat: %s°</string>
|
<string name="prefs_other_switch_utc">UTC time</string>
|
||||||
<string name="loc_lon_text">Lon: %s°</string>
|
<string name="prefs_other_switch_update">Auto-update data</string>
|
||||||
<string name="loc_qth_text">Qth: %s</string>
|
<string name="prefs_other_switch_sweep">Radar sweep</string>
|
||||||
<string name="connect_radio">Connect</string>
|
<string name="prefs_other_switch_sensors">Use sensors</string>
|
||||||
<string name="disconnect_radio">Disconnect</string>
|
<string name="prefs_other_switch_night_mode">Night filter</string>
|
||||||
<string name="track_radio">Track</string>
|
<string name="prefs_other_compass_offset">Radar compass offset</string>
|
||||||
<string name="stop_radio">Stop</string>
|
<string name="prefs_other_compass_offset_elev">Radar compass offset (elev)</string>
|
||||||
<string name="prefs_wavelog_title">WaveLog log server</string>
|
|
||||||
<string name="prefs_wavelog_url_hint">Server address</string>
|
|
||||||
<string name="prefs_wavelog_key_hint">API key</string>
|
|
||||||
<string name="prefs_wavelog_station_hint">Station ID</string>
|
|
||||||
<string name="prefs_wavelog_auto">Auto upload</string>
|
|
||||||
<string name="prefs_wavelog_test">Test connection</string>
|
|
||||||
<string name="prefs_wavelog_upload">Upload now</string>
|
|
||||||
<string name="wavelog_title">Log</string>
|
|
||||||
<string name="wavelog_select_transponder">Select transponder</string>
|
|
||||||
<string name="wavelog_call_hint">Callsign</string>
|
|
||||||
<string name="wavelog_mode_hint">Mode</string>
|
|
||||||
<string name="wavelog_saved">Saved to local log</string>
|
|
||||||
<string name="wavelog_need_config">Configure WaveLog in Settings first</string>
|
|
||||||
<string name="wavelog_empty">No log entries yet</string>
|
|
||||||
<string name="wavelog_grid_mismatch">QTH grid (%1$s) does not match station grid (%2$s). Upload anyway?</string>
|
|
||||||
<string name="wavelog_ignore">Ignore and upload</string>
|
|
||||||
<string name="wavelog_cancel">Cancel</string>
|
|
||||||
<string name="wavelog_copy">Copy</string>
|
|
||||||
<string name="wavelog_delete_undo">Undo</string>
|
|
||||||
<string name="wavelog_col_time">Time</string>
|
|
||||||
<string name="wavelog_col_freq">Freq</string>
|
|
||||||
<string name="wavelog_col_sat">Satellite</string>
|
|
||||||
<string name="wavelog_col_call">Call</string>
|
|
||||||
<string name="wavelog_col_uploaded">Sent</string>
|
|
||||||
<string name="wavelog_ungrouped">Ungrouped</string>
|
|
||||||
<string name="prefs_other_title">Other settings</string>
|
|
||||||
<string name="prefs_ui_title">UI Settings</string>
|
|
||||||
<string name="prefs_ui_order_title">Page order</string>
|
|
||||||
<string name="prefs_ui_main_title">Main menu (bottom bar)</string>
|
|
||||||
<string name="prefs_ui_sub_title">More menu</string>
|
|
||||||
<string name="prefs_ui_move_out">Move to more menu</string>
|
|
||||||
<string name="prefs_ui_move_back">Move back to main menu</string>
|
|
||||||
<string name="prefs_ui_order_locked">Fixed (cannot be moved)</string>
|
|
||||||
<string name="prefs_ui_order_reset">Reset order</string>
|
|
||||||
<string name="prefs_other_switch_utc">Show pass time in UTC</string>
|
|
||||||
<string name="prefs_other_switch_update">Enable automatic data update</string>
|
|
||||||
<string name="prefs_other_switch_sweep">Enable radar sweep animation</string>
|
|
||||||
<string name="prefs_other_switch_sensors">Use sensors to rotate radar view</string>
|
|
||||||
<string name="prefs_other_switch_night_mode">Enable red night mode filter</string>
|
|
||||||
|
|
||||||
<string name="prefs_highlight_title">Elevation highlight</string>
|
<string name="prefs_highlight_title">Elevation highlight</string>
|
||||||
<string name="prefs_highlight_low">Low < %1$d°</string>
|
<string name="prefs_highlight_low">Low < %1$d°</string>
|
||||||
<string name="prefs_highlight_mid">Mid %1$d–%2$d°</string>
|
<string name="prefs_highlight_mid">Mid %1$d–%2$d°</string>
|
||||||
<string name="prefs_highlight_high">High > %1$d°</string>
|
<string name="prefs_highlight_high">High > %1$d°</string>
|
||||||
|
|
||||||
<string name="prefs_outro_title">BG7NTA & the original author would like to thank</string>
|
<string name="prefs_outro_title">I would like to say thanks to</string>
|
||||||
<string name="prefs_outro_thanks" translatable="false">
|
<string name="prefs_outro_thanks" translatable="false">
|
||||||
* Look4Sat users and contributors!
|
* Look4Sat users and contributors!
|
||||||
\n* David A. B. Johnson (predict4java)
|
\n* David A. B. Johnson (predict4java)
|
||||||
@@ -303,8 +232,6 @@
|
|||||||
\n* Dr T.S. Kelso (Celestrak)
|
\n* Dr T.S. Kelso (Celestrak)
|
||||||
\n* Libre Space Foundation (SatNOGS)
|
\n* Libre Space Foundation (SatNOGS)
|
||||||
\n* xdsopl and Robot36 contributors!
|
\n* xdsopl and Robot36 contributors!
|
||||||
\n* BA7OPF (pass matching feature)
|
|
||||||
\n* BG7NTA
|
|
||||||
</string>
|
</string>
|
||||||
<string name="prefs_outro_license">The app comes with no warranty</string>
|
<string name="prefs_outro_license">The app comes with no warranty</string>
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
* Added required tweaks to support Android 17 (API 37)
|
* Added ARISS source, tweaked SSTV sensitivity #225
|
||||||
* Added ACCESS_LOCAL_NETWORK permission check to support network data output
|
* Implemented AOS window and elev filter #226
|
||||||
|
* Added cleartext traffic support for TLEs #227
|
||||||
|
* Fixed OMM import, tweaked error messages #228
|
||||||
|
* Added support for Icom CAT by ruilvo #229
|
||||||
|
* Added tweakable Radar offset by qwqtoday #230
|
||||||
|
* Localized date/time formats by atsunatsu #231
|
||||||
|
* Added linear radio Doppler calc by atsunatsu #232
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import com.android.build.api.dsl.LibraryExtension
|
|
||||||
import com.android.build.api.variant.CanProduceConsumerProguardFiles
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.convention.featurePlugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.rtbishop.look4sat.feature.cw"
|
|
||||||
compileOptions {
|
|
||||||
encoding = "UTF-8"
|
|
||||||
}
|
|
||||||
// 照搬 Morse Expert 1.15: native 解码器仅 armeabi-v7a
|
|
||||||
defaultConfig {
|
|
||||||
ndk {
|
|
||||||
abiFilters += listOf("armeabi-v7a")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CW 类保持规则(JNI 按类名注册 + 照搬混淆类保逻辑), 由 app 的 R8 消费
|
|
||||||
// AGP 9: consumer 规则走 variant 级 CanProduceConsumerProguardFiles
|
|
||||||
androidComponents {
|
|
||||||
onVariants(selector().all()) { variant ->
|
|
||||||
(variant as? CanProduceConsumerProguardFiles)?.consumerProguardFiles?.add(
|
|
||||||
project.layout.projectDirectory.file("proguard-rules.pro")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 照搬的 Java 源码含中文注释, 强制 UTF-8 编译(compileOptions 在部分 AGP 版本不生效)
|
|
||||||
tasks.withType<JavaCompile>().configureEach {
|
|
||||||
options.encoding = "UTF-8"
|
|
||||||
}
|
|
||||||
Vendored
-31
@@ -1,31 +0,0 @@
|
|||||||
# Look4Sat Pro — CW 解码模块(照搬 Morse Expert 1.15)混淆规则
|
|
||||||
# 所有 CW 类保持原名: pas.* 通过 RegisterNatives 按类名注册 JNI(混淆即崩);
|
|
||||||
# 其余为照搬的混淆名类(命名已无混淆意义, 保持以保逻辑完整)。
|
|
||||||
|
|
||||||
# JNI 引擎(RegisterNatives 按类名/方法名查表)
|
|
||||||
-keep class pas.** { *; }
|
|
||||||
|
|
||||||
# 照搬的解码/UI 类(全部保持)
|
|
||||||
-keep class com.ve3nea.morse_expert.** { *; }
|
|
||||||
-keep class H2.** { *; }
|
|
||||||
-keep class I2.b { *; }
|
|
||||||
-keep class J2.** { *; }
|
|
||||||
-keep class k3.** { *; }
|
|
||||||
-keep class i3.** { *; }
|
|
||||||
-keep class g3.** { *; }
|
|
||||||
-keep class j3.** { *; }
|
|
||||||
-keep class s.** { *; }
|
|
||||||
-keep class d1.AbstractC1518b { *; }
|
|
||||||
-keep class B0.b { *; }
|
|
||||||
-keep class B.RunnableC0001b { *; }
|
|
||||||
-keep class D.n { *; }
|
|
||||||
-keep class E2.g { *; }
|
|
||||||
-keep class F2.a { *; }
|
|
||||||
-keep class I0.d { *; }
|
|
||||||
-keep class K1.a { *; }
|
|
||||||
-keep class j1.C1646n { *; }
|
|
||||||
|
|
||||||
# sun.misc stub(Android 无此类, 仅编译期; 防止 R8 异常处理)
|
|
||||||
-dontwarn sun.misc.**
|
|
||||||
-keep class sun.misc.Unsafe { *; }
|
|
||||||
-keep class sun.misc.Cleaner { *; }
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package B;
|
|
||||||
|
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import com.ve3nea.morse_expert.DecodedTextView;
|
|
||||||
import com.ve3nea.morse_expert.MainActivity;
|
|
||||||
import H2.b;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 照搬自 Morse Expert 1.15 B.RunnableC0001b, 类名保持混淆原名。
|
|
||||||
* R8 合并 Runnable 手术: 构造按 smali 真实签名 5 参 (Object,Object,int,int,boolean),
|
|
||||||
* 字段 = i(Object)/h(Object)/g(int)/f(int); run() 仅保留 case 4(f==2 对应 pswitch_4:
|
|
||||||
* 解码文本 UI 更新, H2.b 调用), 其余 case(权限/Intent/通知等库代码)已裁剪。
|
|
||||||
* 注: jadx 4 参构造为错误还原, 已按 smali 修正。
|
|
||||||
*/
|
|
||||||
public final class RunnableC0001b implements Runnable {
|
|
||||||
public final /* synthetic */ int f;
|
|
||||||
|
|
||||||
/* renamed from: g, reason: collision with root package name */
|
|
||||||
public final int g;
|
|
||||||
|
|
||||||
/* renamed from: h, reason: collision with root package name */
|
|
||||||
public final Object h;
|
|
||||||
|
|
||||||
/* renamed from: i, reason: collision with root package name */
|
|
||||||
public final Object i;
|
|
||||||
|
|
||||||
public /* synthetic */ RunnableC0001b(Object obj, Object obj2, int i4, int i5, boolean z) {
|
|
||||||
this.i = obj;
|
|
||||||
this.h = obj2;
|
|
||||||
this.g = i4;
|
|
||||||
this.f = i5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // java.lang.Runnable
|
|
||||||
public final void run() {
|
|
||||||
if (this.f == 2) {
|
|
||||||
MainActivity mainActivity = ((b) this.i).f620p;
|
|
||||||
if (mainActivity != null) {
|
|
||||||
SpannableStringBuilder spannableStringBuilder = (SpannableStringBuilder) this.h;
|
|
||||||
int i5 = this.g;
|
|
||||||
DecodedTextView decodedTextView = (DecodedTextView) mainActivity.f11035D.f11888g;
|
|
||||||
if (!decodedTextView.f11032n && !decodedTextView.f11033o.get()) {
|
|
||||||
decodedTextView.beginBatchEdit();
|
|
||||||
try {
|
|
||||||
int length2 = decodedTextView.length();
|
|
||||||
int min = Math.min(length2, i5);
|
|
||||||
if (min > 0) {
|
|
||||||
decodedTextView.getEditableText().delete(length2 - min, length2);
|
|
||||||
}
|
|
||||||
decodedTextView.q(spannableStringBuilder);
|
|
||||||
decodedTextView.endBatchEdit();
|
|
||||||
return;
|
|
||||||
} catch (Throwable th) {
|
|
||||||
decodedTextView.endBatchEdit();
|
|
||||||
throw th;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package B0;
|
|
||||||
|
|
||||||
import android.widget.TextView;
|
|
||||||
import com.ve3nea.morse_expert.MainActivity;
|
|
||||||
import com.ve3nea.morse_expert.ScaleView;
|
|
||||||
import java.util.Locale;
|
|
||||||
import d1.AbstractC1518b;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 照搬自 Morse Expert 1.15 B0.b, 类名保持混淆原名。
|
|
||||||
* R8 合并 Runnable 手术: 构造按 smali 真实签名 (H2/b, String, [I) -> f=4;
|
|
||||||
* 只保留 case 4(状态栏更新: dBFS/Hz/dB/WPM + ScaleView 频率标记, H2.b 调用),
|
|
||||||
* 其余 case(WorkManager/动画等库代码)已裁剪。
|
|
||||||
* r3 = scaleView.getHeight()(smali 证实, jadx 寄存器未展开)。
|
|
||||||
*/
|
|
||||||
public final class b implements Runnable {
|
|
||||||
public final /* synthetic */ int f;
|
|
||||||
|
|
||||||
/* renamed from: g, reason: collision with root package name */
|
|
||||||
public final Object g;
|
|
||||||
|
|
||||||
/* renamed from: h, reason: collision with root package name */
|
|
||||||
public final Object h;
|
|
||||||
|
|
||||||
/* renamed from: i, reason: collision with root package name */
|
|
||||||
public final Object i;
|
|
||||||
|
|
||||||
public b(H2.b bVar, String str, int[] iArr) {
|
|
||||||
this.f = 4;
|
|
||||||
this.g = bVar;
|
|
||||||
this.h = iArr;
|
|
||||||
this.i = str;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // java.lang.Runnable
|
|
||||||
public final void run() {
|
|
||||||
if (this.f == 4) {
|
|
||||||
MainActivity mainActivity = ((H2.b) this.g).f621q;
|
|
||||||
if (mainActivity != null) {
|
|
||||||
String str2 = (String) this.i;
|
|
||||||
int[] iArr = (int[]) this.h;
|
|
||||||
String concat;
|
|
||||||
String format = String.format(Locale.US, " %3d dBFS", Integer.valueOf(iArr[0]));
|
|
||||||
if (iArr[5] == 0) {
|
|
||||||
concat = format.concat(" Idle ");
|
|
||||||
} else {
|
|
||||||
concat = format.concat(String.format(Locale.US, " %4d Hz %2d dB %2d WPM ", Integer.valueOf(iArr[1]), Integer.valueOf(iArr[2]), Integer.valueOf(iArr[3])));
|
|
||||||
}
|
|
||||||
((TextView) mainActivity.f11035D.f11890i).setText(AbstractC1518b.e(concat, str2));
|
|
||||||
ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h;
|
|
||||||
int i4 = iArr[1];
|
|
||||||
scaleView.f11041g = iArr[5];
|
|
||||||
// r3 = scaleView.getHeight()(smali 证实)
|
|
||||||
scaleView.f = (scaleView.getHeight() - 1) - Math.round(((Math.round(i4 * 0.128f) - H2.b.f606z) * scaleView.getHeight()) / H2.b.f603B);
|
|
||||||
scaleView.invalidate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package D;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.opengl.GLES20;
|
|
||||||
import android.opengl.GLUtils;
|
|
||||||
import com.ve3nea.morse_expert.WaterfallSurfaceView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 照搬自 Morse Expert 1.15, 类名保持混淆原名。
|
|
||||||
* R8 合并 Runnable 手术: 只保留 case 1(瀑布图频谱->OpenGL 纹理, H2.b 调用),
|
|
||||||
* 其余 case(库/广告)已裁剪。
|
|
||||||
*/
|
|
||||||
public final /* synthetic */ class n implements Runnable {
|
|
||||||
public final /* synthetic */ int f;
|
|
||||||
|
|
||||||
/* renamed from: g, reason: collision with root package name */
|
|
||||||
public final /* synthetic */ Object f224g;
|
|
||||||
|
|
||||||
/* renamed from: h, reason: collision with root package name */
|
|
||||||
public final /* synthetic */ Object f225h;
|
|
||||||
|
|
||||||
public /* synthetic */ n(Object obj, int i4, Object obj2) {
|
|
||||||
this.f = i4;
|
|
||||||
this.f224g = obj;
|
|
||||||
this.f225h = obj2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // java.lang.Runnable
|
|
||||||
public final void run() {
|
|
||||||
int i4 = this.f;
|
|
||||||
Object obj = this.f225h;
|
|
||||||
Object obj2 = this.f224g;
|
|
||||||
if (i4 == 1) {
|
|
||||||
float[] fArr = (float[]) obj;
|
|
||||||
J2.b bVar = ((WaterfallSurfaceView) obj2).f;
|
|
||||||
Bitmap bitmap = bVar.f733h;
|
|
||||||
int width = bVar.f730d.getWidth();
|
|
||||||
double d4 = width;
|
|
||||||
double b4 = (bVar.b() + 4.0d) % d4;
|
|
||||||
double d5 = bVar.f737l - b4;
|
|
||||||
if (d5 > d4 / 2.0d) {
|
|
||||||
d5 -= d4;
|
|
||||||
} else if (d5 < (-width) / 2.0d) {
|
|
||||||
d5 += d4;
|
|
||||||
}
|
|
||||||
if (Math.abs(d5) > 8.0d) {
|
|
||||||
bVar.f737l = (int) Math.floor(b4);
|
|
||||||
} else {
|
|
||||||
bVar.f732g = (d5 * 1.0E-5d) + bVar.f732g;
|
|
||||||
}
|
|
||||||
int height = bitmap.getHeight() - 1;
|
|
||||||
for (int i5 = 0; i5 <= height; i5++) {
|
|
||||||
J2.a aVar = bVar.f734i;
|
|
||||||
int round = Math.round((fArr[i5] * 4.0f) + 30.0f);
|
|
||||||
aVar.getClass();
|
|
||||||
bitmap.setPixel(0, height - i5, aVar.f727a[Math.max(0, Math.min(255, round))]);
|
|
||||||
}
|
|
||||||
int i6 = bVar.f737l;
|
|
||||||
GLES20.glBindTexture(3553, bVar.f728a[0]);
|
|
||||||
J2.b.a();
|
|
||||||
GLUtils.texSubImage2D(3553, 0, i6, 0, bitmap);
|
|
||||||
J2.b.a();
|
|
||||||
int i7 = bVar.f737l + 1;
|
|
||||||
bVar.f737l = i7;
|
|
||||||
bVar.f737l = i7 % width;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package E2;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import com.ve3nea.morse_expert.MainActivity;
|
|
||||||
import com.ve3nea.morse_expert.ScaleView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 照搬自 Morse Expert 1.15 (com.ve3nea.morse_expert), 类名保持混淆原名。
|
|
||||||
* R8 合并类手术: 只保留 case 1(瀑布图触摸选频率=应用逻辑),
|
|
||||||
* 其余 case(广告 P4 / appcompat 焦点 / 弹窗)为库代码, 已裁剪。
|
|
||||||
*/
|
|
||||||
public final class g implements View.OnTouchListener {
|
|
||||||
public final /* synthetic */ int f;
|
|
||||||
|
|
||||||
/* renamed from: g, reason: collision with root package name */
|
|
||||||
public final /* synthetic */ Object f411g;
|
|
||||||
|
|
||||||
public /* synthetic */ g(Object obj, int i4) {
|
|
||||||
this.f = i4;
|
|
||||||
this.f411g = obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // android.view.View.OnTouchListener
|
|
||||||
public final boolean onTouch(View view, MotionEvent motionEvent) {
|
|
||||||
int i4 = this.f;
|
|
||||||
Object obj = this.f411g;
|
|
||||||
if (i4 == 1) {
|
|
||||||
MainActivity mainActivity = (MainActivity) obj;
|
|
||||||
if (motionEvent.getActionMasked() == 0 && mainActivity.f11037F != null) {
|
|
||||||
// smali 证实: p1 先被重赋值为 f11035D.h(ScaleView)再 getHeight;
|
|
||||||
// 公式 = round((f606z + ((scaleH-1-y)*f603B/scaleH)) / 0.128f)
|
|
||||||
ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h;
|
|
||||||
float y3 = motionEvent.getY();
|
|
||||||
int round = Math.round((H2.b.f606z + ((((scaleView.getHeight() - 1) - y3) * H2.b.f603B) / scaleView.getHeight())) / 0.128f);
|
|
||||||
mainActivity.f11037F.f622r.set(round);
|
|
||||||
Log.i("Waterfall", String.format("Touch at %d Hz`", Integer.valueOf(round)));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package F2;
|
|
||||||
|
|
||||||
import com.rtbishop.look4sat.feature.cw.R;
|
|
||||||
import android.view.ActionMode;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import com.ve3nea.morse_expert.DecodedTextView;
|
|
||||||
|
|
||||||
/* loaded from: classes.dex */
|
|
||||||
public final class a implements ActionMode.Callback {
|
|
||||||
|
|
||||||
/* renamed from: a, reason: collision with root package name */
|
|
||||||
public final /* synthetic */ DecodedTextView f564a;
|
|
||||||
|
|
||||||
public a(DecodedTextView decodedTextView) {
|
|
||||||
this.f564a = decodedTextView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // android.view.ActionMode.Callback
|
|
||||||
public final boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
|
|
||||||
int itemId = menuItem.getItemId();
|
|
||||||
DecodedTextView decodedTextView = this.f564a;
|
|
||||||
if (itemId != 73) {
|
|
||||||
if (itemId != 74) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
decodedTextView.s();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
decodedTextView.r();
|
|
||||||
actionMode.finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // android.view.ActionMode.Callback
|
|
||||||
public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
|
|
||||||
this.f564a.f11032n = true;
|
|
||||||
// 原: if (menu.findItem(R.id.shareText) == null) 检查(shareText 为原 APK 资源, 简化后始终添加)
|
|
||||||
menu.add(0, 74, 0, "Share").setIcon(R.drawable.ic_baseline_share_24);
|
|
||||||
menu.add(0, 73, 0, "Save").setIcon(R.drawable.ic_baseline_save_24);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // android.view.ActionMode.Callback
|
|
||||||
public final void onDestroyActionMode(ActionMode actionMode) {
|
|
||||||
this.f564a.f11032n = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // android.view.ActionMode.Callback
|
|
||||||
public final boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package H2;
|
|
||||||
|
|
||||||
import android.media.AudioRecord;
|
|
||||||
import android.util.Log;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/* loaded from: classes.dex */
|
|
||||||
public final class a implements Runnable {
|
|
||||||
public final int f;
|
|
||||||
|
|
||||||
/* renamed from: g, reason: collision with root package name */
|
|
||||||
public final AudioRecord f596g;
|
|
||||||
|
|
||||||
/* renamed from: h, reason: collision with root package name */
|
|
||||||
public final ExecutorService f597h;
|
|
||||||
|
|
||||||
/* renamed from: i, reason: collision with root package name */
|
|
||||||
public final AtomicBoolean f598i;
|
|
||||||
|
|
||||||
/* renamed from: j, reason: collision with root package name */
|
|
||||||
public final short[] f599j;
|
|
||||||
|
|
||||||
/* renamed from: k, reason: collision with root package name */
|
|
||||||
public final float[] f600k;
|
|
||||||
|
|
||||||
/* renamed from: l, reason: collision with root package name */
|
|
||||||
public b f601l;
|
|
||||||
|
|
||||||
public a() {
|
|
||||||
int minBufferSize = AudioRecord.getMinBufferSize(8000, 16, 2);
|
|
||||||
int i4 = b.f604C;
|
|
||||||
this.f = i4;
|
|
||||||
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
|
|
||||||
this.f597h = newSingleThreadExecutor;
|
|
||||||
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
|
|
||||||
this.f598i = atomicBoolean;
|
|
||||||
this.f599j = new short[i4];
|
|
||||||
this.f600k = new float[i4];
|
|
||||||
AudioRecord audioRecord = new AudioRecord(1, 8000, 16, 2, minBufferSize * 4);
|
|
||||||
this.f596g = audioRecord;
|
|
||||||
if (audioRecord.getState() == 1) {
|
|
||||||
audioRecord.startRecording();
|
|
||||||
if (audioRecord.getState() != 1) {
|
|
||||||
Log.e("AudioRecord", "cannot start");
|
|
||||||
}
|
|
||||||
atomicBoolean.set(true);
|
|
||||||
newSingleThreadExecutor.execute(this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Log.e("AudioInput", "Failed to initialize");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // java.lang.Runnable
|
|
||||||
public final void run() {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
if (this.f598i.get()) {
|
|
||||||
float[] fArr = this.f600k;
|
|
||||||
AudioRecord audioRecord = this.f596g;
|
|
||||||
short[] sArr = this.f599j;
|
|
||||||
int i4 = this.f;
|
|
||||||
if (audioRecord.read(sArr, 0, i4) == i4) {
|
|
||||||
for (int i5 = 0; i5 < i4; i5++) {
|
|
||||||
fArr[i5] = sArr[i5] / 32768.0f;
|
|
||||||
}
|
|
||||||
b bVar = this.f601l;
|
|
||||||
if (bVar != null) {
|
|
||||||
bVar.a(fArr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TimeUnit.MILLISECONDS.sleep(100L);
|
|
||||||
}
|
|
||||||
} catch (Exception e4) {
|
|
||||||
Log.e("Audio thread", "Exception: ", e4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,582 +0,0 @@
|
|||||||
package H2;
|
|
||||||
|
|
||||||
import B.RunnableC0001b;
|
|
||||||
import D.n;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.media.MediaScannerConnection;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.format.DateFormat;
|
|
||||||
import android.text.style.ForegroundColorSpan;
|
|
||||||
import android.util.Log;
|
|
||||||
import com.ve3nea.morse_expert.MainActivity;
|
|
||||||
import com.ve3nea.morse_expert.WaterfallSurfaceView;
|
|
||||||
import i3.d;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import k3.s;
|
|
||||||
import pas.decoder;
|
|
||||||
import pas.nativedecoder;
|
|
||||||
|
|
||||||
/* loaded from: classes.dex */
|
|
||||||
public final class b {
|
|
||||||
|
|
||||||
/* renamed from: A, reason: collision with root package name */
|
|
||||||
public static final int f602A;
|
|
||||||
|
|
||||||
/* renamed from: B, reason: collision with root package name */
|
|
||||||
public static final int f603B;
|
|
||||||
|
|
||||||
/* renamed from: C, reason: collision with root package name */
|
|
||||||
public static final int f604C;
|
|
||||||
|
|
||||||
/* renamed from: D, reason: collision with root package name */
|
|
||||||
public static final d f605D;
|
|
||||||
|
|
||||||
/* renamed from: z, reason: collision with root package name */
|
|
||||||
public static final int f606z;
|
|
||||||
|
|
||||||
/* renamed from: a, reason: collision with root package name */
|
|
||||||
public final float[] f607a;
|
|
||||||
|
|
||||||
/* renamed from: b, reason: collision with root package name */
|
|
||||||
public final float[] f608b;
|
|
||||||
public final float[] c;
|
|
||||||
|
|
||||||
/* renamed from: d, reason: collision with root package name */
|
|
||||||
public final float[] f609d;
|
|
||||||
|
|
||||||
/* renamed from: e, reason: collision with root package name */
|
|
||||||
public final nativedecoder.TNativeDecoder f610e;
|
|
||||||
public int f;
|
|
||||||
|
|
||||||
/* renamed from: g, reason: collision with root package name */
|
|
||||||
public int f611g;
|
|
||||||
|
|
||||||
/* renamed from: h, reason: collision with root package name */
|
|
||||||
public int f612h;
|
|
||||||
|
|
||||||
/* renamed from: i, reason: collision with root package name */
|
|
||||||
public decoder.TDecoderState f613i;
|
|
||||||
|
|
||||||
/* renamed from: j, reason: collision with root package name */
|
|
||||||
public String f614j;
|
|
||||||
|
|
||||||
/* renamed from: k, reason: collision with root package name */
|
|
||||||
public final c f615k;
|
|
||||||
|
|
||||||
/* renamed from: l, reason: collision with root package name */
|
|
||||||
public int f616l;
|
|
||||||
|
|
||||||
/* renamed from: m, reason: collision with root package name */
|
|
||||||
public final I2.b f617m;
|
|
||||||
|
|
||||||
/* renamed from: n, reason: collision with root package name */
|
|
||||||
public final Handler f618n;
|
|
||||||
|
|
||||||
/* renamed from: o, reason: collision with root package name */
|
|
||||||
public WaterfallSurfaceView f619o;
|
|
||||||
|
|
||||||
/* renamed from: p, reason: collision with root package name */
|
|
||||||
public MainActivity f620p;
|
|
||||||
|
|
||||||
/* renamed from: q, reason: collision with root package name */
|
|
||||||
public MainActivity f621q;
|
|
||||||
|
|
||||||
/* renamed from: r, reason: collision with root package name */
|
|
||||||
public final AtomicInteger f622r;
|
|
||||||
|
|
||||||
/* renamed from: s, reason: collision with root package name */
|
|
||||||
public final AtomicBoolean f623s;
|
|
||||||
|
|
||||||
/* renamed from: t, reason: collision with root package name */
|
|
||||||
public final Context f624t;
|
|
||||||
|
|
||||||
/* renamed from: u, reason: collision with root package name */
|
|
||||||
public int f625u;
|
|
||||||
|
|
||||||
/* renamed from: v, reason: collision with root package name */
|
|
||||||
public final AtomicBoolean f626v;
|
|
||||||
|
|
||||||
/* renamed from: w, reason: collision with root package name */
|
|
||||||
public DataOutputStream f627w;
|
|
||||||
|
|
||||||
/* renamed from: x, reason: collision with root package name */
|
|
||||||
public int f628x;
|
|
||||||
|
|
||||||
/* renamed from: y, reason: collision with root package name */
|
|
||||||
public String f629y;
|
|
||||||
|
|
||||||
/* JADX WARN: Removed duplicated region for block: B:14:0x012d */
|
|
||||||
/* JADX WARN: Type inference failed for: r0v4, types: [i3.d, java.lang.Object] */
|
|
||||||
static {
|
|
||||||
boolean z3;
|
|
||||||
float f;
|
|
||||||
float f4;
|
|
||||||
float f5;
|
|
||||||
int round = Math.round(25.6f);
|
|
||||||
f606z = round;
|
|
||||||
int round2 = Math.round(153.6f);
|
|
||||||
f602A = round2;
|
|
||||||
f603B = round2 - round;
|
|
||||||
f604C = Math.round(85.333336f);
|
|
||||||
i3.d obj = new i3.d();
|
|
||||||
if (2048 > 1073741824) {
|
|
||||||
z3 = true;
|
|
||||||
} else {
|
|
||||||
z3 = false;
|
|
||||||
}
|
|
||||||
obj.f11709l = z3;
|
|
||||||
int i4 = (int) 1024;
|
|
||||||
obj.f11700a = i4;
|
|
||||||
obj.f11701b = 1024L;
|
|
||||||
if (!z3) {
|
|
||||||
obj.f11708k = 1;
|
|
||||||
int[] iArr = new int[((int) g3.c.a((1 << (((int) (g3.c.d(((float) 1024) + 0.5f) / g3.c.d(2.0d))) / 2)) + 2)) + 2];
|
|
||||||
obj.c = iArr;
|
|
||||||
float[] fArr = new float[i4];
|
|
||||||
obj.f11703e = fArr;
|
|
||||||
int i5 = (i4 * 2) >> 2;
|
|
||||||
obj.f11704g = i5;
|
|
||||||
iArr[0] = i5;
|
|
||||||
iArr[1] = 1;
|
|
||||||
if (i5 > 2) {
|
|
||||||
int i6 = i5 >> 1;
|
|
||||||
float f6 = 0.7853982f / i6;
|
|
||||||
float f7 = 2.0f * f6;
|
|
||||||
float b4 = (float) g3.c.b(i6 * f6); // r9 = i6 (smali)
|
|
||||||
fArr[0] = 1.0f;
|
|
||||||
fArr[1] = b4;
|
|
||||||
if (i6 == 4) {
|
|
||||||
double d4 = f7;
|
|
||||||
f4 = 0.5f;
|
|
||||||
fArr[2] = (float) g3.c.b(d4);
|
|
||||||
fArr[3] = (float) g3.c.f(d4);
|
|
||||||
} else {
|
|
||||||
f4 = 0.5f;
|
|
||||||
if (i6 > 4) {
|
|
||||||
iArr[2] = 0;
|
|
||||||
iArr[3] = 16;
|
|
||||||
int i7 = i5;
|
|
||||||
int i8 = 2;
|
|
||||||
f5 = 1.0f;
|
|
||||||
while (i7 > 32) {
|
|
||||||
int i9 = i8 << 1;
|
|
||||||
int i10 = i8 << 4;
|
|
||||||
for (int i11 = i8; i11 < i9; i11++) {
|
|
||||||
int i12 = iArr[i11] << 2;
|
|
||||||
iArr[i8 + i11] = i12;
|
|
||||||
iArr[i9 + i11] = i12 + i10;
|
|
||||||
}
|
|
||||||
i7 >>= 2;
|
|
||||||
i8 = i9;
|
|
||||||
}
|
|
||||||
fArr[2] = 0.5f / ((float) g3.c.b(f7));
|
|
||||||
fArr[3] = 0.5f / ((float) g3.c.b(6.0f * f6));
|
|
||||||
for (int i13 = 4; i13 < i6; i13 += 4) {
|
|
||||||
float f8 = i13 * f6;
|
|
||||||
double d5 = f8;
|
|
||||||
fArr[i13] = (float) g3.c.b(d5);
|
|
||||||
fArr[i13 + 1] = (float) g3.c.f(d5);
|
|
||||||
double d6 = 3.0f * f8;
|
|
||||||
fArr[i13 + 2] = (float) g3.c.b(d6);
|
|
||||||
fArr[i13 + 3] = -((float) g3.c.f(d6));
|
|
||||||
}
|
|
||||||
int i14 = 0;
|
|
||||||
while (i6 > 2) {
|
|
||||||
int i15 = i14 + i6;
|
|
||||||
i6 >>= 1;
|
|
||||||
fArr[i15] = f5;
|
|
||||||
fArr[i15 + 1] = b4;
|
|
||||||
if (i6 == 4) {
|
|
||||||
float f9 = fArr[i14 + 4];
|
|
||||||
float f10 = fArr[i14 + 5];
|
|
||||||
fArr[i15 + 2] = f9;
|
|
||||||
fArr[i15 + 3] = f10;
|
|
||||||
} else if (i6 > 4) {
|
|
||||||
float f11 = fArr[i14 + 4];
|
|
||||||
float f12 = fArr[i14 + 6];
|
|
||||||
fArr[i15 + 2] = f4 / f11;
|
|
||||||
fArr[i15 + 3] = f4 / f12;
|
|
||||||
for (int i16 = 4; i16 < i6; i16 += 4) {
|
|
||||||
int i17 = (i16 * 2) + i14;
|
|
||||||
int i18 = i15 + i16;
|
|
||||||
float f13 = fArr[i17];
|
|
||||||
float f14 = fArr[i17 + 1];
|
|
||||||
float f15 = fArr[i17 + 2];
|
|
||||||
float f16 = fArr[i17 + 3];
|
|
||||||
fArr[i18] = f13;
|
|
||||||
fArr[i18 + 1] = f14;
|
|
||||||
fArr[i18 + 2] = f15;
|
|
||||||
fArr[i18 + 3] = f16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i14 = i15;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f5 = 1.0f;
|
|
||||||
int i142 = 0;
|
|
||||||
while (i6 > 2) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int i19 = i4 >> 2;
|
|
||||||
obj.f11706i = i19;
|
|
||||||
iArr[1] = i19;
|
|
||||||
if (i19 > 1) {
|
|
||||||
int i20 = i19 >> 1;
|
|
||||||
float f17 = 0.7853982f / i20;
|
|
||||||
float b5 = (float) g3.c.b(i20 * f17); // r6 = i20 (smali)
|
|
||||||
fArr[i5] = b5;
|
|
||||||
fArr[i5 + i20] = b5 * 0.5f;
|
|
||||||
for (int i21 = 1; i21 < i20; i21++) {
|
|
||||||
double d7 = i21 * f17;
|
|
||||||
fArr[i5 + i21] = ((float) g3.c.b(d7)) * 0.5f;
|
|
||||||
fArr[(i5 + i19) - i21] = ((float) g3.c.f(d7)) * 0.5f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
obj.f11708k = 1;
|
|
||||||
s sVar = new s(((long) g3.c.a((1 << ((int) (((long) (g3.c.d(((float) 1024) + 0.5f) / g3.c.d(2.0d))) / 2))) + 2)) + 2, true);
|
|
||||||
obj.f11702d = sVar;
|
|
||||||
k3.d dVar = new k3.d(1024L, true);
|
|
||||||
obj.f = dVar;
|
|
||||||
obj.f11705h = 512L;
|
|
||||||
sVar.c(0L, 512L);
|
|
||||||
sVar.c(1L, 1L);
|
|
||||||
if (512 > 2) {
|
|
||||||
long j4 = 512 >> 1;
|
|
||||||
float f18 = 0.7853982f / ((float) j4);
|
|
||||||
float f19 = f18 * 2.0f;
|
|
||||||
float b6 = (float) g3.c.b(j4 * f18); // r14 = j4 (smali 同模式)
|
|
||||||
dVar.c(0L, 1.0f);
|
|
||||||
dVar.c(1L, b6);
|
|
||||||
if (j4 == 4) {
|
|
||||||
f = 0.5f;
|
|
||||||
double d8 = f19;
|
|
||||||
dVar.c(2L, (float) g3.c.b(d8));
|
|
||||||
dVar.c(3L, (float) g3.c.f(d8));
|
|
||||||
} else {
|
|
||||||
f = 0.5f;
|
|
||||||
if (j4 > 4) {
|
|
||||||
sVar.c(2L, 0L);
|
|
||||||
sVar.c(3L, 16L);
|
|
||||||
long j5 = 2;
|
|
||||||
long j6 = 512;
|
|
||||||
while (j6 > 32) {
|
|
||||||
long j7 = j5 << 1;
|
|
||||||
long j8 = j5 << 4;
|
|
||||||
long j9 = j5;
|
|
||||||
while (j9 < j7) {
|
|
||||||
long b7 = sVar.b(j9) << 2;
|
|
||||||
sVar.c(j5 + j9, b7);
|
|
||||||
sVar.c(j7 + j9, b7 + j8);
|
|
||||||
j9++;
|
|
||||||
f19 = f19;
|
|
||||||
}
|
|
||||||
j6 >>= 2;
|
|
||||||
j5 = j7;
|
|
||||||
}
|
|
||||||
dVar.c(2L, 0.5f / ((float) g3.c.b(f19)));
|
|
||||||
dVar.c(3L, 0.5f / ((float) g3.c.b(6.0f * f18)));
|
|
||||||
long j10 = 4;
|
|
||||||
while (j10 < j4) {
|
|
||||||
float f20 = ((float) j10) * f18;
|
|
||||||
double d9 = f20;
|
|
||||||
dVar.c(j10, (float) g3.c.b(d9));
|
|
||||||
dVar.c(j10 + 1, (float) g3.c.f(d9));
|
|
||||||
double d10 = 3.0f * f20;
|
|
||||||
long j11 = j10;
|
|
||||||
dVar.c(j10 + 2, (float) g3.c.b(d10));
|
|
||||||
dVar.c(j11 + 3, -((float) g3.c.f(d10)));
|
|
||||||
j10 = j11 + 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
long j12 = 0;
|
|
||||||
long j13 = 2;
|
|
||||||
while (j4 > j13) {
|
|
||||||
long j14 = j12 + j4;
|
|
||||||
j4 >>= 1;
|
|
||||||
dVar.c(j14, 1.0f);
|
|
||||||
dVar.c(j14 + 1, b6);
|
|
||||||
if (j4 == 4) {
|
|
||||||
float b8 = dVar.b(j12 + 4);
|
|
||||||
float b9 = dVar.b(j12 + 5);
|
|
||||||
j13 = 2;
|
|
||||||
dVar.c(j14 + 2, b8);
|
|
||||||
dVar.c(j14 + 3, b9);
|
|
||||||
} else {
|
|
||||||
j13 = 2;
|
|
||||||
if (j4 > 4) {
|
|
||||||
float b10 = dVar.b(j12 + 4);
|
|
||||||
float b11 = dVar.b(6 + j12);
|
|
||||||
dVar.c(j14 + 2, f / b10);
|
|
||||||
dVar.c(j14 + 3, f / b11);
|
|
||||||
long j15 = 4;
|
|
||||||
while (j15 < j4) {
|
|
||||||
long j16 = (j15 * 2) + j12;
|
|
||||||
float f21 = b6;
|
|
||||||
long j17 = j14 + j15;
|
|
||||||
long j18 = j12;
|
|
||||||
float b12 = dVar.b(j16);
|
|
||||||
long j19 = j14;
|
|
||||||
float b13 = dVar.b(j16 + 1);
|
|
||||||
float b14 = dVar.b(j16 + 2);
|
|
||||||
float b15 = dVar.b(j16 + 3);
|
|
||||||
dVar.c(j17, b12);
|
|
||||||
dVar.c(j17 + 1, b13);
|
|
||||||
dVar.c(j17 + 2, b14);
|
|
||||||
dVar.c(j17 + 3, b15);
|
|
||||||
j15 += 4;
|
|
||||||
b6 = f21;
|
|
||||||
j12 = j18;
|
|
||||||
j14 = j19;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b6 = b6;
|
|
||||||
j12 = j14;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
long j20 = 256;
|
|
||||||
obj.f11707j = 256L;
|
|
||||||
long j21 = 1;
|
|
||||||
sVar.c(1L, 256L);
|
|
||||||
if (256 > 1) {
|
|
||||||
long j22 = 256 >> 1;
|
|
||||||
float f22 = 0.7853982f / ((float) j22);
|
|
||||||
long j23 = 512;
|
|
||||||
dVar.c(512L, (float) g3.c.b(j22 * f22)); // r1 = j22 (smali)
|
|
||||||
dVar.c(512 + j22, dVar.b(512L) * 0.5f);
|
|
||||||
long j24 = 1;
|
|
||||||
while (j24 < j22) {
|
|
||||||
long j25 = j20;
|
|
||||||
long j26 = j21;
|
|
||||||
double d11 = ((float) j24) * f22;
|
|
||||||
long j27 = j23;
|
|
||||||
dVar.c(j23 + j24, ((float) g3.c.b(d11)) * 0.5f);
|
|
||||||
dVar.c((j27 + j25) - j24, ((float) g3.c.f(d11)) * 0.5f);
|
|
||||||
j24 += j26;
|
|
||||||
j20 = j25;
|
|
||||||
j21 = j26;
|
|
||||||
j23 = j27;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f605D = obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
public b(Context context) {
|
|
||||||
float[] fArr = new float[1024];
|
|
||||||
fArr[512] = 1.0f;
|
|
||||||
for (int i4 = 1; i4 < 512; i4++) {
|
|
||||||
double d4 = (i4 * 6.283185307179586d) / 1024;
|
|
||||||
float cos = (float) ((Math.cos(d4 * 2.0d) * 0.08d) + (0.42d - (Math.cos(d4) * 0.5d)));
|
|
||||||
fArr[i4] = cos;
|
|
||||||
double d5 = 0.003125f * 6.283185307179586d * (i4 - 512);
|
|
||||||
float sin = (float) ((Math.sin(d5) / d5) * cos);
|
|
||||||
fArr[i4] = sin;
|
|
||||||
fArr[1024 - i4] = sin;
|
|
||||||
}
|
|
||||||
this.f607a = fArr;
|
|
||||||
this.f608b = new float[1024];
|
|
||||||
this.c = new float[1024];
|
|
||||||
this.f609d = new float[f603B];
|
|
||||||
this.f610e = nativedecoder.TNativeDecoder.Create();
|
|
||||||
this.f614j = "";
|
|
||||||
this.f615k = new c(4000, 0);
|
|
||||||
I2.b bVar = new I2.b();
|
|
||||||
this.f617m = bVar;
|
|
||||||
this.f618n = new Handler(Looper.getMainLooper());
|
|
||||||
this.f622r = new AtomicInteger();
|
|
||||||
this.f623s = new AtomicBoolean(false);
|
|
||||||
this.f625u = 0;
|
|
||||||
this.f626v = new AtomicBoolean(false);
|
|
||||||
this.f628x = 0;
|
|
||||||
this.f624t = context;
|
|
||||||
bVar.b(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* JADX WARN: Removed duplicated region for block: B:57:0x01e2 */
|
|
||||||
/* JADX WARN: Removed duplicated region for block: B:63:0x020a A[LOOP:6: B:62:0x0208->B:63:0x020a, LOOP_END] */
|
|
||||||
/* JADX WARN: Removed duplicated region for block: B:66:0x0216 A[LOOP:7: B:65:0x0214->B:66:0x0216, LOOP_END] */
|
|
||||||
/* JADX WARN: Removed duplicated region for block: B:70:0x0232 */
|
|
||||||
/* JADX WARN: Removed duplicated region for block: B:73:? A[RETURN, SYNTHETIC] */
|
|
||||||
/*
|
|
||||||
Code decompiled incorrectly, please refer to instructions dump.
|
|
||||||
*/
|
|
||||||
public final void a(float[] fArr) {
|
|
||||||
c cVar;
|
|
||||||
float[] fArr2;
|
|
||||||
float[] fArr3;
|
|
||||||
b bVar;
|
|
||||||
Exception exc;
|
|
||||||
int i4;
|
|
||||||
float f;
|
|
||||||
int i5;
|
|
||||||
WaterfallSurfaceView waterfallSurfaceView;
|
|
||||||
int length = fArr.length;
|
|
||||||
int i6 = 0;
|
|
||||||
while (true) {
|
|
||||||
cVar = this.f615k;
|
|
||||||
if (i6 >= length) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
cVar.c(Math.abs(fArr[i6]));
|
|
||||||
i6++;
|
|
||||||
}
|
|
||||||
this.f616l = Math.round(((float) Math.log10(Math.max(-0.5d, cVar.f633e))) * 20.0f);
|
|
||||||
int i7 = f604C;
|
|
||||||
int i8 = 1024 - i7;
|
|
||||||
float[] fArr4 = this.f608b;
|
|
||||||
System.arraycopy(fArr4, i7, fArr4, 0, i8);
|
|
||||||
System.arraycopy(fArr, 0, fArr4, i8, i7);
|
|
||||||
int i9 = 0;
|
|
||||||
while (true) {
|
|
||||||
fArr2 = this.c;
|
|
||||||
if (i9 >= 1024) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
fArr2[i9] = fArr4[i9] * this.f607a[i9];
|
|
||||||
i9++;
|
|
||||||
}
|
|
||||||
f605D.k(fArr2, 0);
|
|
||||||
int i10 = f606z;
|
|
||||||
int i11 = i10;
|
|
||||||
while (true) {
|
|
||||||
int i12 = f602A;
|
|
||||||
float f4 = 0.0f;
|
|
||||||
fArr3 = this.f609d;
|
|
||||||
if (i11 < i12) {
|
|
||||||
int i13 = i11 * 2;
|
|
||||||
float f5 = fArr2[i13];
|
|
||||||
float f6 = fArr2[i13 + 1];
|
|
||||||
float f7 = ((f6 * f6) + (f5 * f5)) * 0.001f;
|
|
||||||
if (!Float.isNaN(f7)) {
|
|
||||||
f4 = f7;
|
|
||||||
}
|
|
||||||
fArr3[i11 - i10] = f4;
|
|
||||||
i11++;
|
|
||||||
} else {
|
|
||||||
// smali: 循环内无 try; 录音写入 try 在循环后(见下)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DataOutputStream dataOutputStream = this.f627w;
|
|
||||||
Context context = this.f624t;
|
|
||||||
AtomicBoolean atomicBoolean = this.f626v;
|
|
||||||
try { // smali try_start_0: 录音写入整块(原 jadx 误把 try 放 break 处)
|
|
||||||
if (dataOutputStream == null && atomicBoolean.get()) {
|
|
||||||
File file = new File(context.getExternalFilesDir(null), ((String) DateFormat.format("yyyy-MM-dd_HH_mm_ss", new Date())) + "_spectra.bin");
|
|
||||||
this.f629y = file.getAbsoluteFile().toString();
|
|
||||||
this.f627w = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file, false)));
|
|
||||||
this.f628x = 0;
|
|
||||||
this.f614j = " Recording...";
|
|
||||||
}
|
|
||||||
if (this.f627w != null) {
|
|
||||||
for (float f8 : fArr3) {
|
|
||||||
this.f627w.writeFloat(f8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (atomicBoolean.get()) {
|
|
||||||
int i14 = this.f628x + 1;
|
|
||||||
this.f628x = i14;
|
|
||||||
if (i14 >= 2048) {
|
|
||||||
atomicBoolean.set(false);
|
|
||||||
this.f627w.close();
|
|
||||||
this.f627w = null;
|
|
||||||
this.f614j = "";
|
|
||||||
MediaScannerConnection.scanFile(context, new String[]{this.f629y}, new String[]{"application/x-binary"}, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e4) {
|
|
||||||
Log.e("File write", "Exception: ", e4);
|
|
||||||
}
|
|
||||||
nativedecoder.TNativeDecoder tNativeDecoder = this.f610e;
|
|
||||||
try {
|
|
||||||
this.f613i = tNativeDecoder.ProcessSpectrum(fArr3);
|
|
||||||
int i15 = this.f625u + 1;
|
|
||||||
this.f625u = i15;
|
|
||||||
Handler handler = this.f618n;
|
|
||||||
if (i15 == 15) {
|
|
||||||
try {
|
|
||||||
this.f625u = 0;
|
|
||||||
this.f = tNativeDecoder.getPitch();
|
|
||||||
this.f611g = tNativeDecoder.getSnr();
|
|
||||||
this.f612h = tNativeDecoder.getWpm();
|
|
||||||
handler.post(new B0.b(this, this.f614j, new int[]{this.f616l, this.f, this.f611g, this.f612h, tNativeDecoder.getSignalCount(), this.f613i.Value}));
|
|
||||||
AtomicInteger atomicInteger = this.f622r;
|
|
||||||
int i16 = atomicInteger.get();
|
|
||||||
if (i16 != 0) {
|
|
||||||
tNativeDecoder.LockToPitch(i16);
|
|
||||||
atomicInteger.set(0);
|
|
||||||
}
|
|
||||||
tNativeDecoder.setHamMode(this.f623s.get());
|
|
||||||
} catch (Exception e5) {
|
|
||||||
// jadx 幻觉区(空循环/空 if 为 catch 错位还原)已裁剪; 原 smali: 记录异常后跳回正常流程尾部
|
|
||||||
exc = e5;
|
|
||||||
bVar = this;
|
|
||||||
Log.e("Audio Processor", "Exception", exc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int charCount = tNativeDecoder.getCharCount();
|
|
||||||
if (charCount > 0) {
|
|
||||||
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
|
|
||||||
int i18 = 0;
|
|
||||||
while (i18 < charCount) {
|
|
||||||
decoder.TCharInfo GetChar = tNativeDecoder.GetChar(i18);
|
|
||||||
spannableStringBuilder.append(GetChar.getChar());
|
|
||||||
ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(this.f617m.a(GetChar));
|
|
||||||
int i19 = i18 + 1;
|
|
||||||
spannableStringBuilder.setSpan(foregroundColorSpan, i18, i19, 33);
|
|
||||||
i18 = i19;
|
|
||||||
}
|
|
||||||
bVar = this;
|
|
||||||
try {
|
|
||||||
handler.post(new RunnableC0001b(bVar, spannableStringBuilder, tNativeDecoder.getDeleteCount(), 2, false));
|
|
||||||
} catch (Exception e6) {
|
|
||||||
// jadx 幻觉区已裁剪(同 catch e5); 原 smali: 记录异常后跳回正常流程尾部
|
|
||||||
exc = e6;
|
|
||||||
Log.e("Audio Processor", "Exception", exc);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bVar = this;
|
|
||||||
}
|
|
||||||
} catch (Exception e7) {
|
|
||||||
// jadx 幻觉(e 变量)已裁剪
|
|
||||||
bVar = this;
|
|
||||||
}
|
|
||||||
i4 = f603B;
|
|
||||||
int i1722 = i4 / 2;
|
|
||||||
float[] copyOfRange22 = Arrays.copyOfRange(fArr3, i1722 - 20, i1722 + 20);
|
|
||||||
Arrays.sort(copyOfRange22);
|
|
||||||
f = copyOfRange22[20];
|
|
||||||
if (f > 0.0f) {
|
|
||||||
for (int i20 = 0; i20 < i4; i20++) {
|
|
||||||
fArr3[i20] = fArr3[i20] / f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
float[] fArr522 = (float[]) fArr3.clone();
|
|
||||||
new c(12, 1).a(fArr522);
|
|
||||||
new c(12, 0).a(fArr522);
|
|
||||||
for (i5 = 0; i5 < i4; i5++) {
|
|
||||||
fArr3[i5] = fArr3[i5] - fArr522[i5];
|
|
||||||
}
|
|
||||||
for (int i21 = 0; i21 < i4; i21++) {
|
|
||||||
fArr3[i21] = ((float) Math.log10(Math.max(-0.5d, fArr3[i21]) + 1.0d)) * 10.0f;
|
|
||||||
}
|
|
||||||
waterfallSurfaceView = bVar.f619o;
|
|
||||||
if (waterfallSurfaceView != null) {
|
|
||||||
waterfallSurfaceView.queueEvent(new n(waterfallSurfaceView, 1, (float[]) fArr3.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package H2;
|
|
||||||
|
|
||||||
/* loaded from: classes.dex */
|
|
||||||
public final class c {
|
|
||||||
|
|
||||||
/* renamed from: a, reason: collision with root package name */
|
|
||||||
public final int f630a;
|
|
||||||
|
|
||||||
/* renamed from: b, reason: collision with root package name */
|
|
||||||
public float[] f631b;
|
|
||||||
public int c;
|
|
||||||
|
|
||||||
/* renamed from: d, reason: collision with root package name */
|
|
||||||
public float f632d;
|
|
||||||
|
|
||||||
/* renamed from: e, reason: collision with root package name */
|
|
||||||
public float f633e;
|
|
||||||
public final /* synthetic */ int f;
|
|
||||||
|
|
||||||
public c(int i4, int i5) {
|
|
||||||
this.f = i5;
|
|
||||||
this.f630a = i4 - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void a(float[] fArr) {
|
|
||||||
int i4 = this.f630a / 2;
|
|
||||||
int length = fArr.length;
|
|
||||||
if (length > i4) {
|
|
||||||
this.f631b = null;
|
|
||||||
for (int i5 = 0; i5 < i4; i5++) {
|
|
||||||
c(fArr[i5]);
|
|
||||||
}
|
|
||||||
for (int i6 = i4; i6 < length; i6++) {
|
|
||||||
fArr[i6 - i4] = c(fArr[i6]);
|
|
||||||
}
|
|
||||||
for (int i7 = length; i7 < length + i4; i7++) {
|
|
||||||
fArr[i7 - i4] = c(fArr[length - 1]);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Sliding Filter error: input too short");
|
|
||||||
}
|
|
||||||
|
|
||||||
public final float b(float f, float f4) {
|
|
||||||
switch (this.f) {
|
|
||||||
case 0:
|
|
||||||
return Math.max(f, f4);
|
|
||||||
default:
|
|
||||||
return Math.min(f, f4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final float c(float f) {
|
|
||||||
float[] fArr = this.f631b;
|
|
||||||
int i4 = this.f630a;
|
|
||||||
if (fArr == null) {
|
|
||||||
this.f631b = new float[i4];
|
|
||||||
for (int i5 = 0; i5 < i4; i5++) {
|
|
||||||
this.f631b[i5] = f;
|
|
||||||
}
|
|
||||||
this.f632d = f;
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
float b4 = b(this.f632d, f);
|
|
||||||
this.f632d = b4;
|
|
||||||
this.f633e = b(this.f631b[this.c], b4);
|
|
||||||
float[] fArr2 = this.f631b;
|
|
||||||
int i6 = this.c;
|
|
||||||
int i7 = i6 + 1;
|
|
||||||
this.c = i7;
|
|
||||||
fArr2[i6] = f;
|
|
||||||
if (i7 == i4) {
|
|
||||||
int length = fArr2.length;
|
|
||||||
while (true) {
|
|
||||||
this.c = length - 1;
|
|
||||||
int i8 = this.c;
|
|
||||||
if (i8 <= 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
float[] fArr3 = this.f631b;
|
|
||||||
fArr3[i8 - 1] = b(fArr3[i8 - 1], fArr3[i8]);
|
|
||||||
length = this.c;
|
|
||||||
}
|
|
||||||
this.c = 0;
|
|
||||||
this.f632d = f;
|
|
||||||
}
|
|
||||||
return this.f633e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package I0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 照搬自 Morse Expert 1.15 I0.d, 类名保持混淆原名。
|
|
||||||
* R8 合并类手术: 只保留 (i3.d, int, int, float[]) FFT 任务构造;
|
|
||||||
* run() 按 smali 原样还原: R8 已将任务体优化为死代码(aget 后 throw null,
|
|
||||||
* 永不执行), 保留原语义(行为与原 APK 完全一致)。
|
|
||||||
* SystemForegroundService(work 库)case 已裁剪。
|
|
||||||
*/
|
|
||||||
public final class d implements Runnable {
|
|
||||||
public final /* synthetic */ int f = 0;
|
|
||||||
|
|
||||||
/* renamed from: g, reason: collision with root package name */
|
|
||||||
public final /* synthetic */ int f653g;
|
|
||||||
|
|
||||||
/* renamed from: h, reason: collision with root package name */
|
|
||||||
public final /* synthetic */ int f654h;
|
|
||||||
|
|
||||||
/* renamed from: i, reason: collision with root package name */
|
|
||||||
public final /* synthetic */ Object f655i;
|
|
||||||
|
|
||||||
/* renamed from: j, reason: collision with root package name */
|
|
||||||
public final /* synthetic */ Object f656j;
|
|
||||||
|
|
||||||
public d(i3.d dVar, int i4, int i5, float[] fArr) {
|
|
||||||
this.f656j = dVar;
|
|
||||||
this.f653g = i4;
|
|
||||||
this.f654h = i5;
|
|
||||||
this.f655i = fArr;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // java.lang.Runnable
|
|
||||||
public final void run() {
|
|
||||||
int i6 = this.f654h;
|
|
||||||
int i7 = this.f653g;
|
|
||||||
if (i7 >= i6) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
float f = ((float[]) this.f655i)[i7 * 2];
|
|
||||||
// R8 原代码: aget 后 throw null(死代码, 永不执行) —— 照搬语义, 不执行任何计算
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package I2;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import pas.decoder;
|
|
||||||
|
|
||||||
/* loaded from: classes.dex */
|
|
||||||
public final class b {
|
|
||||||
|
|
||||||
/* renamed from: b, reason: collision with root package name */
|
|
||||||
public static final String[] f663b = {"bg_color", "text_color", "text_color_weak", "call_color", "call_color_weak", "cq_color", "cq_color_weak", "rst_color", "rst_color_weak"};
|
|
||||||
public static final String[] c = {"Background color", "Text color", "Text color, weak", "Callsign color", "Callsign color, weak", "CQ color", "CQ color, weak", "RST color", "RST color, weak"};
|
|
||||||
|
|
||||||
/* renamed from: d, reason: collision with root package name */
|
|
||||||
public static final int[] f664d = {-3084048, -16777216, -5592406, -65536, -30584, -16776961, -7829249, -65281, -30465};
|
|
||||||
|
|
||||||
/* renamed from: a, reason: collision with root package name */
|
|
||||||
public final int[] f665a = (int[]) f664d.clone();
|
|
||||||
|
|
||||||
public final int a(decoder.TCharInfo tCharInfo) {
|
|
||||||
boolean lowSnr = tCharInfo.getLowSnr();
|
|
||||||
int[] iArr = this.f665a;
|
|
||||||
if (lowSnr) {
|
|
||||||
int i4 = tCharInfo.getWordType().Value;
|
|
||||||
if (i4 != 0) {
|
|
||||||
if (i4 != 1) {
|
|
||||||
if (i4 != 2) {
|
|
||||||
if (i4 != 3) {
|
|
||||||
if (i4 != 4) {
|
|
||||||
if (i4 == 5) {
|
|
||||||
return iArr[4];
|
|
||||||
}
|
|
||||||
return -16777216;
|
|
||||||
}
|
|
||||||
return iArr[2];
|
|
||||||
}
|
|
||||||
return iArr[6];
|
|
||||||
}
|
|
||||||
return iArr[8];
|
|
||||||
}
|
|
||||||
return iArr[2];
|
|
||||||
}
|
|
||||||
return iArr[2];
|
|
||||||
}
|
|
||||||
int i5 = tCharInfo.getWordType().Value;
|
|
||||||
if (i5 != 0) {
|
|
||||||
if (i5 != 1) {
|
|
||||||
if (i5 != 2) {
|
|
||||||
if (i5 != 3) {
|
|
||||||
if (i5 != 4) {
|
|
||||||
if (i5 != 5) {
|
|
||||||
return -16777216;
|
|
||||||
}
|
|
||||||
return iArr[3];
|
|
||||||
}
|
|
||||||
return iArr[1];
|
|
||||||
}
|
|
||||||
return iArr[5];
|
|
||||||
}
|
|
||||||
return iArr[7];
|
|
||||||
}
|
|
||||||
return iArr[1];
|
|
||||||
}
|
|
||||||
return iArr[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void b(Context context) {
|
|
||||||
SharedPreferences sharedPreferences = context.getSharedPreferences(context.getPackageName() + "_preferences", 0); // 原 c0.x.a(context)
|
|
||||||
for (int i4 = 0; i4 < 9; i4++) {
|
|
||||||
this.f665a[i4] = sharedPreferences.getInt(f663b[i4], f664d[i4]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package J2;
|
|
||||||
|
|
||||||
/* loaded from: classes.dex */
|
|
||||||
public final class a {
|
|
||||||
|
|
||||||
/* renamed from: b, reason: collision with root package name */
|
|
||||||
public static final int[] f726b = {16, 17749, 65535, 16776960, 16711680};
|
|
||||||
|
|
||||||
/* renamed from: a, reason: collision with root package name */
|
|
||||||
public final int[] f727a;
|
|
||||||
|
|
||||||
public a() {
|
|
||||||
int[] iArr = new int[256];
|
|
||||||
this.f727a = iArr;
|
|
||||||
int i4 = 4;
|
|
||||||
float f = 255.0f / 4;
|
|
||||||
int[] iArr2 = f726b;
|
|
||||||
int i5 = 0;
|
|
||||||
int i6 = -16777216;
|
|
||||||
iArr[0] = iArr2[0] | (-16777216);
|
|
||||||
int i7 = 1;
|
|
||||||
while (i7 <= i4) {
|
|
||||||
int round = Math.round(i7 * f);
|
|
||||||
iArr[round] = iArr2[i7] | i6;
|
|
||||||
int i8 = round - i5;
|
|
||||||
int i9 = iArr[i5];
|
|
||||||
int i10 = (i9 >> 16) & 255;
|
|
||||||
int i11 = (i9 >> 8) & 255;
|
|
||||||
int i12 = i9 & 255;
|
|
||||||
float f4 = i8;
|
|
||||||
// r10 = iArr2[i7] (下一个锚点色, smali: aget v10, v4, v8)
|
|
||||||
float f5 = (((iArr2[i7] >> 16) & 255) - i10) / f4;
|
|
||||||
float f6 = (((iArr2[i7] >> 8) & 255) - i11) / f4;
|
|
||||||
float f7 = ((iArr2[i7] & 255) - i12) / f4;
|
|
||||||
for (int i13 = 1; i13 < i8; i13++) {
|
|
||||||
float f8 = i13;
|
|
||||||
iArr[i5 + i13] = ((Math.round(f5 * f8) + i10) << 16) | (-16777216) | ((Math.round(f6 * f8) + i11) << 8) | (Math.round(f8 * f7) + i12);
|
|
||||||
}
|
|
||||||
i7++;
|
|
||||||
i5 = round;
|
|
||||||
i4 = 4;
|
|
||||||
i6 = -16777216;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
package J2;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.opengl.GLES20;
|
|
||||||
import android.opengl.GLSurfaceView;
|
|
||||||
import android.opengl.GLUtils;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.util.Log;
|
|
||||||
import java.nio.Buffer;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.FloatBuffer;
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig;
|
|
||||||
import javax.microedition.khronos.opengles.GL10;
|
|
||||||
|
|
||||||
/* loaded from: classes.dex */
|
|
||||||
public final class b implements GLSurfaceView.Renderer {
|
|
||||||
|
|
||||||
/* renamed from: b, reason: collision with root package name */
|
|
||||||
public FloatBuffer f729b;
|
|
||||||
public int c;
|
|
||||||
|
|
||||||
/* renamed from: d, reason: collision with root package name */
|
|
||||||
public final Bitmap f730d;
|
|
||||||
public final Context f;
|
|
||||||
|
|
||||||
/* renamed from: g, reason: collision with root package name */
|
|
||||||
public double f732g;
|
|
||||||
|
|
||||||
/* renamed from: h, reason: collision with root package name */
|
|
||||||
public final Bitmap f733h;
|
|
||||||
|
|
||||||
/* renamed from: i, reason: collision with root package name */
|
|
||||||
public final a f734i;
|
|
||||||
|
|
||||||
/* renamed from: j, reason: collision with root package name */
|
|
||||||
public long f735j;
|
|
||||||
|
|
||||||
/* renamed from: k, reason: collision with root package name */
|
|
||||||
public double f736k;
|
|
||||||
|
|
||||||
/* renamed from: l, reason: collision with root package name */
|
|
||||||
public int f737l;
|
|
||||||
|
|
||||||
/* renamed from: a, reason: collision with root package name */
|
|
||||||
public final int[] f728a = new int[1];
|
|
||||||
|
|
||||||
/* renamed from: e, reason: collision with root package name */
|
|
||||||
public final float[] f731e = {-1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f};
|
|
||||||
|
|
||||||
public b(Context context, int i4) {
|
|
||||||
Bitmap.Config config = Bitmap.Config.ARGB_8888;
|
|
||||||
Bitmap createBitmap = Bitmap.createBitmap(2048, i4, config);
|
|
||||||
this.f730d = createBitmap;
|
|
||||||
createBitmap.eraseColor(16);
|
|
||||||
this.f732g = 0.09375d;
|
|
||||||
this.f734i = new a();
|
|
||||||
this.f735j = 0L;
|
|
||||||
this.f736k = 0.0d;
|
|
||||||
this.f737l = -1;
|
|
||||||
this.f = context;
|
|
||||||
this.f733h = Bitmap.createBitmap(1, i4, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void a() {
|
|
||||||
int glGetError = GLES20.glGetError();
|
|
||||||
if (glGetError != 0) {
|
|
||||||
StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[3];
|
|
||||||
Log.e(stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber(), ", GLES20 error: " + glGetError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final double b() {
|
|
||||||
long uptimeMillis = SystemClock.uptimeMillis();
|
|
||||||
double width = (((uptimeMillis - this.f735j) * this.f732g) + this.f736k) % this.f730d.getWidth();
|
|
||||||
this.f735j = uptimeMillis;
|
|
||||||
this.f736k = width;
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void c(GL10 gl10) {
|
|
||||||
GLES20.glUseProgram(this.c);
|
|
||||||
a();
|
|
||||||
GLES20.glActiveTexture(33984);
|
|
||||||
a();
|
|
||||||
GLES20.glBindTexture(3553, this.f728a[0]);
|
|
||||||
a();
|
|
||||||
GLES20.glUniform1i(GLES20.glGetUniformLocation(this.c, "uTexture"), 0);
|
|
||||||
a();
|
|
||||||
int glGetAttribLocation = GLES20.glGetAttribLocation(this.c, "aPosition");
|
|
||||||
a();
|
|
||||||
int glGetAttribLocation2 = GLES20.glGetAttribLocation(this.c, "aTexPos");
|
|
||||||
a();
|
|
||||||
this.f729b.position(0);
|
|
||||||
a();
|
|
||||||
GLES20.glVertexAttribPointer(glGetAttribLocation, 2, 5126, false, 16, (Buffer) this.f729b);
|
|
||||||
a();
|
|
||||||
GLES20.glEnableVertexAttribArray(glGetAttribLocation);
|
|
||||||
a();
|
|
||||||
this.f729b.position(2);
|
|
||||||
a();
|
|
||||||
GLES20.glVertexAttribPointer(glGetAttribLocation2, 2, 5126, false, 16, (Buffer) this.f729b);
|
|
||||||
a();
|
|
||||||
GLES20.glEnableVertexAttribArray(glGetAttribLocation2);
|
|
||||||
a();
|
|
||||||
GLES20.glDrawArrays(5, 0, 4);
|
|
||||||
a();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void d(GL10 gl10, int i4, int i5) {
|
|
||||||
GLES20.glViewport(0, 0, i4, i5);
|
|
||||||
a();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // android.opengl.GLSurfaceView.Renderer
|
|
||||||
public final void onDrawFrame(GL10 gl10) {
|
|
||||||
float b4 = (float) (b() / this.f730d.getWidth());
|
|
||||||
float f = 0.5f + b4;
|
|
||||||
float[] fArr = this.f731e;
|
|
||||||
fArr[6] = f;
|
|
||||||
fArr[2] = f;
|
|
||||||
float f4 = b4 + 1.0f;
|
|
||||||
fArr[14] = f4;
|
|
||||||
fArr[10] = f4;
|
|
||||||
this.f729b.position(0);
|
|
||||||
this.f729b.put(this.f731e);
|
|
||||||
c(gl10);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // android.opengl.GLSurfaceView.Renderer
|
|
||||||
public final void onSurfaceChanged(GL10 gl10, int i4, int i5) {
|
|
||||||
d(gl10, i4, i5);
|
|
||||||
float width = 1.0f - (((this.f730d.getWidth() * 0.7f) / (i4 / this.f.getResources().getDisplayMetrics().density)) * 2.0f);
|
|
||||||
float[] fArr = this.f731e;
|
|
||||||
fArr[4] = width;
|
|
||||||
fArr[0] = width;
|
|
||||||
this.f729b.position(0);
|
|
||||||
this.f729b.put(this.f731e);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // android.opengl.GLSurfaceView.Renderer
|
|
||||||
public final void onSurfaceCreated(GL10 gl10, EGLConfig eGLConfig) {
|
|
||||||
int glCreateShader = GLES20.glCreateShader(35633);
|
|
||||||
a();
|
|
||||||
GLES20.glShaderSource(glCreateShader, "attribute vec2 aPosition;\nattribute vec2 aTexPos;\nvarying vec2 vTexPos;\nvoid main() {\n vTexPos = aTexPos;\n gl_Position = vec4(aPosition.xy, 0.0, 1.0);\n}");
|
|
||||||
a();
|
|
||||||
GLES20.glCompileShader(glCreateShader);
|
|
||||||
a();
|
|
||||||
int glCreateShader2 = GLES20.glCreateShader(35632);
|
|
||||||
a();
|
|
||||||
GLES20.glShaderSource(glCreateShader2, "precision mediump float;\nuniform sampler2D uTexture;\nvarying vec2 vTexPos;\nvoid main(void)\n{\n gl_FragColor = texture2D(uTexture, vTexPos);\n}");
|
|
||||||
a();
|
|
||||||
GLES20.glCompileShader(glCreateShader2);
|
|
||||||
a();
|
|
||||||
this.c = GLES20.glCreateProgram();
|
|
||||||
a();
|
|
||||||
GLES20.glAttachShader(this.c, glCreateShader);
|
|
||||||
a();
|
|
||||||
GLES20.glAttachShader(this.c, glCreateShader2);
|
|
||||||
a();
|
|
||||||
GLES20.glLinkProgram(this.c);
|
|
||||||
a();
|
|
||||||
int[] iArr = this.f728a;
|
|
||||||
GLES20.glGenTextures(1, iArr, 0);
|
|
||||||
a();
|
|
||||||
GLES20.glBindTexture(3553, iArr[0]);
|
|
||||||
a();
|
|
||||||
GLES20.glTexParameteri(3553, 10240, 9729);
|
|
||||||
GLES20.glTexParameteri(3553, 10242, 10497);
|
|
||||||
GLES20.glTexParameteri(3553, 10241, 9729);
|
|
||||||
GLES20.glTexParameteri(3553, 10243, 10497);
|
|
||||||
a();
|
|
||||||
GLUtils.texImage2D(3553, 0, this.f730d, 0);
|
|
||||||
a();
|
|
||||||
float[] fArr = this.f731e;
|
|
||||||
ByteBuffer allocateDirect = ByteBuffer.allocateDirect(fArr.length * 4);
|
|
||||||
allocateDirect.order(ByteOrder.nativeOrder());
|
|
||||||
FloatBuffer asFloatBuffer = allocateDirect.asFloatBuffer();
|
|
||||||
this.f729b = asFloatBuffer;
|
|
||||||
asFloatBuffer.put(fArr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package K1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 照搬自 Morse Expert 1.15 K1.a 精简版: 仅保留 k3 FFT 库引用的内存统计静态字段
|
|
||||||
* (K1.a.c 长整型记账)。原 K1.a(828 行)的其余方法为状态栏/库代码, 已裁剪
|
|
||||||
* (状态栏逻辑已在 MainActivity 内联)。
|
|
||||||
*/
|
|
||||||
public class a {
|
|
||||||
public static long c = 0;
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
/*
|
|
||||||
* CwDecodeScreen.kt — Look4Sat 的 CW 解码页面(Compose 外壳)。
|
|
||||||
*
|
|
||||||
* 照搬自 Morse Expert 1.15 的 com.ve3nea.morse_expert.MainActivity(已改为普通控制器类):
|
|
||||||
* - AndroidView 嵌入原版 activity_main.xml(ConstraintLayout 根, 含 decodedTextView/scaleView/
|
|
||||||
* statusTextView/verticalLayout/waterfallView);
|
|
||||||
* - 生命周期 onCreate/onResume/onPause/onDestroy 全部委托给控制器, 顺序与原 Activity 一致
|
|
||||||
* (onCreate 必须在 onResume 前, onDispose 先 onPause 再 onDestroy);
|
|
||||||
* - 麦克风权限(RECORD_AUDIO)由 Compose 侧管理, 授予后调用 onPermissionGranted()(= v()),
|
|
||||||
* 用 initialized 标记保证只调一次(v() 会 new a() 覆盖 f11036E);
|
|
||||||
* - 原 options_menu 的 清除/暂停/保存/录音/设置 改为顶部一行按钮(图标用模块内原版 drawable)。
|
|
||||||
*
|
|
||||||
* 注意: 控制器 onCreate 内的 setImmersive(window, false) 会把宿主窗口恢复为
|
|
||||||
* decorFitsSystemWindows(true)(原版行为照搬, 会对整个宿主 Activity 生效)。
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.feature.cw
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
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.systemBarsPadding
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import com.ve3nea.morse_expert.MainActivity
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val activity = remember(context) {
|
|
||||||
context as? Activity ?: error("CwDecodeScreen must be hosted in an Activity")
|
|
||||||
}
|
|
||||||
// 照搬的控制器(普通类, 非 Activity); 每次进入页面新建实例
|
|
||||||
val controller = remember { MainActivity() }
|
|
||||||
// 提前 inflate 原版布局, 供 AndroidView 与控制器 onCreate 共用同一根视图
|
|
||||||
val rootView = remember(context) {
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.activity_main, null) as ConstraintLayout
|
|
||||||
}
|
|
||||||
|
|
||||||
var permissionGranted by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) ==
|
|
||||||
PackageManager.PERMISSION_GRANTED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var permanentlyDenied by remember { mutableStateOf(false) }
|
|
||||||
// v() 会 new a() 覆盖 f11036E, 必须保证只初始化一次
|
|
||||||
var initialized by remember { mutableStateOf(false) }
|
|
||||||
var showSettings by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// 生命周期: onCreate 必须在 onResume 前; onDispose 先 onPause 再 onDestroy
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
controller.onCreate(activity, rootView)
|
|
||||||
controller.onResume()
|
|
||||||
onDispose {
|
|
||||||
controller.onPause()
|
|
||||||
controller.onDestroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 权限授予后启动解码核心; "首次进入已授权" 与 "弹窗回调授权" 两条路径统一走这里
|
|
||||||
LaunchedEffect(permissionGranted) {
|
|
||||||
if (permissionGranted && !initialized) {
|
|
||||||
controller.onPermissionGranted() // = v(): 创建音频采集 + 解码核心
|
|
||||||
// v() 只创建核心; 页面此刻已处于 resumed, 需再走一次 onResume 立即启动录音
|
|
||||||
// (AudioRecord 是新建的, startRecording 不会重复; GLSurfaceView.onResume 幂等)
|
|
||||||
controller.onResume()
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val permissionLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { granted ->
|
|
||||||
permissionGranted = granted
|
|
||||||
permanentlyDenied = !granted &&
|
|
||||||
!activity.shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 原版 tap_back_again_to_close: 单击返回只提示, 2 秒内再按才退出
|
|
||||||
BackHandler {
|
|
||||||
if (!controller.handleBackPress()) navigateUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showSettings) {
|
|
||||||
CwSettingsDialog(controller = controller, onDismiss = { showSettings = false })
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.systemBarsPadding()
|
|
||||||
) {
|
|
||||||
// 原 options_menu: 暂停/清除/保存(原版图标) + 录音/设置(文字按钮)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
IconButton(onClick = { controller.togglePause() }) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_baseline_pause_24),
|
|
||||||
contentDescription = stringResource(R.string.pause)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = { controller.clearDecoded() }) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_baseline_delete_24),
|
|
||||||
contentDescription = stringResource(R.string.clear)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = { controller.saveText() }) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_baseline_save_24),
|
|
||||||
contentDescription = stringResource(R.string.save_text)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// recordSignals 需要解码核心(f11037F), 未初始化时禁用
|
|
||||||
TextButton(
|
|
||||||
onClick = { if (initialized) controller.recordSignals() },
|
|
||||||
enabled = initialized
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.record_signals))
|
|
||||||
}
|
|
||||||
TextButton(onClick = { showSettings = true }) {
|
|
||||||
Text(stringResource(R.string.settings))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
|
||||||
AndroidView(
|
|
||||||
factory = { rootView },
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
if (!permissionGranted) {
|
|
||||||
// 未授予麦克风权限: 遮罩 + 提示文字 + 请求按钮
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color(0x99000000)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.cw_mic_permission),
|
|
||||||
color = Color.White,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(horizontal = 24.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Button(onClick = {
|
|
||||||
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
|
||||||
}) {
|
|
||||||
Text(stringResource(R.string.cw_grant_permission))
|
|
||||||
}
|
|
||||||
if (permanentlyDenied) {
|
|
||||||
// 勾选了"不再询问": 引导去系统设置开启
|
|
||||||
TextButton(onClick = {
|
|
||||||
activity.startActivity(
|
|
||||||
Intent(
|
|
||||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
|
||||||
Uri.parse("package:${activity.packageName}")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
Text(stringResource(R.string.cw_open_settings))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.feature.cw
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
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
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.RadioButton
|
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.ve3nea.morse_expert.MainActivity
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
/** 键名/默认值照搬原 Morse Expert(root_preferences.xml + SettingsActivity 逻辑)。 */
|
|
||||||
private const val KEY_MESSAGE_TYPE = "message_type"
|
|
||||||
private const val KEY_TEXT_FONT_SIZE = "text_font_size"
|
|
||||||
private const val DEFAULT_MESSAGE_TYPE = "general_text"
|
|
||||||
private const val DEFAULT_TEXT_FONT_SIZE = 18
|
|
||||||
private const val MIN_FONT_SIZE = 7
|
|
||||||
private const val MAX_FONT_SIZE = 99
|
|
||||||
|
|
||||||
/** 预置色板(9 色, 3x3 网格);原 ColorPreferenceCompat 用第三方 colorpicker, 不引入。 */
|
|
||||||
private val PaletteColors = listOf(
|
|
||||||
0xFFD0F0F0.toInt(), 0xFF000000.toInt(), 0xFFFFFFFF.toInt(),
|
|
||||||
0xFFAAAAAA.toInt(), 0xFFFF0000.toInt(), 0xFF00FF00.toInt(),
|
|
||||||
0xFF0000FF.toInt(), 0xFFFFFF00.toInt(), 0xFFFF00FF.toInt(),
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CW 设置弹窗(替代原 Morse Expert 的 SettingsActivity)。
|
|
||||||
* 设置项与键名照搬原 APK: message_type / text_font_size / 9 个颜色
|
|
||||||
* (I2.b.f663b 键名, I2.b.f664d 默认值, I2.b.c 英文标题)。
|
|
||||||
* 存储: SharedPreferences(getPackageName() + "_preferences")。
|
|
||||||
* 点击 OK 保存并调用 controller.onResume() 立即生效; Cancel/点外部仅关闭不保存。
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
|
|
||||||
val activity = controller.mActivity ?: return
|
|
||||||
val prefs = remember(activity) {
|
|
||||||
activity.getSharedPreferences(activity.packageName + "_preferences", Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageType by remember {
|
|
||||||
mutableStateOf(prefs.getString(KEY_MESSAGE_TYPE, DEFAULT_MESSAGE_TYPE) ?: DEFAULT_MESSAGE_TYPE)
|
|
||||||
}
|
|
||||||
var fontSize by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
prefs.getInt(KEY_TEXT_FONT_SIZE, DEFAULT_TEXT_FONT_SIZE)
|
|
||||||
.toFloat().coerceIn(MIN_FONT_SIZE.toFloat(), MAX_FONT_SIZE.toFloat())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var colorValues by remember {
|
|
||||||
mutableStateOf(IntArray(9) { i -> prefs.getInt(I2.b.f663b[i], I2.b.f664d[i]) })
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = { Text("CW Settings") },
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(top = 4.dp)
|
|
||||||
) {
|
|
||||||
Text("Message type", style = MaterialTheme.typography.titleSmall)
|
|
||||||
MessageTypeRow("general_text", "General Text", messageType) { messageType = it }
|
|
||||||
MessageTypeRow("ham_radio_qso", "Ham Radio QSO", messageType) { messageType = it }
|
|
||||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
Text("Text font size", style = MaterialTheme.typography.titleSmall)
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Slider(
|
|
||||||
value = fontSize,
|
|
||||||
onValueChange = { fontSize = it },
|
|
||||||
valueRange = MIN_FONT_SIZE.toFloat()..MAX_FONT_SIZE.toFloat(),
|
|
||||||
steps = MAX_FONT_SIZE - MIN_FONT_SIZE - 1,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "${fontSize.roundToInt()}",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
textAlign = TextAlign.End,
|
|
||||||
modifier = Modifier.width(44.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
Text("Colors", style = MaterialTheme.typography.titleSmall)
|
|
||||||
for (i in 0 until 9) {
|
|
||||||
ColorSettingRow(
|
|
||||||
title = I2.b.c[i],
|
|
||||||
value = colorValues[i],
|
|
||||||
onSelect = { selected ->
|
|
||||||
colorValues = colorValues.copyOf().also { it[i] = selected }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
val editor = prefs.edit()
|
|
||||||
editor.putString(KEY_MESSAGE_TYPE, messageType)
|
|
||||||
editor.putInt(KEY_TEXT_FONT_SIZE, fontSize.roundToInt())
|
|
||||||
for (i in 0 until 9) {
|
|
||||||
editor.putInt(I2.b.f663b[i], colorValues[i])
|
|
||||||
}
|
|
||||||
editor.apply()
|
|
||||||
// 原 SettingsActivity 返回时由 MainActivity.onResume() 重新读取全部设置; 照搬逻辑已就位
|
|
||||||
controller.onResume()
|
|
||||||
onDismiss()
|
|
||||||
}) { Text("OK") }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MessageTypeRow(option: String, label: String, selected: String, onSelect: (String) -> Unit) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(6.dp))
|
|
||||||
.clickable { onSelect(option) }
|
|
||||||
.padding(vertical = 2.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
RadioButton(selected = option == selected, onClick = { onSelect(option) })
|
|
||||||
Text(label, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ColorSettingRow(title: String, value: Int, onSelect: (Int) -> Unit) {
|
|
||||||
Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(title, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.size(20.dp)
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.background(Color(value)),
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(6.dp))
|
|
||||||
Text(
|
|
||||||
text = String.format("#%06X", value and 0xFFFFFF),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(6.dp))
|
|
||||||
PaletteGrid(selected = value, onSelect = onSelect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PaletteGrid(selected: Int, onSelect: (Int) -> Unit) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
||||||
PaletteColors.chunked(3).forEach { rowColors ->
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
||||||
rowColors.forEach { color ->
|
|
||||||
val isSelected = color == selected
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(36.dp)
|
|
||||||
.border(
|
|
||||||
width = if (isSelected) 3.dp else 1.dp,
|
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.primary
|
|
||||||
else MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
shape = RoundedCornerShape(6.dp),
|
|
||||||
)
|
|
||||||
.clip(RoundedCornerShape(6.dp))
|
|
||||||
.background(Color(color))
|
|
||||||
.clickable { onSelect(color) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.rtbishop.look4sat.feature.cw.suncompat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 编译期 stub:Android 无 sun.misc.Cleaner(照搬 Morse Expert 1.15 k3 FFT 库的清理回调,
|
|
||||||
* 实际 FFT 1024 点不触发 native 路径)。
|
|
||||||
*/
|
|
||||||
public class Cleaner {
|
|
||||||
public static Cleaner create(Object referent, Runnable thunk) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clean() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package com.rtbishop.look4sat.feature.cw.suncompat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 编译期 stub:Android 无 sun.misc.Unsafe(运行时通过反射获取,失败时 k3.r.f12109a 为 null)。
|
|
||||||
* 照搬自 Morse Expert 1.15(k3 FFT 库的大数组 native 路径 >1GB 不触发,实际 FFT 走 float[] 分支)。
|
|
||||||
*/
|
|
||||||
public class Unsafe {
|
|
||||||
public long allocateMemory(long bytes) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void freeMemory(long address) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte getByte(long address) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putByte(long address, byte value) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getShort(long address) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putShort(long address, short value) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getInt(long address) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putInt(long address, int value) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLong(long address) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putLong(long address, long value) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getFloat(long address) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putFloat(long address, float value) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getDouble(long address) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putDouble(long address, double value) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMemory(long address, long bytes, byte value) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copyMemory(long src, long dst, long len) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object getObject(Object o, long offset) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putObject(Object o, long offset, Object value) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
|
|
||||||
public long objectFieldOffset(java.lang.reflect.Field field) {
|
|
||||||
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package com.ve3nea.morse_expert;
|
|
||||||
|
|
||||||
import F2.a;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.media.MediaScannerConnection;
|
|
||||||
import android.text.Layout;
|
|
||||||
import android.text.Selection;
|
|
||||||
import android.text.Spannable;
|
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.format.DateFormat;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.ActionMode;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 照搬自 Morse Expert 1.15 DecodedTextView, 类名保持混淆原名。
|
|
||||||
* 适配: 原 extends l.C1702e0(appcompat 混淆类) -> 直接 extends TextView
|
|
||||||
* (C1702e0 仅提供 TextView 兼容功能, 本类未使用其特有方法)。
|
|
||||||
*/
|
|
||||||
public class DecodedTextView extends TextView {
|
|
||||||
|
|
||||||
/* renamed from: m, reason: collision with root package name */
|
|
||||||
public final Context f11031m;
|
|
||||||
|
|
||||||
/* renamed from: n, reason: collision with root package name */
|
|
||||||
public boolean f11032n;
|
|
||||||
|
|
||||||
/* renamed from: o, reason: collision with root package name */
|
|
||||||
public final AtomicBoolean f11033o;
|
|
||||||
|
|
||||||
public DecodedTextView(Context context, AttributeSet attributeSet) {
|
|
||||||
super(context, attributeSet);
|
|
||||||
this.f11032n = false;
|
|
||||||
this.f11033o = new AtomicBoolean(false);
|
|
||||||
this.f11031m = context;
|
|
||||||
setCustomSelectionActionModeCallback(getActionModeCallback());
|
|
||||||
}
|
|
||||||
|
|
||||||
private ActionMode.Callback getActionModeCallback() {
|
|
||||||
return new a(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDecodedText() {
|
|
||||||
int selectionStart = getSelectionStart();
|
|
||||||
int selectionEnd = getSelectionEnd();
|
|
||||||
int max = Math.max(0, Math.min(selectionStart, selectionEnd));
|
|
||||||
int max2 = Math.max(0, Math.max(selectionStart, selectionEnd));
|
|
||||||
if (max2 > max) {
|
|
||||||
return getText().subSequence(max, max2).toString();
|
|
||||||
}
|
|
||||||
return getText().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override // android.widget.TextView
|
|
||||||
public final void onSelectionChanged(int i4, int i5) {
|
|
||||||
super.onSelectionChanged(i4, i5);
|
|
||||||
if (hasSelection()) {
|
|
||||||
this.f11032n = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void q(SpannableStringBuilder spannableStringBuilder) {
|
|
||||||
boolean z3;
|
|
||||||
try {
|
|
||||||
CharSequence text = getText();
|
|
||||||
Layout layout = getLayout();
|
|
||||||
int lineCount = getLineCount();
|
|
||||||
int length = text.length();
|
|
||||||
int lineHeight = getLineHeight();
|
|
||||||
int scrollY = getScrollY();
|
|
||||||
if (layout.getLineBottom(lineCount - 1) == getHeight() + scrollY) {
|
|
||||||
z3 = true;
|
|
||||||
} else {
|
|
||||||
z3 = false;
|
|
||||||
}
|
|
||||||
getSelectionStart();
|
|
||||||
getSelectionEnd();
|
|
||||||
hasSelection();
|
|
||||||
if (z3) {
|
|
||||||
Selection.setSelection((Spannable) text, length, length);
|
|
||||||
}
|
|
||||||
append(spannableStringBuilder);
|
|
||||||
int i4 = lineCount - 50;
|
|
||||||
if (i4 > 0) {
|
|
||||||
int i5 = lineHeight * i4;
|
|
||||||
getEditableText().delete(0, layout.getLineEnd(lineCount - 51));
|
|
||||||
if (scrollY >= i5) {
|
|
||||||
scrollBy(0, -i5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e4) {
|
|
||||||
Log.e("printChars()", "Exception: ", e4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void r() {
|
|
||||||
String decodedText = getDecodedText();
|
|
||||||
boolean isEmpty = TextUtils.isEmpty(decodedText);
|
|
||||||
Context context = this.f11031m;
|
|
||||||
if (isEmpty) {
|
|
||||||
Toast.makeText(context, "No text to save", 0).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
File file = new File(getContext().getExternalFilesDir(null), ((String) DateFormat.format("yyyy-MM-dd_HH_mm_ss", new Date())) + ".txt");
|
|
||||||
String absolutePath = file.getAbsolutePath();
|
|
||||||
PrintWriter printWriter = new PrintWriter(file);
|
|
||||||
try {
|
|
||||||
printWriter.println(decodedText);
|
|
||||||
printWriter.close();
|
|
||||||
MediaScannerConnection.scanFile(context, new String[]{absolutePath}, new String[]{"text/plain"}, null);
|
|
||||||
Toast.makeText(context, "Text saved to " + absolutePath, 1).show();
|
|
||||||
} catch (Throwable th) {
|
|
||||||
printWriter.close();
|
|
||||||
throw th;
|
|
||||||
}
|
|
||||||
} catch (Exception e4) {
|
|
||||||
Log.e("File Save", "Failed", e4);
|
|
||||||
Toast.makeText(context, "Unable to save: " + e4.getMessage(), 1).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void s() {
|
|
||||||
Intent intent = new Intent("android.intent.action.SEND");
|
|
||||||
intent.setFlags(268435456);
|
|
||||||
intent.putExtra("android.intent.extra.TEXT", getDecodedText());
|
|
||||||
intent.setType("text/plain");
|
|
||||||
this.f11031m.startActivity(Intent.createChooser(intent, "Share Decoded Text"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user