Release GPS2Audio v2.5.44 POI category fix
This commit is contained in:
@@ -21,7 +21,8 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.FiberManualRecord
|
||||
import androidx.compose.material.icons.filled.LibraryMusic
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.material.icons.filled.MicOff
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
@@ -41,7 +42,8 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -61,30 +63,33 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.waypointaudio.R
|
||||
import de.waypointaudio.data.AudioRoutingSettings
|
||||
import de.waypointaudio.service.AudioDeviceItem
|
||||
import de.waypointaudio.data.PttRecordingFormat
|
||||
import de.waypointaudio.service.LivePttManager
|
||||
|
||||
/**
|
||||
* Karte für Live / PTT auf dem Hauptbildschirm.
|
||||
* Platzierung: zwischen Atmo-Mini-Player und Wegpunkt-Tracks-Überschrift.
|
||||
*
|
||||
* Features:
|
||||
* - PTT starten / stoppen (Toggle-Button)
|
||||
* - Statusanzeige (Bereit / Mikrofon aktiv / Berechtigung fehlt)
|
||||
* - Echo-Warnung bei fehlendem Headset
|
||||
* - Button zum Öffnen der Audio-Geräte-Einstellungen
|
||||
* - Gerätewahl-Kurzanzeige (gewählte Geräte-Namen)
|
||||
* v2.1.0:
|
||||
* - Toggle „Live-Ausgabe" und „Aufnahme speichern" (mit Format M4A/AAC).
|
||||
* - Aufnahme-Indikator (rotes Punkt) im aktiven Zustand.
|
||||
* - Knopf „Aufnahmen…" öffnet die Bibliothek; Post-Aufnahme-Dialog erscheint
|
||||
* automatisch nach jeder gespeicherten Aufnahme.
|
||||
*
|
||||
* @param pttManager Singleton-Manager
|
||||
* @param onOpenRecordings Callback um die Aufnahme-Bibliothek zu öffnen
|
||||
*/
|
||||
@Composable
|
||||
fun LivePttCard(
|
||||
pttManager: LivePttManager,
|
||||
onOpenRecordings: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val pttActive by pttManager.pttActive.collectAsState()
|
||||
val routing by pttManager.routingSettings.collectAsState()
|
||||
val inputDevices by pttManager.inputDevices.collectAsState()
|
||||
val statusMsg by pttManager.statusMessage.collectAsState()
|
||||
val pttActive by pttManager.pttActive.collectAsState()
|
||||
val recordingActive by pttManager.recordingActive.collectAsState()
|
||||
val routing by pttManager.routingSettings.collectAsState()
|
||||
val inputDevices by pttManager.inputDevices.collectAsState()
|
||||
val statusMsg by pttManager.statusMessage.collectAsState()
|
||||
|
||||
var hasMicPermission by remember {
|
||||
mutableStateOf(
|
||||
@@ -106,12 +111,10 @@ fun LivePttCard(
|
||||
}
|
||||
}
|
||||
|
||||
// Geräte laden wenn Karte angezeigt wird
|
||||
LaunchedEffect(Unit) {
|
||||
pttManager.refreshDevices()
|
||||
}
|
||||
|
||||
// Farbanimation für PTT-Button
|
||||
val pttContainerColor by animateColorAsState(
|
||||
targetValue = if (pttActive)
|
||||
MaterialTheme.colorScheme.error
|
||||
@@ -121,24 +124,26 @@ fun LivePttCard(
|
||||
label = "pttColor"
|
||||
)
|
||||
|
||||
// Anzeige-Name des gewählten Eingabegeräts
|
||||
val inputDeviceName = remember(routing.selectedInputDeviceId, inputDevices) {
|
||||
if (routing.selectedInputDeviceId == null) null
|
||||
else inputDevices.firstOrNull { it.id == routing.selectedInputDeviceId }?.displayName
|
||||
}
|
||||
|
||||
// v2.5.1 — Card mit sanften Cornern und größerem Innenabstand für
|
||||
// einen ruhigeren Look in der GPS2Audio-Farbwelt.
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 2.dp),
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(14.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
|
||||
|
||||
// ── Kopfzeile: Titel + Geräte-Button ─────────────────────────────
|
||||
// ── Kopfzeile: Titel + Aufnahmen-Button + Einstellungen-Button ───
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -158,7 +163,13 @@ fun LivePttCard(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
// Gerätewahl-Button
|
||||
IconButton(onClick = onOpenRecordings) {
|
||||
Icon(
|
||||
Icons.Filled.LibraryMusic,
|
||||
contentDescription = stringResource(R.string.ptt_recordings_button),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
pttManager.refreshDevices()
|
||||
@@ -178,13 +189,13 @@ fun LivePttCard(
|
||||
// ── Status-Zeile ──────────────────────────────────────────────────
|
||||
val statusText = when {
|
||||
!hasMicPermission -> stringResource(R.string.ptt_status_permission_missing)
|
||||
recordingActive -> stringResource(R.string.ptt_recording_status)
|
||||
pttActive -> stringResource(R.string.ptt_status_active)
|
||||
statusMsg != null -> statusMsg!!
|
||||
else -> stringResource(R.string.ptt_status_ready)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Status-Indikator (runder Punkt)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
@@ -205,9 +216,17 @@ fun LivePttCard(
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
if (recordingActive) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Filled.FiberManualRecord,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Gewähltes Gerät anzeigen (falls nicht Systemstandard)
|
||||
if (inputDeviceName != null) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
@@ -217,7 +236,6 @@ fun LivePttCard(
|
||||
)
|
||||
}
|
||||
|
||||
// Berechtigung verweigert – Hinweis
|
||||
if (showPermissionDeniedHint) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
@@ -227,6 +245,19 @@ fun LivePttCard(
|
||||
)
|
||||
}
|
||||
|
||||
// Mode summary (Live / Aufnahme / beides)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
val modeText = when {
|
||||
routing.liveOutputEnabled && routing.recordingEnabled -> "Live + Aufnahme"
|
||||
routing.recordingEnabled -> "Nur Aufnahme"
|
||||
else -> "Nur Live"
|
||||
}
|
||||
Text(
|
||||
text = modeText,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
||||
// ── PTT-Hauptbutton ───────────────────────────────────────────────
|
||||
@@ -257,10 +288,9 @@ fun LivePttCard(
|
||||
)
|
||||
}
|
||||
|
||||
// Echo-Warnung (immer sichtbar wenn kein Headset konfiguriert)
|
||||
val noHeadset = routing.selectedOutputDeviceId == null &&
|
||||
routing.selectedInputDeviceId == null
|
||||
if (noHeadset) {
|
||||
if (noHeadset && routing.liveOutputEnabled) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.ptt_echo_warning),
|
||||
@@ -271,9 +301,8 @@ fun LivePttCard(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Audio-Geräte-Dialog ───────────────────────────────────────────────────
|
||||
if (showDeviceDialog) {
|
||||
AudioDeviceDialog(
|
||||
PttSettingsDialog(
|
||||
pttManager = pttManager,
|
||||
onDismiss = { showDeviceDialog = false }
|
||||
)
|
||||
@@ -281,12 +310,12 @@ fun LivePttCard(
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Audio-Geräte-Dialog
|
||||
// PTT-Einstellungen Dialog (Live/Aufnahme + Geräte)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AudioDeviceDialog(
|
||||
fun PttSettingsDialog(
|
||||
pttManager: LivePttManager,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
@@ -294,9 +323,12 @@ fun AudioDeviceDialog(
|
||||
val outputDevices by pttManager.outputDevices.collectAsState()
|
||||
val routing by pttManager.routingSettings.collectAsState()
|
||||
|
||||
// Lokale Auswahl-States
|
||||
var selectedInputId by remember(routing) { mutableStateOf(routing.selectedInputDeviceId) }
|
||||
var selectedOutputId by remember(routing) { mutableStateOf(routing.selectedOutputDeviceId) }
|
||||
var liveEnabled by remember(routing) { mutableStateOf(routing.liveOutputEnabled) }
|
||||
var recordEnabled by remember(routing) { mutableStateOf(routing.recordingEnabled) }
|
||||
var format by remember(routing) { mutableStateOf(routing.recordingFormat) }
|
||||
var validationHint by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var inputExpanded by remember { mutableStateOf(false) }
|
||||
var outputExpanded by remember { mutableStateOf(false) }
|
||||
@@ -314,10 +346,113 @@ fun AudioDeviceDialog(
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// ── Live / Aufnahme-Modus ─────────────────────────────────────
|
||||
Text(
|
||||
text = stringResource(R.string.ptt_settings_section),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
// ── Refresh-Button ────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.ptt_live_output_toggle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.ptt_live_output_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = liveEnabled,
|
||||
onCheckedChange = { v ->
|
||||
liveEnabled = v
|
||||
validationHint = null
|
||||
if (!v && !recordEnabled) {
|
||||
liveEnabled = true
|
||||
validationHint = "Mindestens eine Option aktiv lassen."
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.ptt_recording_toggle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.ptt_recording_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = recordEnabled,
|
||||
onCheckedChange = { v ->
|
||||
recordEnabled = v
|
||||
validationHint = null
|
||||
if (!v && !liveEnabled) {
|
||||
recordEnabled = true
|
||||
validationHint = "Mindestens eine Option aktiv lassen."
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
validationHint?.let { hint ->
|
||||
Text(
|
||||
text = hint,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
// Format (M4A only, WAV disabled)
|
||||
if (recordEnabled) {
|
||||
Text(
|
||||
text = stringResource(R.string.ptt_format_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = format == PttRecordingFormat.M4A_AAC,
|
||||
onClick = { format = PttRecordingFormat.M4A_AAC }
|
||||
)
|
||||
Text(stringResource(R.string.ptt_format_m4a))
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = false,
|
||||
onClick = { /* WAV deaktiviert in v2.1.0 */ },
|
||||
enabled = false
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.ptt_format_wav),
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// ── Geräteauswahl ─────────────────────────────────────────────
|
||||
TextButton(
|
||||
onClick = { pttManager.refreshDevices() },
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
@@ -329,7 +464,6 @@ fun AudioDeviceDialog(
|
||||
style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
|
||||
// ── Eingabegerät (Mikrofon) ───────────────────────────────────
|
||||
Text(
|
||||
text = stringResource(R.string.audio_input_device_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
@@ -365,7 +499,6 @@ fun AudioDeviceDialog(
|
||||
expanded = inputExpanded,
|
||||
onDismissRequest = { inputExpanded = false }
|
||||
) {
|
||||
// Systemstandard
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.ptt_device_system_default)) },
|
||||
onClick = {
|
||||
@@ -387,7 +520,6 @@ fun AudioDeviceDialog(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ausgabegerät (Lautsprecher) ───────────────────────────────
|
||||
Text(
|
||||
text = stringResource(R.string.audio_output_device_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
@@ -423,7 +555,6 @@ fun AudioDeviceDialog(
|
||||
expanded = outputExpanded,
|
||||
onDismissRequest = { outputExpanded = false }
|
||||
) {
|
||||
// Systemstandard
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.ptt_device_system_default)) },
|
||||
onClick = {
|
||||
@@ -445,7 +576,6 @@ fun AudioDeviceDialog(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hinweis zu Geräte-IDs ─────────────────────────────────────
|
||||
HorizontalDivider()
|
||||
Text(
|
||||
text = stringResource(R.string.audio_devices_hint),
|
||||
@@ -459,7 +589,10 @@ fun AudioDeviceDialog(
|
||||
pttManager.saveRoutingSettings(
|
||||
AudioRoutingSettings(
|
||||
selectedInputDeviceId = selectedInputId,
|
||||
selectedOutputDeviceId = selectedOutputId
|
||||
selectedOutputDeviceId = selectedOutputId,
|
||||
liveOutputEnabled = liveEnabled,
|
||||
recordingEnabled = recordEnabled,
|
||||
recordingFormat = format
|
||||
)
|
||||
)
|
||||
onDismiss()
|
||||
|
||||
Reference in New Issue
Block a user