mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-08-25 21:45:30 +02:00
android: refactor... stuff (3, see description)
implement head gestures for calls, clean up code ig
This commit is contained in:
@@ -77,7 +77,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "me.kavishdevar.librepods"
|
||||
targetSdk = 37
|
||||
versionCode = 65
|
||||
versionCode = 80
|
||||
versionName = appVersionName
|
||||
}
|
||||
buildTypes {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "7a502eac34ab57e1c12ba8f849e172cf",
|
||||
"identityHash": "c547604d916583c3b94c7bf1ac787fdf",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "AppleEntity",
|
||||
@@ -102,7 +102,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "AppStateEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `hasCompletedOnboarding` INTEGER NOT NULL, `lastVersionShown` TEXT, `hasConnectedToAACP` INTEGER NOT NULL, `firstSuccessfulConnectionTime` INTEGER, `reviewPrompted` INTEGER NOT NULL, `timeUntilFOSSPremiumExpiry` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isPremium` INTEGER NOT NULL, `hasCompletedOnboarding` INTEGER NOT NULL, `lastVersionShown` TEXT, `hasConnectedToAACP` INTEGER NOT NULL, `firstSuccessfulConnectionTime` INTEGER, `reviewPrompted` INTEGER NOT NULL, `timeUntilFOSSPremiumExpiry` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
@@ -110,6 +110,12 @@
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPremium",
|
||||
"columnName": "isPremium",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasCompletedOnboarding",
|
||||
"columnName": "hasCompletedOnboarding",
|
||||
@@ -209,7 +215,7 @@
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7a502eac34ab57e1c12ba8f849e172cf')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c547604d916583c3b94c7bf1ac787fdf')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.room3.Room
|
||||
import io.github.libxposed.service.XposedService
|
||||
import io.github.libxposed.service.XposedServiceHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.kavishdevar.librepods.billing.BillingManager
|
||||
import me.kavishdevar.librepods.billing.BillingProviderFactory
|
||||
import me.kavishdevar.librepods.database.LibrePodsDatabase
|
||||
@@ -50,6 +52,11 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener
|
||||
).build()
|
||||
|
||||
XposedServiceHelper.registerListener(this)
|
||||
|
||||
runBlocking(Dispatchers.IO) {
|
||||
appDataRepository.awaitInitialized()
|
||||
}
|
||||
|
||||
BillingManager.provider = BillingProviderFactory.create(this)
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
|
||||
+9
-11
@@ -21,7 +21,6 @@ package me.kavishdevar.librepods.billing
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -31,17 +30,19 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.LibrePodsApplication
|
||||
import me.kavishdevar.librepods.R
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class FOSSBillingProvider(context: Context): BillingProvider {
|
||||
private val _isPremium = MutableStateFlow(false)
|
||||
private val appDataRepository = (context.applicationContext as LibrePodsApplication).appDataRepository
|
||||
|
||||
private val _isPremium = MutableStateFlow(appDataRepository.state.value.isPremium)
|
||||
override val isPremium: StateFlow<Boolean> = _isPremium
|
||||
|
||||
private val _price = MutableStateFlow(context.getString(R.string.name_your_own_price))
|
||||
override val price: StateFlow<String> = _price
|
||||
|
||||
private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var purchaseJob: Job? = null
|
||||
|
||||
@@ -57,21 +58,18 @@ class FOSSBillingProvider(context: Context): BillingProvider {
|
||||
purchaseJob?.cancel()
|
||||
|
||||
purchaseJob = scope.launch {
|
||||
delay(5_000)
|
||||
delay(5.seconds)
|
||||
_isPremium.value = true
|
||||
sharedPreferences.edit { putBoolean("foss_upgraded", true) }
|
||||
appDataRepository.updateState { it.copy(isPremium = true) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun queryPurchases() {
|
||||
val stored = sharedPreferences.getBoolean("foss_upgraded", false)
|
||||
if (stored != _isPremium.value) {
|
||||
_isPremium.value = stored
|
||||
}
|
||||
_isPremium.value = appDataRepository.state.value.isPremium
|
||||
}
|
||||
|
||||
override fun restorePurchases() {
|
||||
_isPremium.value = true
|
||||
sharedPreferences.edit { putBoolean("foss_upgraded", true) }
|
||||
appDataRepository.updateState { it.copy(isPremium = true) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.LibrePodsApplication
|
||||
|
||||
const val TAG = "PlayBillingProvider"
|
||||
|
||||
@@ -49,10 +50,11 @@ private const val PREMIUM_PRODUCT_ID = "librepods.advanced_features.v2"
|
||||
class PlayBillingProvider(
|
||||
context: Context
|
||||
) : BillingProvider, PurchasesUpdatedListener {
|
||||
private val appDataRepository = (context.applicationContext as LibrePodsApplication).appDataRepository
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val _isPremium = MutableStateFlow(false)
|
||||
private val _isPremium = MutableStateFlow(appDataRepository.state.value.isPremium)
|
||||
override val isPremium: StateFlow<Boolean> = _isPremium
|
||||
|
||||
private val _price = MutableStateFlow("unknown")
|
||||
@@ -177,6 +179,8 @@ class PlayBillingProvider(
|
||||
|
||||
_isPremium.value = hasPremium
|
||||
|
||||
appDataRepository.updateState { it.copy(isPremium = hasPremium) }
|
||||
|
||||
scope.launch {
|
||||
purchases
|
||||
.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED && !it.isAcknowledged }
|
||||
|
||||
@@ -637,8 +637,9 @@ class AACPManager(private val device: AppleDevice) {
|
||||
return sendPacket(packet)
|
||||
}
|
||||
|
||||
// todo: implement adaptive volume
|
||||
fun sendSourceFeatureCapabilities(): Boolean {
|
||||
val payload = byteArrayOf(0xFF.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
val payload = byteArrayOf(0xd7.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
val packet = AACPPacket.createUnknownPacket(
|
||||
opcode = MessageOpcode.SOURCE_FEATURE_CAPABILITIES,
|
||||
payload
|
||||
|
||||
@@ -8,6 +8,8 @@ data class AppStateEntity(
|
||||
@PrimaryKey
|
||||
val id: Int = 0,
|
||||
|
||||
val isPremium: Boolean = false,
|
||||
|
||||
val hasCompletedOnboarding: Boolean = false,
|
||||
val lastVersionShown: String? = null,
|
||||
|
||||
|
||||
@@ -75,6 +75,12 @@ class AppleDevice(
|
||||
_metadata.update(transform)
|
||||
}
|
||||
|
||||
internal inline fun updateSettings(
|
||||
transform: (AppleSettings) -> AppleSettings
|
||||
) {
|
||||
_settings.update(transform)
|
||||
}
|
||||
|
||||
internal suspend fun emitEvent(event: AppleEvent) {
|
||||
_events.emit(event)
|
||||
}
|
||||
@@ -197,12 +203,9 @@ class AppleDevice(
|
||||
currentByte or modeBit
|
||||
}
|
||||
setControlCommand(ControlCommandIdentifier.LISTENING_MODE_CONFIGS, newValue)
|
||||
// sharedPreferences.edit { putInt("long_press_byte", newValue) }
|
||||
}
|
||||
|
||||
fun setLongPressAction(side: String, action: StemAction) {
|
||||
// val prefKey = if (side.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
|
||||
// sharedPreferences.edit { putString(prefKey, action.name) }
|
||||
_settings.update {
|
||||
if (side.lowercase() == "left") it.copy(leftLongPressAction = action) else it.copy(rightLongPressAction = action)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
data class AppleSettings(
|
||||
val disconnectWhenNotWearing: Boolean = true, // disconnect_when_not_wearing
|
||||
|
||||
val earDetectionEnabled: Boolean = true,
|
||||
|
||||
val cacheDisconnectedComponentBattery: Boolean = true,
|
||||
|
||||
val headGesturesEnabled: Boolean = true, // head_gestures_enabled
|
||||
@@ -33,6 +35,9 @@ data class AppleSettings(
|
||||
val conversationalAwarenessPauseMusicEnabled: Boolean = false, // conversational_awareness_pause_music
|
||||
val relativeConversationalAwarenessVolumeEnabled: Boolean = true, // relative_conversational_awareness_volume
|
||||
|
||||
val conversationalAwarenessVolume: Float = 43f, // conversational_awareness_volume
|
||||
val conversationalAwarenessVolume: Float = 43f,
|
||||
val conversationalAwarenessReducedVolume: Float = 20f,
|
||||
|
||||
val hrAlertEnabled: Boolean = true,
|
||||
val hrmAlertThreshold: Int = 120,
|
||||
): DeviceSettings
|
||||
|
||||
+25
-40
@@ -25,7 +25,6 @@ import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.hardware.display.DisplayManager
|
||||
@@ -46,7 +45,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
@@ -56,9 +54,11 @@ import me.kavishdevar.librepods.LibrePodsApplication
|
||||
import me.kavishdevar.librepods.presentation.navigation.NavigationRoot
|
||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
||||
import me.kavishdevar.librepods.repository.AppDataRepository
|
||||
import me.kavishdevar.librepods.services.LibrePodsService
|
||||
import me.kavishdevar.librepods.utils.XposedState
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
private lateinit var serviceConnection: ServiceConnection
|
||||
private lateinit var connectionStatusReceiver: BroadcastReceiver
|
||||
@@ -128,13 +128,13 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
try {
|
||||
if (::serviceConnection.isInitialized) try {
|
||||
unbindService(serviceConnection)
|
||||
Log.d("MainActivity", "Unbound service")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error while unbinding service: $e")
|
||||
}
|
||||
try {
|
||||
if (::connectionStatusReceiver.isInitialized) try {
|
||||
unregisterReceiver(connectionStatusReceiver)
|
||||
Log.d("MainActivity", "Unregistered receiver")
|
||||
} catch (e: Exception) {
|
||||
@@ -142,41 +142,24 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
try {
|
||||
unbindService(serviceConnection)
|
||||
Log.d("MainActivity", "Unbound service")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error while unbinding service: $e")
|
||||
}
|
||||
try {
|
||||
unregisterReceiver(connectionStatusReceiver)
|
||||
Log.d("MainActivity", "Unregistered receiver")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error while unregistering receiver: $e")
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Main() {
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
|
||||
val librepodsService = remember { mutableStateOf<LibrePodsService?>(null) }
|
||||
|
||||
val appDataRepository: AppDataRepository = (LocalContext.current.applicationContext as LibrePodsApplication).appDataRepository
|
||||
val appState by appDataRepository.state.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (BuildConfig.PLAY_BUILD) {
|
||||
val now = System.currentTimeMillis()
|
||||
val firstConn =
|
||||
sharedPreferences.getLong("first_connection_successful_time", 0L)
|
||||
val firstConn = appState.firstSuccessfulConnectionTime?: 0L
|
||||
|
||||
val alreadyPrompted =
|
||||
sharedPreferences.getBoolean("review_prompted", false)
|
||||
val alreadyPrompted = appState.reviewPrompted
|
||||
|
||||
val oneDay = 24 * 60 * 60 * 1000L
|
||||
val oneDay = 24.hours.inWholeMilliseconds
|
||||
|
||||
if (
|
||||
firstConn != 0L &&
|
||||
@@ -185,17 +168,19 @@ fun Main() {
|
||||
) {
|
||||
triggerReviewFlow(context as? Activity ?: return@LaunchedEffect)
|
||||
|
||||
sharedPreferences.edit {
|
||||
putBoolean("review_prompted", true)
|
||||
appDataRepository.updateState {
|
||||
it.copy(reviewPrompted = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onboardingComplete = sharedPreferences.getBoolean("onboarding_complete", false)
|
||||
val onboardingComplete = appState.hasCompletedOnboarding
|
||||
|
||||
val releaseNotesShownPrefKey = "release_notes_shown_${BuildConfig.VERSION_NAME.removeSuffix("-debug").removeSuffix("-play")}"
|
||||
val releaseNotesShown = sharedPreferences.getBoolean(releaseNotesShownPrefKey, false)
|
||||
val currentVersion = BuildConfig.VERSION_NAME.removeSuffix("-debug").removeSuffix("-play")
|
||||
val lastVersionShown = appState.lastVersionShown
|
||||
|
||||
val releaseNotesShown = lastVersionShown == currentVersion
|
||||
|
||||
val devicesState = remember(librepodsService.value) {
|
||||
librepodsService.value?.devices ?: MutableStateFlow(emptyMap())
|
||||
@@ -241,12 +226,6 @@ fun Main() {
|
||||
val binder = service as LibrePodsService.LocalBinder
|
||||
val service = binder.getService()
|
||||
librepodsService.value = service
|
||||
|
||||
if (!sharedPreferences.contains("first_connection_successful_time")) {
|
||||
sharedPreferences.edit {
|
||||
putLong("first_connection_successful_time", System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
@@ -263,10 +242,16 @@ fun Main() {
|
||||
|
||||
NavigationRoot(
|
||||
showReleaseNotes = !releaseNotesShown,
|
||||
updatesShown = { sharedPreferences.edit { putBoolean(releaseNotesShownPrefKey, true) } },
|
||||
updatesShown = {
|
||||
appDataRepository.updateState {
|
||||
it.copy(lastVersionShown = currentVersion)
|
||||
}
|
||||
},
|
||||
showOnboarding = !onboardingComplete,
|
||||
onboardingComplete = {
|
||||
sharedPreferences.edit { putBoolean("onboarding_complete", true) }
|
||||
appDataRepository.updateState {
|
||||
it.copy(hasCompletedOnboarding = true)
|
||||
}
|
||||
bindService()
|
||||
},
|
||||
devicesState = devicesState
|
||||
|
||||
+4
-4
@@ -37,10 +37,10 @@ import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.MacAddress
|
||||
import me.kavishdevar.librepods.database.widget.WidgetConfigEntity
|
||||
import me.kavishdevar.librepods.devices.Device
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowCompat
|
||||
import me.kavishdevar.librepods.LibrePodsApplication
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.screens.onboarding.PrivacyPolicyPage
|
||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
Copyright (C) 2025 LibrePods 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
|
||||
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/>.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.kavishdevar.librepods.R
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@Composable
|
||||
fun ConnectionSettings(
|
||||
automaticEarDetectionEnabled: Boolean,
|
||||
onAutomaticEarDetectionChanged: (Boolean) -> Unit,
|
||||
automaticConnectionEnabled: Boolean,
|
||||
onAutomaticConnectionChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
StyledList {
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.ear_detection),
|
||||
checked = automaticEarDetectionEnabled,
|
||||
onCheckedChange = onAutomaticEarDetectionChanged
|
||||
)
|
||||
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.automatically_connect),
|
||||
description = stringResource(R.string.automatically_connect_description),
|
||||
checked = automaticConnectionEnabled,
|
||||
onCheckedChange = onAutomaticConnectionChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
Copyright (C) 2025 LibrePods 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
|
||||
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/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val SelectedColorBlue = Color(0xFF0A84FF)
|
||||
private val UnselectedColor = Color(0x593C3C3E)
|
||||
private val TextColor = Color.White
|
||||
private val IconTint = Color.White
|
||||
|
||||
@Composable
|
||||
fun ControlCenterButton(
|
||||
label: String,
|
||||
icon: Painter,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
iconAreaSize: Dp,
|
||||
isSelected: Boolean,
|
||||
backgroundBrush: Brush? = null
|
||||
) {
|
||||
val targetBackgroundColor = if (isSelected) SelectedColorBlue else UnselectedColor
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = targetBackgroundColor,
|
||||
label = "ButtonBackground"
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(iconAreaSize)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundBrush ?: Brush.linearGradient(colors=listOf(backgroundColor, backgroundColor)))
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
tint = IconTint,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = label,
|
||||
color = TextColor,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
}
|
||||
-242
@@ -1,242 +0,0 @@
|
||||
/*
|
||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
Copyright (C) 2025 LibrePods 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
|
||||
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 me.kavishdevar.librepods.presentation.components
|
||||
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
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.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.devices.NoiseControlMode
|
||||
|
||||
private val ContainerColor = Color(0x593C3C3E)
|
||||
private val SelectedIndicatorColorGray = Color(0xFF6C6C6E)
|
||||
private val SelectedIndicatorColorBlue = Color(0xFF0A84FF)
|
||||
private val TextColor = Color.White
|
||||
private val IconTintUnselected = Color.White
|
||||
private val IconTintSelected = Color.White
|
||||
|
||||
internal val AdaptiveRainbowBrush = Brush.sweepGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFB03A2F), Color(0xFFB07A2F), Color(0xFFB0A22F), Color(0xFF6AB02F),
|
||||
Color(0xFF2FAAB0), Color(0xFF2F5EB0), Color(0xFF7D2FB0), Color(0xFFB02F7D),
|
||||
Color(0xFFB03A2F)
|
||||
)
|
||||
)
|
||||
|
||||
internal val IconAreaSize = 72.dp
|
||||
private val IconSize = 42.dp
|
||||
private val IconRowHeight = IconAreaSize + 12.dp
|
||||
private val TextRowHeight = 24.dp
|
||||
private val TextSize = 12.sp
|
||||
|
||||
@Composable
|
||||
fun ControlCenterNoiseControlSegmentedButton(
|
||||
modifier: Modifier = Modifier,
|
||||
availableModes: List<NoiseControlMode>,
|
||||
selectedMode: NoiseControlMode,
|
||||
onModeSelected: (NoiseControlMode) -> Unit
|
||||
) {
|
||||
val selectedIndex = availableModes.indexOf(selectedMode).coerceAtLeast(0)
|
||||
val density = LocalDensity.current
|
||||
var iconRowWidthPx by remember { mutableFloatStateOf(0f) }
|
||||
val itemCount = availableModes.size
|
||||
|
||||
val itemSlotWidthPx = remember(iconRowWidthPx, itemCount) {
|
||||
if (itemCount > 0 && iconRowWidthPx > 0) {
|
||||
iconRowWidthPx / itemCount
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
val itemSlotWidthDp = remember(itemSlotWidthPx) { with(density) { itemSlotWidthPx.toDp() } }
|
||||
val iconAreaSizePx = remember { with(density) { IconAreaSize.toPx() } }
|
||||
|
||||
val targetIndicatorStartPx = remember(selectedIndex, itemSlotWidthPx, iconAreaSizePx) {
|
||||
if (itemSlotWidthPx > 0) {
|
||||
val slotCenterPx = (selectedIndex + 0.5f) * itemSlotWidthPx
|
||||
slotCenterPx - (iconAreaSizePx / 2f)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
val indicatorOffset: Dp by animateDpAsState(
|
||||
targetValue = with(density) { targetIndicatorStartPx.toDp() },
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessMedium
|
||||
),
|
||||
label = "IndicatorOffset"
|
||||
)
|
||||
|
||||
val indicatorBackground = remember(selectedMode) {
|
||||
when (selectedMode) {
|
||||
NoiseControlMode.ADAPTIVE -> AdaptiveRainbowBrush
|
||||
NoiseControlMode.OFF -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorGray, SelectedIndicatorColorGray))
|
||||
NoiseControlMode.TRANSPARENCY,
|
||||
NoiseControlMode.NOISE_CANCELLATION -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorBlue, SelectedIndicatorColorBlue))
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IconRowHeight)
|
||||
.clip(CircleShape)
|
||||
.background(ContainerColor)
|
||||
.onSizeChanged { iconRowWidthPx = it.width.toFloat() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.offset(x = indicatorOffset)
|
||||
.size(IconAreaSize)
|
||||
.clip(CircleShape)
|
||||
.background(indicatorBackground)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().align(Alignment.Center),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
availableModes.forEach { mode ->
|
||||
val isSelected = selectedMode == mode
|
||||
NoiseControlIconItem(
|
||||
modifier = Modifier.size(IconAreaSize),
|
||||
mode = mode,
|
||||
isSelected = isSelected,
|
||||
onClick = { onModeSelected(mode) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TextRowHeight),
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
availableModes.forEach { mode ->
|
||||
val isSelected = selectedMode == mode
|
||||
Text(
|
||||
text = getModeLabel(mode),
|
||||
color = TextColor,
|
||||
fontSize = TextSize,
|
||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.width(itemSlotWidthDp.coerceAtLeast(1.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoiseControlIconItem(
|
||||
modifier: Modifier = Modifier,
|
||||
mode: NoiseControlMode,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val iconRes = remember(mode) { getModeIconRes(mode) }
|
||||
|
||||
val tint = IconTintUnselected
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = getModeLabel(mode),
|
||||
tint = if (isSelected && mode == NoiseControlMode.ADAPTIVE) IconTintSelected else tint,
|
||||
modifier = Modifier.size(IconSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getModeIconRes(mode: NoiseControlMode): Int {
|
||||
return when (mode) {
|
||||
NoiseControlMode.OFF -> R.drawable.ic_noise_cancellation
|
||||
NoiseControlMode.TRANSPARENCY -> R.drawable.ic_transparency
|
||||
NoiseControlMode.ADAPTIVE -> R.drawable.ic_adaptive
|
||||
NoiseControlMode.NOISE_CANCELLATION -> R.drawable.ic_noise_cancellation
|
||||
}
|
||||
}
|
||||
|
||||
private fun getModeLabel(mode: NoiseControlMode): String {
|
||||
return when (mode) {
|
||||
NoiseControlMode.OFF -> "Off"
|
||||
NoiseControlMode.TRANSPARENCY -> "Transparency"
|
||||
NoiseControlMode.ADAPTIVE -> "Adaptive"
|
||||
NoiseControlMode.NOISE_CANCELLATION -> "Noise Cancellation"
|
||||
}
|
||||
}
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.utils.XposedState
|
||||
|
||||
@Composable
|
||||
fun DeviceInfoCard() {
|
||||
StyledList(title = stringResource(R.string.device_info)) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.manufacturer),
|
||||
supportingText = Build.MANUFACTURER,
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.model_number),
|
||||
supportingText = Build.MODEL,
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.build_id),
|
||||
supportingText = Build.DISPLAY,
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.version),
|
||||
supportingText = "${Build.ID} (${Build.VERSION.SDK_INT_FULL})",
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.xposed_available),
|
||||
supportingText = if (XposedState.isAvailable) {
|
||||
stringResource(R.string.yes)
|
||||
} else {
|
||||
stringResource(R.string.no)
|
||||
},
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.app_enabled_in_xposed),
|
||||
supportingText = if (XposedState.bluetoothScopeEnabled) {
|
||||
stringResource(R.string.yes)
|
||||
} else {
|
||||
stringResource(R.string.no)
|
||||
},
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
}
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
/*
|
||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
Copyright (C) 2025 LibrePods 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
|
||||
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 me.kavishdevar.librepods.presentation.components
|
||||
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
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.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sign
|
||||
|
||||
@Composable
|
||||
fun VerticalVolumeSlider(
|
||||
displayFraction: Float,
|
||||
maxVolume: Int,
|
||||
onVolumeChange: (Int) -> Unit,
|
||||
initialFraction: Float,
|
||||
onDragStateChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
baseSliderHeight: Dp = 400.dp,
|
||||
baseSliderWidth: Dp = 145.dp,
|
||||
baseCornerRadius: Dp = 45.dp,
|
||||
maxStretchFactor: Float = 1.15f,
|
||||
minCompressionFactor: Float = 0.875f,
|
||||
stretchSensitivity: Float = 1.0f,
|
||||
compressionSensitivity: Float = 1.0f,
|
||||
cornerRadiusChangeFactor: Float = 0.2f,
|
||||
directionalStretchRatio: Float = 0.75f
|
||||
) {
|
||||
val trackColor = Color(0x593C3C3E)
|
||||
val progressColor = Color.White
|
||||
|
||||
var dragFraction by remember { mutableFloatStateOf(initialFraction) }
|
||||
var isDragging by remember { mutableStateOf(false) }
|
||||
|
||||
var rawDragPosition by remember { mutableFloatStateOf(initialFraction) }
|
||||
var overscrollAmount by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
val baseHeightPx = with(LocalDensity.current) { baseSliderHeight.toPx() }
|
||||
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = dragFraction.coerceIn(0f, 1f),
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessMedium
|
||||
),
|
||||
label = "ProgressAnimation"
|
||||
)
|
||||
|
||||
val animatedOverscroll by animateFloatAsState(
|
||||
targetValue = overscrollAmount,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessMediumLow
|
||||
),
|
||||
label = "OverscrollAnimation"
|
||||
)
|
||||
|
||||
val maxOverscrollEffect = (maxStretchFactor - 1f).coerceAtLeast(0f)
|
||||
|
||||
val stretchMultiplier = stretchSensitivity
|
||||
val compressionMultiplier = compressionSensitivity
|
||||
|
||||
val overscrollDirection = sign(animatedOverscroll)
|
||||
|
||||
val totalStretchAmount = (min(maxOverscrollEffect, abs(animatedOverscroll) * stretchMultiplier) * baseSliderHeight.value).dp
|
||||
|
||||
val offsetY = if (abs(animatedOverscroll) > 0.001f) {
|
||||
val asymmetricOffset = totalStretchAmount * (directionalStretchRatio - 0.5f)
|
||||
(-overscrollDirection * asymmetricOffset.value).dp
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
|
||||
val heightStretch = baseSliderHeight + totalStretchAmount
|
||||
|
||||
val widthCompression = baseSliderWidth * max(
|
||||
minCompressionFactor,
|
||||
1f - min(1f - minCompressionFactor, abs(animatedOverscroll) * compressionMultiplier)
|
||||
)
|
||||
|
||||
val dynamicCornerRadius = baseCornerRadius * (1f - min(cornerRadiusChangeFactor, abs(animatedOverscroll) * cornerRadiusChangeFactor * 2f))
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(heightStretch)
|
||||
.width(widthCompression)
|
||||
.offset(y = offsetY)
|
||||
.clip(RoundedCornerShape(dynamicCornerRadius))
|
||||
.background(trackColor)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures { offset ->
|
||||
val newFraction = 1f - (offset.y / size.height).coerceIn(0f, 1f)
|
||||
dragFraction = newFraction
|
||||
rawDragPosition = newFraction
|
||||
overscrollAmount = 0f
|
||||
|
||||
val newVolume = (newFraction * maxVolume).roundToInt()
|
||||
onVolumeChange(newVolume)
|
||||
}
|
||||
}
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { delta ->
|
||||
rawDragPosition -= (delta / baseHeightPx)
|
||||
|
||||
dragFraction = rawDragPosition.coerceIn(0f, 1f)
|
||||
|
||||
overscrollAmount = when {
|
||||
rawDragPosition > 1f -> min(1.0f, (rawDragPosition - 1f) * 2.0f)
|
||||
rawDragPosition < 0f -> max(-1.0f, rawDragPosition * 2.0f)
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
val newVolume = (dragFraction * maxVolume).roundToInt()
|
||||
onVolumeChange(newVolume)
|
||||
},
|
||||
onDragStarted = {
|
||||
isDragging = true
|
||||
dragFraction = displayFraction
|
||||
rawDragPosition = displayFraction
|
||||
overscrollAmount = 0f
|
||||
onDragStateChange(true)
|
||||
},
|
||||
onDragStopped = {
|
||||
isDragging = false
|
||||
overscrollAmount = 0f
|
||||
rawDragPosition = dragFraction
|
||||
onDragStateChange(false)
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(animatedProgress)
|
||||
.background(progressColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -18,7 +18,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -28,6 +28,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.icons.richText
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||
@@ -50,7 +52,7 @@ fun AboutCard(
|
||||
|
||||
val serialNumber = remember { mutableIntStateOf(0) }
|
||||
|
||||
StyledList (title = stringResource(R.string.about)) {
|
||||
StyledList(title = stringResource(R.string.about)) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.model_name),
|
||||
supportingText = modelName
|
||||
@@ -61,7 +63,7 @@ fun AboutCard(
|
||||
supportingText = actualModel
|
||||
)
|
||||
|
||||
StyledListItem (
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.serial_number),
|
||||
supportingContent = {
|
||||
Text(
|
||||
+5
-11
@@ -18,11 +18,14 @@
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@Composable
|
||||
@@ -30,7 +33,6 @@ fun AudioSettings(
|
||||
adaptiveVolumeCapability: Boolean,
|
||||
conversationalAwarenessCapability: Boolean,
|
||||
loudSoundReductionCapability: Boolean,
|
||||
adaptiveAudioCapability: Boolean,
|
||||
customEqCapability: Boolean,
|
||||
|
||||
adaptiveVolumeChecked: Boolean,
|
||||
@@ -42,13 +44,12 @@ fun AudioSettings(
|
||||
loudSoundReductionChecked: Boolean,
|
||||
onLoudSoundReductionCheckedChange: (Boolean) -> Unit,
|
||||
|
||||
navigateToAdaptiveStrength: () -> Unit,
|
||||
navigateToEqualizer: () -> Unit,
|
||||
|
||||
vendorIdHook: Boolean,
|
||||
isPremium: Boolean
|
||||
) {
|
||||
if (adaptiveVolumeCapability || conversationalAwarenessCapability || loudSoundReductionCapability || adaptiveAudioCapability) {
|
||||
if (adaptiveVolumeCapability || conversationalAwarenessCapability || loudSoundReductionCapability) {
|
||||
StyledList(title = stringResource(R.string.audio)) {
|
||||
if (adaptiveVolumeCapability) {
|
||||
StyledToggle(
|
||||
@@ -80,13 +81,6 @@ fun AudioSettings(
|
||||
)
|
||||
}
|
||||
|
||||
if (adaptiveAudioCapability) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.adaptive_audio),
|
||||
onClick = navigateToAdaptiveStrength,
|
||||
)
|
||||
}
|
||||
|
||||
if (customEqCapability) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.equalizer),
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
|
||||
import android.content.res.Configuration
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
+10
-8
@@ -18,7 +18,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -28,6 +28,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@ExperimentalHazeMaterialsApi
|
||||
@@ -47,20 +49,20 @@ fun CallControlSettings(
|
||||
|
||||
StyledList(title = stringResource(R.string.call_controls)) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.answer_call),
|
||||
supportingText = stringResource(R.string.press_once),
|
||||
contentText = stringResource(R.string.answer_call),
|
||||
supportingText = stringResource(R.string.press_once),
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = muteUnmuteText,
|
||||
supportingText = singlePressAction,
|
||||
onClick = { navigateToCallControlScreen(muteUnmuteText) } ,
|
||||
contentText = muteUnmuteText,
|
||||
supportingText = singlePressAction,
|
||||
onClick = { navigateToCallControlScreen(muteUnmuteText) },
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = hangUpText,
|
||||
supportingText = doublePressAction,
|
||||
contentText = hangUpText,
|
||||
supportingText = doublePressAction,
|
||||
onClick = { navigateToCallControlScreen(hangUpText) }
|
||||
)
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
Copyright (C) 2025 LibrePods 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
|
||||
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/>.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@Composable
|
||||
fun ConnectionSettings(
|
||||
automaticEarDetectionEnabled: Boolean,
|
||||
onAutomaticEarDetectionChanged: (Boolean) -> Unit,
|
||||
automaticConnectionEnabled: Boolean,
|
||||
onAutomaticConnectionChanged: (Boolean) -> Unit,
|
||||
disconnectWhenNotWearing: Boolean,
|
||||
onDisconnectWhenNotWearingChanged: (Boolean) -> Unit,
|
||||
|
||||
takeoverWhenDisconnected: Boolean,
|
||||
onTakeoverWhenDisconnectedChanged: (Boolean) -> Unit,
|
||||
takeoverWhenIdle: Boolean,
|
||||
onTakeoverWhenIdleChanged: (Boolean) -> Unit,
|
||||
takeoverWhenMusic: Boolean,
|
||||
onTakeoverWhenMusicChanged: (Boolean) -> Unit,
|
||||
takeoverWhenCall: Boolean,
|
||||
onTakeoverWhenCallChanged: (Boolean) -> Unit,
|
||||
|
||||
takeoverWhenRingingCall: Boolean,
|
||||
onTakeoverWhenRingingCallChanged: (Boolean) -> Unit,
|
||||
takeoverWhenMediaStart: Boolean,
|
||||
onTakeoverWhenMediaStartChanged: (Boolean) -> Unit,
|
||||
|
||||
isPremium: Boolean
|
||||
) {
|
||||
StyledList {
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.ear_detection),
|
||||
checked = automaticEarDetectionEnabled,
|
||||
onCheckedChange = onAutomaticEarDetectionChanged
|
||||
)
|
||||
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.automatically_connect),
|
||||
description = stringResource(R.string.automatically_connect_description),
|
||||
checked = automaticConnectionEnabled,
|
||||
onCheckedChange = onAutomaticConnectionChanged
|
||||
)
|
||||
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.disconnect_when_not_wearing),
|
||||
description = stringResource(R.string.disconnect_when_not_wearing_description),
|
||||
checked = disconnectWhenNotWearing,
|
||||
onCheckedChange = onDisconnectWhenNotWearingChanged
|
||||
)
|
||||
}
|
||||
|
||||
// StyledList(title = stringResource(R.string.takeover_airpods_state)) {
|
||||
// StyledToggle(
|
||||
// label = stringResource(R.string.takeover_disconnected),
|
||||
// description = stringResource(R.string.takeover_disconnected_desc),
|
||||
// checked = takeoverWhenDisconnected,
|
||||
// onCheckedChange = onTakeoverWhenDisconnectedChanged,
|
||||
// enabled = isPremium
|
||||
// )
|
||||
// StyledToggle(
|
||||
// label = stringResource(R.string.takeover_idle),
|
||||
// description = stringResource(R.string.takeover_idle_desc),
|
||||
// checked = takeoverWhenIdle,
|
||||
// onCheckedChange = onTakeoverWhenIdleChanged,
|
||||
// enabled = isPremium
|
||||
// )
|
||||
// StyledToggle(
|
||||
// label = stringResource(R.string.takeover_music),
|
||||
// description = stringResource(R.string.takeover_music_desc),
|
||||
// checked = takeoverWhenMusic,
|
||||
// onCheckedChange = onTakeoverWhenMusicChanged,
|
||||
// enabled = isPremium
|
||||
// )
|
||||
//
|
||||
// StyledToggle(
|
||||
// label = stringResource(R.string.takeover_call),
|
||||
// description = stringResource(R.string.takeover_call_desc),
|
||||
// checked = takeoverWhenCall,
|
||||
// onCheckedChange = onTakeoverWhenCallChanged,
|
||||
// enabled = isPremium
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// StyledList(title = stringResource(R.string.takeover_phone_state)) {
|
||||
// StyledToggle(
|
||||
// label = stringResource(R.string.takeover_ringing_call),
|
||||
// description = stringResource(R.string.takeover_ringing_call_desc),
|
||||
// checked = takeoverWhenRingingCall,
|
||||
// onCheckedChange = onTakeoverWhenRingingCallChanged,
|
||||
// enabled = isPremium
|
||||
// )
|
||||
// StyledToggle(
|
||||
// label = stringResource(R.string.takeover_media_start),
|
||||
// description = stringResource(R.string.takeover_media_start_desc),
|
||||
// checked = takeoverWhenMediaStart,
|
||||
// onCheckedChange = onTakeoverWhenMediaStartChanged,
|
||||
// enabled = isPremium
|
||||
// )
|
||||
// }
|
||||
}
|
||||
+5
-3
@@ -18,11 +18,13 @@
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@Composable
|
||||
@@ -38,12 +40,12 @@ fun HearingHealthSettings(
|
||||
if (hasPPECapability && shouldShowHearingAid) {
|
||||
StyledList(title = stringResource(R.string.hearing_health)) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.hearing_protection),
|
||||
contentText = stringResource(R.string.hearing_protection),
|
||||
onClick = navigateToHearingProtection
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.hearing_aid),
|
||||
contentText = stringResource(R.string.hearing_aid),
|
||||
onClick = navigateToHearingAid
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
+4
-1
@@ -18,7 +18,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -174,6 +175,7 @@ fun NoiseControlSettings(
|
||||
}
|
||||
|
||||
if (showLabels) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = stringResource(labelRes),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
@@ -181,6 +183,7 @@ fun NoiseControlSettings(
|
||||
maxLines = 2,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -16,12 +16,14 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.apple
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.data.StemAction
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
|
||||
@Composable
|
||||
fun PressAndHoldSettings(
|
||||
@@ -46,12 +48,12 @@ fun PressAndHoldSettings(
|
||||
title = stringResource(R.string.press_and_hold_airpods)
|
||||
) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.left),
|
||||
contentText = stringResource(R.string.left),
|
||||
supportingText = leftActionText,
|
||||
onClick = navigateToLeftLongPress
|
||||
)
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.right),
|
||||
contentText = stringResource(R.string.right),
|
||||
supportingText = rightActionText,
|
||||
onClick = navigateToRightLongPress,
|
||||
)
|
||||
+11
-9
@@ -16,12 +16,14 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.kavishdevar.librepods.BuildConfig
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
|
||||
@Composable
|
||||
fun AppInfoCard(
|
||||
@@ -29,24 +31,24 @@ fun AppInfoCard(
|
||||
) {
|
||||
StyledList(title = stringResource(R.string.about)) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.version),
|
||||
supportingText = BuildConfig.VERSION_NAME,
|
||||
contentText = stringResource(R.string.version),
|
||||
supportingText = BuildConfig.VERSION_NAME,
|
||||
onClick = navigateToReleaseNotesScreen
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.version_code),
|
||||
supportingText = BuildConfig.VERSION_CODE.toString(),
|
||||
contentText = stringResource(R.string.version_code),
|
||||
supportingText = BuildConfig.VERSION_CODE.toString(),
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.flavor),
|
||||
supportingText = BuildConfig.FLAVOR,
|
||||
contentText = stringResource(R.string.flavor),
|
||||
supportingText = BuildConfig.FLAVOR,
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.build_type),
|
||||
supportingText = BuildConfig.BUILD_TYPE,
|
||||
contentText = stringResource(R.string.build_type),
|
||||
supportingText = BuildConfig.BUILD_TYPE,
|
||||
)
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package me.kavishdevar.librepods.presentation.components.common
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.utils.XposedState
|
||||
|
||||
@Composable
|
||||
fun DeviceInfoCard() {
|
||||
StyledList(title = stringResource(R.string.device_info)) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.manufacturer),
|
||||
supportingText = Build.MANUFACTURER,
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.model_number),
|
||||
supportingText = Build.MODEL,
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.build_id),
|
||||
supportingText = Build.DISPLAY,
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.version),
|
||||
supportingText = "${Build.ID} (${Build.VERSION.SDK_INT_FULL})",
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.xposed_available),
|
||||
supportingText = if (XposedState.isAvailable) {
|
||||
stringResource(R.string.yes)
|
||||
} else {
|
||||
stringResource(R.string.no)
|
||||
},
|
||||
enabled = false
|
||||
)
|
||||
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.app_enabled_in_xposed),
|
||||
supportingText = if (XposedState.bluetoothScopeEnabled) {
|
||||
stringResource(R.string.yes)
|
||||
} else {
|
||||
stringResource(R.string.no)
|
||||
},
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import android.graphics.RuntimeShader
|
||||
import android.os.Build
|
||||
+2
-2
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -62,7 +62,7 @@ import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ConfirmationDialog(
|
||||
fun StyledConfirmationDialog(
|
||||
showDialog: MutableState<Boolean>,
|
||||
title: String,
|
||||
message: String,
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class)
|
||||
|
||||
package me.kavishdevar.librepods.presentation.components
|
||||
package me.kavishdevar.librepods.presentation.components.primitives
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
+1
-17
@@ -22,7 +22,6 @@ import me.kavishdevar.librepods.presentation.screens.OpenSourceLicensesScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.PurchaseScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.ReleaseNotesScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.apple.AccessibilitySettingsScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.apple.AdaptiveStrengthScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.apple.AppleSettingsRoute
|
||||
import me.kavishdevar.librepods.presentation.screens.apple.CallControlScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.apple.DebugRoute
|
||||
@@ -77,7 +76,7 @@ fun RenderScreenContent(
|
||||
updatesShown: () -> Unit,
|
||||
onboardingComplete: () -> Unit
|
||||
) {
|
||||
val navigate: (Screen) -> Unit = { target -> backStack.add(target) }
|
||||
val navigate: (Screen) -> Unit = { target -> if (target !in backStack) backStack.add(target) } // prevents multiple clicks while transitioning
|
||||
fun navigateToPurchase() = navigate(Screen.Purchase)
|
||||
|
||||
val navigateBack: (() -> Unit)? = if (backStack.size > 1) {
|
||||
@@ -129,7 +128,6 @@ fun RenderScreenContent(
|
||||
navigateToLeftLongPress = { navigate(Screen.LongPress(screen.macAddress, left)) },
|
||||
navigateToRightLongPress = { navigate(Screen.LongPress(screen.macAddress, right)) },
|
||||
navigateToPurchase = ::navigateToPurchase,
|
||||
navigateToAdaptiveStrength = { navigate(Screen.AdaptiveStrength(screen.macAddress)) },
|
||||
navigateToEqualizer = { navigate(Screen.Equalizer(screen.macAddress)) },
|
||||
navigateToHeadTracking = { navigate(Screen.HeadTracking(screen.macAddress)) },
|
||||
navigateToAccessibility = { navigate(Screen.Accessibility(screen.macAddress)) },
|
||||
@@ -245,20 +243,6 @@ fun RenderScreenContent(
|
||||
)
|
||||
}
|
||||
|
||||
is Screen.AdaptiveStrength -> {
|
||||
val device = devices[screen.macAddress] as? AppleDevice ?: return
|
||||
val factory = createAppleViewModelFactory(device, appDataRepository, recordingRepository, heartRateRepository)
|
||||
val appleViewModel: AppleViewModel = viewModel(
|
||||
key = "${screen.macAddress.value}:${device.connectionNumber}",
|
||||
factory = factory
|
||||
)
|
||||
AdaptiveStrengthScreen(
|
||||
viewModel = appleViewModel,
|
||||
navigateBack = navigateBack,
|
||||
navigateToPurchase = ::navigateToPurchase
|
||||
)
|
||||
}
|
||||
|
||||
Screen.OpenSourceLicenses -> OpenSourceLicensesScreen(navigateBack)
|
||||
|
||||
is Screen.UpdateHearingTest -> {
|
||||
|
||||
@@ -55,11 +55,6 @@ sealed interface Screen: NavKey {
|
||||
override val macAddress: MacAddress
|
||||
): DeviceScreen
|
||||
|
||||
@Serializable
|
||||
data class AdaptiveStrength(
|
||||
override val macAddress: MacAddress
|
||||
): DeviceScreen
|
||||
|
||||
// @Serializable
|
||||
// data object CameraControl: Screen
|
||||
|
||||
|
||||
+11
-13
@@ -38,7 +38,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -63,17 +62,17 @@ import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import me.kavishdevar.librepods.BuildConfig
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.AppInfoCard
|
||||
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
|
||||
import me.kavishdevar.librepods.presentation.components.StyledBottomSheet
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.components.common.AppInfoCard
|
||||
import me.kavishdevar.librepods.presentation.components.common.DeviceInfoCard
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledBottomSheet
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
||||
@@ -81,7 +80,6 @@ import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
||||
import me.kavishdevar.librepods.utils.XposedState
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppSettingsScreen(
|
||||
viewModel: AppSettingsViewModel = viewModel(),
|
||||
|
||||
+5
-5
@@ -20,11 +20,11 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
||||
+6
-6
@@ -66,12 +66,12 @@ import me.kavishdevar.librepods.devices.AppleState
|
||||
import me.kavishdevar.librepods.devices.BaseCapability
|
||||
import me.kavishdevar.librepods.devices.ConnectionState
|
||||
import me.kavishdevar.librepods.devices.Device
|
||||
import me.kavishdevar.librepods.presentation.components.NoiseControlSettings
|
||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.apple.NoiseControlSettings
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||
|
||||
+1
-7
@@ -30,13 +30,9 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -58,9 +54,7 @@ import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryDetailMode
|
||||
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryInlineDetail
|
||||
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryRow
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
|
||||
@Composable
|
||||
fun OpenSourceLicensesScreen(
|
||||
|
||||
+6
-12
@@ -21,13 +21,9 @@ package me.kavishdevar.librepods.presentation.screens
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -44,14 +40,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.MaterialButtonStyle
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
||||
import me.kavishdevar.librepods.utils.XposedState
|
||||
|
||||
|
||||
+6
-12
@@ -21,14 +21,10 @@ package me.kavishdevar.librepods.presentation.screens.apple
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -52,15 +48,13 @@ import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||
import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
||||
import me.kavishdevar.librepods.devices.AirPodsSpecs
|
||||
import me.kavishdevar.librepods.devices.BaseCapability
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
Copyright (C) 2025 LibrePods 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
|
||||
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 me.kavishdevar.librepods.presentation.screens.apple
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun AdaptiveStrengthScreen(
|
||||
viewModel: AppleViewModel,
|
||||
navigateBack: (() -> Unit)?,
|
||||
navigateToPurchase: () -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val state = uiState.state
|
||||
|
||||
val backdrop = rememberLayerBackdrop()
|
||||
|
||||
StyledScaffold(
|
||||
title = stringResource(R.string.customize_adaptive_audio),
|
||||
navigateBack = navigateBack
|
||||
) { topPadding, bottomPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.layerBackdrop(backdrop)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(topPadding))
|
||||
if (!uiState.isPremium) {
|
||||
StyledButton(
|
||||
onClick = navigateToPurchase,
|
||||
backdrop = rememberLayerBackdrop(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxScale = 0.05f,
|
||||
surfaceColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.unlock_advanced_features),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
val sliderValue = remember {
|
||||
mutableFloatStateOf(100f - (state.controlStates[ControlCommandIdentifier.AUTO_ANC_STRENGTH]?.getOrNull(0)?.toFloat() ?: 50f))
|
||||
}
|
||||
|
||||
LaunchedEffect(sliderValue) {
|
||||
snapshotFlow { sliderValue.floatValue }
|
||||
.debounce(100.milliseconds)
|
||||
.collect { value ->
|
||||
viewModel.setControlCommand(
|
||||
ControlCommandIdentifier.AUTO_ANC_STRENGTH,
|
||||
byteArrayOf((100 - value).toInt().toByte())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider(
|
||||
value = sliderValue.floatValue,
|
||||
onValueChange = { sliderValue.floatValue = it },
|
||||
valueRange = 0f..100f,
|
||||
snapPoints = listOf(0f, 50f, 100f),
|
||||
startImageVector = LocalIcons.current.SpeakerMin,
|
||||
endImageVector = LocalIcons.current.SpeakerMax,
|
||||
independent = true,
|
||||
description = stringResource(R.string.adaptive_audio_description),
|
||||
enabled = uiState.isPremium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(bottomPadding))
|
||||
}
|
||||
}
|
||||
}
|
||||
+134
-57
@@ -19,6 +19,11 @@
|
||||
package me.kavishdevar.librepods.presentation.screens.apple
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -30,36 +35,45 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||
import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
||||
import me.kavishdevar.librepods.devices.AirPodsSpecs
|
||||
import me.kavishdevar.librepods.devices.AppleSettings
|
||||
import me.kavishdevar.librepods.devices.BaseCapability
|
||||
import me.kavishdevar.librepods.presentation.components.AboutCard
|
||||
import me.kavishdevar.librepods.presentation.components.AudioSettings
|
||||
import me.kavishdevar.librepods.presentation.components.BatteryView
|
||||
import me.kavishdevar.librepods.presentation.components.CallControlSettings
|
||||
import me.kavishdevar.librepods.presentation.components.ConnectionSettings
|
||||
import me.kavishdevar.librepods.presentation.components.HearingHealthSettings
|
||||
import me.kavishdevar.librepods.presentation.components.NoiseControlSettings
|
||||
import me.kavishdevar.librepods.presentation.components.PressAndHoldSettings
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.components.apple.AboutCard
|
||||
import me.kavishdevar.librepods.presentation.components.apple.AudioSettings
|
||||
import me.kavishdevar.librepods.presentation.components.apple.BatteryView
|
||||
import me.kavishdevar.librepods.presentation.components.apple.CallControlSettings
|
||||
import me.kavishdevar.librepods.presentation.components.apple.ConnectionSettings
|
||||
import me.kavishdevar.librepods.presentation.components.apple.HearingHealthSettings
|
||||
import me.kavishdevar.librepods.presentation.components.apple.NoiseControlSettings
|
||||
import me.kavishdevar.librepods.presentation.components.apple.PressAndHoldSettings
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun AppleSettingsRoute(
|
||||
@@ -71,7 +85,6 @@ fun AppleSettingsRoute(
|
||||
navigateToLeftLongPress: () -> Unit,
|
||||
navigateToRightLongPress: () -> Unit,
|
||||
navigateToPurchase: () -> Unit,
|
||||
navigateToAdaptiveStrength: () -> Unit,
|
||||
navigateToEqualizer: () -> Unit,
|
||||
navigateToHeadTracking: () -> Unit,
|
||||
navigateToAccessibility: () -> Unit,
|
||||
@@ -94,7 +107,8 @@ fun AppleSettingsRoute(
|
||||
|
||||
writeATTCharacteristic = viewModel::writeATTCharacteristic,
|
||||
|
||||
// onAutomaticEarDetectionChanged = viewModel::setAutomaticEarDetectionEnabled,
|
||||
updateSettings = viewModel::updateSettings,
|
||||
|
||||
// onAutomaticConnectionChanged = viewModel::setAutomaticConnectionEnabled,
|
||||
disconnect = viewModel::disconnect,
|
||||
|
||||
@@ -105,7 +119,6 @@ fun AppleSettingsRoute(
|
||||
navigateToLeftLongPress = navigateToLeftLongPress,
|
||||
navigateToRightLongPress = navigateToRightLongPress,
|
||||
navigateToPurchase = navigateToPurchase,
|
||||
navigateToAdaptiveStrength = navigateToAdaptiveStrength,
|
||||
navigateToEqualizer = navigateToEqualizer,
|
||||
navigateToHeadTracking = navigateToHeadTracking,
|
||||
navigateToAccessibility = navigateToAccessibility,
|
||||
@@ -130,6 +143,8 @@ fun AppleSettingsScreen(
|
||||
|
||||
writeATTCharacteristic: (ATTHandle, ByteArray) -> Unit,
|
||||
|
||||
updateSettings: (transform: (AppleSettings) -> AppleSettings) -> Unit,
|
||||
|
||||
// onAutomaticEarDetectionChanged: (Boolean) -> Unit,
|
||||
// onAutomaticConnectionChanged: (Boolean) -> Unit,
|
||||
|
||||
@@ -142,7 +157,6 @@ fun AppleSettingsScreen(
|
||||
navigateToLeftLongPress: () -> Unit,
|
||||
navigateToRightLongPress: () -> Unit,
|
||||
navigateToPurchase: () -> Unit,
|
||||
navigateToAdaptiveStrength: () -> Unit,
|
||||
navigateToEqualizer: () -> Unit,
|
||||
navigateToHeadTracking: () -> Unit,
|
||||
navigateToAccessibility: () -> Unit,
|
||||
@@ -209,20 +223,6 @@ fun AppleSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.version3.startsWith("8") || metadata.version3.startsWith("9")) {
|
||||
item(key = "spacer_recording") {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
item(key = "recording") {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.recorder),
|
||||
supportingText = stringResource(R.string.recorder_description),
|
||||
onClick = navigateToRecordingScreen,
|
||||
orientation = StyledListItemOrientation.Vertical
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (baseCapabilities.contains(BaseCapability.LISTENING_MODE)) {
|
||||
item(key = "spacer_noise") {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -238,6 +238,67 @@ fun AppleSettingsScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (baseCapabilities.contains(BaseCapability.ADAPTIVE_AUDIO)) {
|
||||
item(key = "adaptive_strength") {
|
||||
AnimatedVisibility(
|
||||
visible = state.controlStates[ControlCommandIdentifier.LISTENING_MODE]?.getOrNull(0)?.toInt() == 4,
|
||||
enter = remember {
|
||||
fadeIn() + slideInVertically()
|
||||
},
|
||||
exit = remember {
|
||||
fadeOut() + slideOutVertically()
|
||||
}
|
||||
) {
|
||||
val sliderValue = remember {
|
||||
mutableFloatStateOf(
|
||||
100f - (state.controlStates[ControlCommandIdentifier.AUTO_ANC_STRENGTH]?.getOrNull(
|
||||
0
|
||||
)?.toFloat() ?: 50f)
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(sliderValue) {
|
||||
snapshotFlow { sliderValue.floatValue }
|
||||
.debounce(100.milliseconds)
|
||||
.collect { value ->
|
||||
setControlCommandInt(
|
||||
ControlCommandIdentifier.AUTO_ANC_STRENGTH,
|
||||
(100 - value).toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider(
|
||||
value = sliderValue.floatValue,
|
||||
onValueChange = { sliderValue.floatValue = it },
|
||||
valueRange = 0f..100f,
|
||||
snapPoints = listOf(0f, 50f, 100f),
|
||||
startImageVector = LocalIcons.current.SpeakerMin,
|
||||
endImageVector = LocalIcons.current.SpeakerMax,
|
||||
independent = true,
|
||||
description = stringResource(R.string.adaptive_audio_description),
|
||||
enabled = uiState.isPremium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.version3.isNotBlank() && metadata.version3.first().digitToInt() >= 8) {
|
||||
item(key = "spacer_recording") {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
item(key = "recording") {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.recorder),
|
||||
supportingText = stringResource(R.string.recorder_description),
|
||||
onClick = navigateToRecordingScreen,
|
||||
orientation = StyledListItemOrientation.Vertical
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (baseCapabilities.contains(BaseCapability.HRM)) {
|
||||
@@ -319,30 +380,18 @@ fun AppleSettingsScreen(
|
||||
|
||||
item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item(key = "audio") {
|
||||
val adaptiveVolumeCapability =
|
||||
baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME)
|
||||
val conversationalAwarenessCapability =
|
||||
baseCapabilities.contains(BaseCapability.CONVERSATION_AWARENESS)
|
||||
val loudSoundReductionCapability =
|
||||
baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION)
|
||||
val adaptiveAudioCapability =
|
||||
baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME)
|
||||
val adaptiveVolumeCapability = baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME)
|
||||
val conversationalAwarenessCapability = baseCapabilities.contains(BaseCapability.CONVERSATION_AWARENESS)
|
||||
val loudSoundReductionCapability = baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION)
|
||||
|
||||
val adaptiveVolumeChecked =
|
||||
state.controlStates[ControlCommandIdentifier.ADAPTIVE_VOLUME_CONFIG]?.getOrNull(
|
||||
0
|
||||
) == 0x01.toByte()
|
||||
val conversationalAwarenessChecked =
|
||||
state.controlStates[ControlCommandIdentifier.CONVERSATION_DETECT_CONFIG]?.getOrNull(
|
||||
0
|
||||
) == 0x01.toByte()
|
||||
val adaptiveVolumeChecked = state.controlStates[ControlCommandIdentifier.ADAPTIVE_VOLUME_CONFIG]?.getOrNull(0) == 0x01.toByte()
|
||||
val conversationalAwarenessChecked = state.controlStates[ControlCommandIdentifier.CONVERSATION_DETECT_CONFIG]?.getOrNull(0) == 0x01.toByte()
|
||||
|
||||
AudioSettings(
|
||||
adaptiveVolumeCapability = adaptiveVolumeCapability,
|
||||
conversationalAwarenessCapability = conversationalAwarenessCapability,
|
||||
loudSoundReductionCapability = loudSoundReductionCapability,
|
||||
adaptiveAudioCapability = adaptiveAudioCapability,
|
||||
customEqCapability = metadata.version3.startsWith("9"),
|
||||
customEqCapability = metadata.version3.isNotBlank() && metadata.version3.first().digitToInt() >= 9,
|
||||
adaptiveVolumeChecked = adaptiveVolumeChecked,
|
||||
onAdaptiveVolumeCheckedChange = { checked ->
|
||||
setControlCommandBoolean(
|
||||
@@ -364,7 +413,6 @@ fun AppleSettingsScreen(
|
||||
byteArrayOf(if (checked) 0x01.toByte() else 0x00.toByte())
|
||||
)
|
||||
},
|
||||
navigateToAdaptiveStrength = navigateToAdaptiveStrength,
|
||||
navigateToEqualizer = navigateToEqualizer,
|
||||
vendorIdHook = uiState.vendorIdHook,
|
||||
isPremium = uiState.isPremium
|
||||
@@ -374,10 +422,28 @@ fun AppleSettingsScreen(
|
||||
item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item(key = "connection") {
|
||||
ConnectionSettings(
|
||||
automaticEarDetectionEnabled = state.controlStates[ControlCommandIdentifier.EAR_DETECTION_CONFIG]?.getOrNull(0) == 0x01.toByte(),
|
||||
onAutomaticEarDetectionChanged = { setControlCommandBoolean(ControlCommandIdentifier.EAR_DETECTION_CONFIG, it) },
|
||||
automaticEarDetectionEnabled = state.controlStates[ControlCommandIdentifier.EAR_DETECTION_CONFIG]?.getOrNull(0) == 0x01.toByte() || settings.earDetectionEnabled,
|
||||
onAutomaticEarDetectionChanged = { enabled -> setControlCommandBoolean(ControlCommandIdentifier.EAR_DETECTION_CONFIG, enabled); updateSettings { it.copy(earDetectionEnabled = enabled) } },
|
||||
automaticConnectionEnabled = state.controlStates[ControlCommandIdentifier.SMART_ROUTING_MODE]?.getOrNull(0) == 0x01.toByte(),
|
||||
onAutomaticConnectionChanged = { setControlCommandBoolean(ControlCommandIdentifier.SMART_ROUTING_MODE, it) }
|
||||
onAutomaticConnectionChanged = { setControlCommandBoolean(ControlCommandIdentifier.SMART_ROUTING_MODE, it) },
|
||||
disconnectWhenNotWearing = settings.disconnectWhenNotWearing,
|
||||
onDisconnectWhenNotWearingChanged = { enabled -> updateSettings { it.copy(disconnectWhenNotWearing = enabled) } },
|
||||
|
||||
takeoverWhenDisconnected = settings.takeoverWhenDisconnected,
|
||||
onTakeoverWhenDisconnectedChanged = { enabled -> updateSettings { it.copy(takeoverWhenDisconnected = enabled) } },
|
||||
takeoverWhenIdle = settings.takeoverWhenIdle,
|
||||
onTakeoverWhenIdleChanged = { enabled -> updateSettings { it.copy(takeoverWhenIdle = enabled) } },
|
||||
takeoverWhenMusic = settings.takeoverWhenMusic,
|
||||
onTakeoverWhenMusicChanged = { enabled -> updateSettings { it.copy(takeoverWhenMusic = enabled) } },
|
||||
takeoverWhenCall = settings.takeoverWhenCall,
|
||||
onTakeoverWhenCallChanged = { enabled -> updateSettings { it.copy(takeoverWhenCall = enabled) } },
|
||||
|
||||
takeoverWhenRingingCall = settings.takeoverWhenRingingCall,
|
||||
onTakeoverWhenRingingCallChanged = { enabled -> updateSettings { it.copy(takeoverWhenRingingCall = enabled) } },
|
||||
takeoverWhenMediaStart = settings.takeoverWhenMediaStart,
|
||||
onTakeoverWhenMediaStartChanged = { enabled -> updateSettings { it.copy(takeoverWhenMediaStart = enabled) } },
|
||||
|
||||
isPremium = uiState.isPremium
|
||||
)
|
||||
}
|
||||
|
||||
@@ -441,7 +507,7 @@ fun AppleSettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION) && (metadata.version3.startsWith("8") || metadata.version3.startsWith("9"))) {
|
||||
if (baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION) && (metadata.version3.isNotBlank() && metadata.version3.first().digitToInt() >= 8)) {
|
||||
item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item(key = "off_listening") {
|
||||
val id = ControlCommandIdentifier.ALLOW_OFF_OPTION
|
||||
@@ -487,7 +553,16 @@ fun AppleSettingsScreen(
|
||||
item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
|
||||
if (uiState.appSettings.debugMode) {
|
||||
item(key = "debug") {
|
||||
item(key = "show_cached_battery") {
|
||||
StyledToggle(
|
||||
label = "show cached battery",
|
||||
checked = settings.cacheDisconnectedComponentBattery,
|
||||
onCheckedChange = { enabled ->
|
||||
updateSettings { it.copy(cacheDisconnectedComponentBattery = enabled) }
|
||||
}
|
||||
)
|
||||
}
|
||||
item(key = "debug_button") {
|
||||
StyledListItem(
|
||||
contentText = "debug",
|
||||
onClick = navigateToDebugScreen
|
||||
@@ -517,6 +592,8 @@ fun AppleSettingsScreenPreviewApple() {
|
||||
setControlCommandBoolean = { _, _ -> },
|
||||
writeATTCharacteristic = { _, _ -> },
|
||||
|
||||
updateSettings = { _ -> },
|
||||
|
||||
disconnect = {},
|
||||
|
||||
navigateBack = null,
|
||||
@@ -526,7 +603,6 @@ fun AppleSettingsScreenPreviewApple() {
|
||||
navigateToLeftLongPress = {},
|
||||
navigateToRightLongPress = {},
|
||||
navigateToPurchase = {},
|
||||
navigateToAdaptiveStrength = {},
|
||||
navigateToEqualizer = {},
|
||||
navigateToHeadTracking = {},
|
||||
navigateToAccessibility = {},
|
||||
@@ -559,6 +635,8 @@ fun AppleSettingsScreenPreviewMaterial() {
|
||||
setControlCommandBoolean = { _, _ -> },
|
||||
writeATTCharacteristic = { _, _ -> },
|
||||
|
||||
updateSettings = { _ -> },
|
||||
|
||||
disconnect = {},
|
||||
|
||||
navigateBack = null,
|
||||
@@ -568,7 +646,6 @@ fun AppleSettingsScreenPreviewMaterial() {
|
||||
navigateToLeftLongPress = {},
|
||||
navigateToRightLongPress = {},
|
||||
navigateToPurchase = {},
|
||||
navigateToAdaptiveStrength = {},
|
||||
navigateToEqualizer = {},
|
||||
navigateToHeadTracking = {},
|
||||
navigateToAccessibility = {},
|
||||
|
||||
+3
-3
@@ -19,9 +19,9 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
|
||||
@Composable
|
||||
|
||||
+6
-6
@@ -34,12 +34,12 @@ import me.kavishdevar.librepods.bluetooth.aacp.types.RTBuddyDescriptor
|
||||
import me.kavishdevar.librepods.bluetooth.aacp.types.SensorDataWxBuddyPayload
|
||||
import me.kavishdevar.librepods.devices.DeviceComponent
|
||||
import me.kavishdevar.librepods.devices.PacketDestination
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.icons.richText
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
|
||||
+4
-4
@@ -77,10 +77,10 @@ import com.kyant.backdrop.effects.lens
|
||||
import com.kyant.backdrop.highlight.Highlight
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||
|
||||
+6
-6
@@ -84,12 +84,12 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.MaterialButtonStyle
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
|
||||
+3
-3
@@ -48,9 +48,9 @@ import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.HearingAidSettings
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.sendHearingAidSettings
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
|
||||
|
||||
+6
-6
@@ -50,11 +50,11 @@ import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings
|
||||
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledConfirmationDialog
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
|
||||
private const val TAG = "HearingAidScreen"
|
||||
@@ -187,7 +187,7 @@ fun HearingAidScreen(
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmationDialog(
|
||||
StyledConfirmationDialog(
|
||||
showDialog = showDialog,
|
||||
title = "Enable Hearing Aid",
|
||||
message = "Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.",
|
||||
|
||||
+3
-3
@@ -38,9 +38,9 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||
import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
|
||||
@Composable
|
||||
|
||||
+78
-59
@@ -27,10 +27,12 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.toShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLocale
|
||||
@@ -41,11 +43,15 @@ import androidx.health.connect.client.records.HeartRateRecord
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.devices.AppleSettings
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
@@ -55,6 +61,8 @@ import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun HeartRateRoute(
|
||||
@@ -68,7 +76,8 @@ fun HeartRateRoute(
|
||||
navigateBack = navigateBack,
|
||||
startHr = viewModel::startHr,
|
||||
stopHr = viewModel::stopHr,
|
||||
setHrRange = viewModel::setHrRange
|
||||
setHrRange = viewModel::setHrRange,
|
||||
updateSettings = viewModel::updateSettings
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,9 +88,11 @@ fun HeartRateScreen(
|
||||
navigateBack: (() -> Unit)?,
|
||||
startHr: () -> Unit,
|
||||
stopHr: () -> Unit,
|
||||
setHrRange: (ClosedRange<Long>) -> Unit
|
||||
setHrRange: (ClosedRange<Long>) -> Unit,
|
||||
updateSettings: (transform: (AppleSettings) -> AppleSettings) -> Unit,
|
||||
) {
|
||||
val state = uiState.state
|
||||
val settings = uiState.settings
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
@@ -129,59 +140,7 @@ fun HeartRateScreen(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(topPadding))
|
||||
|
||||
val sliderValue = remember {
|
||||
mutableFloatStateOf(state.heartRateInterval.inWholeMilliseconds.toFloat())
|
||||
}
|
||||
|
||||
val healthPermissions = rememberPermissionState(
|
||||
HealthPermission.getWritePermission(HeartRateRecord::class)
|
||||
)
|
||||
|
||||
AnimatedVisibility(visible = !healthPermissions.status.isGranted) {
|
||||
if (healthPermissions.status.isGranted) return@AnimatedVisibility
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.permission_healthconnect),
|
||||
onClick = { healthPermissions.launchPermissionRequest() },
|
||||
supportingText = stringResource(R.string.permission_description_healthconnect),
|
||||
leadingContent = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
MaterialShapes.SoftBurst.normalized()
|
||||
.toShape()
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = LocalIcons.current.VitalSigns,
|
||||
contentDescription = "vital signs",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
orientation = StyledListItemOrientation.Vertical
|
||||
)
|
||||
}
|
||||
|
||||
// saw this while using one of the forks. 1 minute doesn't seem to work for me
|
||||
// StyledList(
|
||||
// title = stringResource(R.string.interval),
|
||||
// description = stringResource(R.string.heart_rate_interval_description),
|
||||
// ) {
|
||||
// StyledListItem(
|
||||
// onClick = { setHrInterval(1.seconds) },
|
||||
// contentText = stringResource(R.string.one_second),
|
||||
// selected = state.heartRateInterval == 1.seconds
|
||||
// )
|
||||
// StyledListItem(
|
||||
// onClick = { setHrInterval(1.minutes) },
|
||||
// contentText = stringResource(R.string.one_minute),
|
||||
// selected = state.heartRateInterval == 1.minutes
|
||||
// )
|
||||
// }
|
||||
val healthPermissions = rememberPermissionState(HealthPermission.getWritePermission(HeartRateRecord::class))
|
||||
|
||||
AnimatedVisibility(state.currentHeartRate != null) {
|
||||
if (state.currentHeartRate == null) return@AnimatedVisibility
|
||||
@@ -235,6 +194,66 @@ fun HeartRateScreen(
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = !healthPermissions.status.isGranted) {
|
||||
StyledListItem(
|
||||
contentText = stringResource(R.string.permission_healthconnect),
|
||||
onClick = { healthPermissions.launchPermissionRequest() },
|
||||
supportingText = stringResource(R.string.permission_description_healthconnect),
|
||||
leadingContent = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
MaterialShapes.SoftBurst.normalized()
|
||||
.toShape()
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = LocalIcons.current.VitalSigns,
|
||||
contentDescription = "vital signs",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
orientation = StyledListItemOrientation.Vertical
|
||||
)
|
||||
}
|
||||
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.heart_rate_alert),
|
||||
description = stringResource(R.string.hrm_alert_description),
|
||||
checked = settings.hrAlertEnabled,
|
||||
onCheckedChange = { enabled ->
|
||||
updateSettings {
|
||||
it.copy(hrAlertEnabled = enabled)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val sliderValue = remember { mutableFloatStateOf(settings.hrmAlertThreshold.toFloat()) }
|
||||
|
||||
LaunchedEffect(sliderValue) {
|
||||
snapshotFlow { sliderValue.floatValue }
|
||||
.debounce(250.milliseconds)
|
||||
.collect { value ->
|
||||
updateSettings {
|
||||
it.copy(hrmAlertThreshold = value.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.heart_rate_alert_threshold),
|
||||
value = sliderValue.floatValue,
|
||||
onValueChange = { sliderValue.floatValue = it },
|
||||
valueRange = 120f..180f,
|
||||
description = "${sliderValue.floatValue.roundToInt()} bpm",
|
||||
independent = true
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
+3
-3
@@ -17,9 +17,9 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
|
||||
@Composable
|
||||
|
||||
+10
-7
@@ -45,11 +45,11 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||
import me.kavishdevar.librepods.data.StemAction
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
import kotlin.experimental.and
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
@@ -78,6 +78,9 @@ fun LongPress(
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
// ik enum would take like 1 minute to implement, idc
|
||||
val side = if (name == stringResource(R.string.left)) "left" else "right"
|
||||
|
||||
StyledScaffold(
|
||||
title = name,
|
||||
navigateBack = navigateBack
|
||||
@@ -98,7 +101,7 @@ fun LongPress(
|
||||
selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES,
|
||||
onClick = {
|
||||
viewModel.setLongPressAction(
|
||||
name,
|
||||
side,
|
||||
StemAction.CYCLE_NOISE_CONTROL_MODES
|
||||
)
|
||||
}
|
||||
@@ -109,7 +112,7 @@ fun LongPress(
|
||||
selected = longPressAction == StemAction.DIGITAL_ASSISTANT,
|
||||
onClick = {
|
||||
viewModel.setLongPressAction(
|
||||
name,
|
||||
side,
|
||||
StemAction.DIGITAL_ASSISTANT
|
||||
)
|
||||
},
|
||||
|
||||
+5
-6
@@ -35,17 +35,16 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLocale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import me.kavishdevar.librepods.BuildConfig
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.data.recording.Recording
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
import java.time.Instant
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
|
||||
+3
-3
@@ -60,9 +60,9 @@ import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.TransparencySettings
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.HearingAidSettings
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse
|
||||
import me.kavishdevar.librepods.bluetooth.att.types.sendHearingAidSettings
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||
|
||||
+3
-3
@@ -30,9 +30,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||
|
||||
@Composable
|
||||
|
||||
+3
-3
@@ -18,9 +18,9 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.AppInfoCard
|
||||
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.common.AppInfoCard
|
||||
import me.kavishdevar.librepods.presentation.components.common.DeviceInfoCard
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
|
||||
@Composable
|
||||
fun NotSupportedPage(
|
||||
|
||||
+3
-3
@@ -50,9 +50,9 @@ import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
|
||||
+4
@@ -198,5 +198,9 @@ class AppleViewModel(
|
||||
_heartRateRange.value = range
|
||||
}
|
||||
|
||||
fun updateSettings( transform : (AppleSettings) -> AppleSettings) {
|
||||
device.updateSettings(transform)
|
||||
}
|
||||
|
||||
fun sendRawPacket(data: ByteArray): Boolean = device.sendRawPacket(data)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package me.kavishdevar.librepods.repository
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -16,6 +17,12 @@ class AppDataRepository(
|
||||
private val settingsDao: AppSettingsDao,
|
||||
private val stateDao: AppStateDao,
|
||||
) {
|
||||
private val initialized = CompletableDeferred<Unit>()
|
||||
|
||||
suspend fun awaitInitialized() {
|
||||
initialized.await()
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val _settings = MutableStateFlow(AppSettingsEntity())
|
||||
@@ -28,6 +35,7 @@ class AppDataRepository(
|
||||
scope.launch {
|
||||
_settings.value = settingsDao.get() ?: AppSettingsEntity()
|
||||
_state.value = stateDao.get() ?: AppStateEntity()
|
||||
initialized.complete(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,14 @@ import android.os.IBinder
|
||||
import android.os.ParcelUuid
|
||||
import android.os.ext.SdkExtensions
|
||||
import android.provider.Settings
|
||||
import android.telecom.TelecomManager
|
||||
import android.telecom.VideoProfile
|
||||
import android.telephony.TelephonyCallback
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.health.connect.client.permission.HealthPermission
|
||||
import androidx.health.connect.client.records.HeartRateRecord
|
||||
@@ -111,8 +116,53 @@ class LibrePodsService : Service() {
|
||||
(application as LibrePodsApplication).healthConnectClient
|
||||
}
|
||||
|
||||
private val hasConnectedToAACP by lazy {
|
||||
appDataRepository.state.value.hasConnectedToAACP
|
||||
private var isCallRinging = false
|
||||
|
||||
private val telephonyCallback = object: TelephonyCallback(), TelephonyCallback.CallStateListener {
|
||||
override fun onCallStateChanged(state: Int) {
|
||||
isCallRinging = state == TelephonyManager.CALL_STATE_RINGING
|
||||
|
||||
if (state == TelephonyManager.CALL_STATE_RINGING) {
|
||||
_devices.value.values.firstOrNull { device ->
|
||||
device is AppleDevice &&
|
||||
device.connectionState.value == ConnectionState.CONNECTED &&
|
||||
device.state.value.componentState.any { it.status == ComponentStatus.IN_EAR }
|
||||
}?.let { device ->
|
||||
(device as AppleDevice).detectHeadGestures { accept ->
|
||||
if (!isCallRinging) return@detectHeadGestures
|
||||
|
||||
try {
|
||||
val telecomManager = getSystemService(TelecomManager::class.java)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
if (accept) {
|
||||
telecomManager?.acceptRingingCall(
|
||||
VideoProfile.STATE_AUDIO_ONLY
|
||||
)
|
||||
Toast.makeText(
|
||||
this@LibrePodsService,
|
||||
getString(R.string.call_accepted),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@LibrePodsService,
|
||||
getString(R.string.call_rejected),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
telecomManager?.endCall()
|
||||
}
|
||||
|
||||
isCallRinging = false
|
||||
device.stopHeadTracking()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error accepting call", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -132,6 +182,14 @@ class LibrePodsService : Service() {
|
||||
localMac = null // TODO: smart routing. MAC_ADDRESS message gives host mac?
|
||||
)
|
||||
|
||||
val telephonyManager = getSystemService(TelephonyManager::class.java)
|
||||
|
||||
telephonyManager.registerTelephonyCallback(
|
||||
mainExecutor,
|
||||
telephonyCallback
|
||||
)
|
||||
|
||||
|
||||
startForegroundNotification()
|
||||
}
|
||||
|
||||
@@ -538,7 +596,6 @@ class LibrePodsService : Service() {
|
||||
return device
|
||||
}
|
||||
|
||||
|
||||
fun observeAppSettings(): Job {
|
||||
var oldAppSettings: AppSettingsEntity = appDataRepository.settings.value
|
||||
|
||||
@@ -562,9 +619,12 @@ class LibrePodsService : Service() {
|
||||
|
||||
|
||||
if (state.aacpPackets != previousState.aacpPackets) {
|
||||
if (!hasConnectedToAACP) {
|
||||
if (!appDataRepository.state.value.hasConnectedToAACP) {
|
||||
appDataRepository.updateState { it.copy(hasConnectedToAACP = true) }
|
||||
}
|
||||
if (appDataRepository.state.value.firstSuccessfulConnectionTime == null) {
|
||||
appDataRepository.updateState { it.copy(firstSuccessfulConnectionTime = System.currentTimeMillis()) }
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
@@ -655,7 +715,7 @@ class LibrePodsService : Service() {
|
||||
TAG,
|
||||
"ear detection control command value: ${earDetectionCtrlCmdValue.toHexString()}"
|
||||
)
|
||||
val earDetectionEnabled = earDetectionCtrlCmdValue[0] == 0x01.toByte()
|
||||
val earDetectionEnabled = earDetectionCtrlCmdValue[0] == 0x01.toByte() || deviceSettings.earDetectionEnabled // temporary, cache broken (?)
|
||||
Log.d(TAG, "ear detection enabled: $earDetectionEnabled")
|
||||
if (earDetectionEnabled) {
|
||||
processComponentStateChange(
|
||||
@@ -695,10 +755,29 @@ class LibrePodsService : Service() {
|
||||
"conversational awareness state: ${state.conversationalAwarenessState}"
|
||||
)
|
||||
|
||||
// TODO: multi-step volume change. implementation exists in linux rewrite; copy from there
|
||||
when (state.conversationalAwarenessState) {
|
||||
1, 2 -> MediaController.startSpeaking()
|
||||
6, 8, 9 -> MediaController.stopSpeaking()
|
||||
1 -> {
|
||||
MediaController.startSpeaking()
|
||||
MediaController.setVolume(
|
||||
deviceSettings.conversationalAwarenessVolume.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
2 -> {
|
||||
MediaController.setVolume(
|
||||
deviceSettings.conversationalAwarenessReducedVolume.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
3 -> {
|
||||
MediaController.setVolume(
|
||||
deviceSettings.conversationalAwarenessVolume.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
6, 7, 8, 9 -> {
|
||||
MediaController.stopSpeaking()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -311,38 +311,74 @@ object MediaController {
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun startSpeaking() {
|
||||
Log.d("MediaController", "Starting speaking max vol: ${audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)}, current vol: ${audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)}, conversationalAwarenessVolume: $conversationalAwarenessVolume, relativeVolume: $relativeVolume")
|
||||
// @Synchronized
|
||||
// fun startSpeaking() {
|
||||
// Log.d("MediaController", "Starting speaking max vol: ${audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)}, current vol: ${audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)}, conversationalAwarenessVolume: $conversationalAwarenessVolume, relativeVolume: $relativeVolume")
|
||||
//
|
||||
// if (initialVolume == null) {
|
||||
// initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
// Log.d("MediaController", "Initial Volume: $initialVolume")
|
||||
// val targetVolume = if (relativeVolume) {
|
||||
// (initialVolume!! * conversationalAwarenessVolume / 100)
|
||||
// } else if (initialVolume!! > (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)) {
|
||||
// (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)
|
||||
// } else {
|
||||
// initialVolume!!
|
||||
// }
|
||||
// smoothVolumeTransition(initialVolume!!, targetVolume)
|
||||
// if (conversationalAwarenessPauseMusic) {
|
||||
// sendPause(force = true)
|
||||
// }
|
||||
// }
|
||||
// Log.d("MediaController", "Initial Volume: $initialVolume")
|
||||
// }
|
||||
|
||||
if (initialVolume == null) {
|
||||
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
Log.d("MediaController", "Initial Volume: $initialVolume")
|
||||
val targetVolume = if (relativeVolume) {
|
||||
(initialVolume!! * conversationalAwarenessVolume / 100)
|
||||
} else if (initialVolume!! > (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)) {
|
||||
(audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)
|
||||
} else {
|
||||
initialVolume!!
|
||||
}
|
||||
smoothVolumeTransition(initialVolume!!, targetVolume)
|
||||
if (conversationalAwarenessPauseMusic) {
|
||||
sendPause(force = true)
|
||||
}
|
||||
}
|
||||
Log.d("MediaController", "Initial Volume: $initialVolume")
|
||||
// @Synchronized
|
||||
// fun stopSpeaking() {
|
||||
// Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume")
|
||||
// if (initialVolume != null) {
|
||||
// smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!)
|
||||
// if (conversationalAwarenessPauseMusic) {
|
||||
// sendPlay()
|
||||
// }
|
||||
// initialVolume = null
|
||||
// }
|
||||
// }
|
||||
|
||||
@Synchronized
|
||||
fun setVolume(volumePercent: Int) {
|
||||
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||
val targetVolume = maxVolume * volumePercent / 100
|
||||
|
||||
smoothVolumeTransition(
|
||||
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
|
||||
targetVolume,
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stopSpeaking() {
|
||||
Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume")
|
||||
if (initialVolume != null) {
|
||||
smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!)
|
||||
if (conversationalAwarenessPauseMusic) {
|
||||
sendPlay()
|
||||
}
|
||||
initialVolume = null
|
||||
fun startSpeaking() {
|
||||
if (initialVolume == null) {
|
||||
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
}
|
||||
|
||||
Log.d(
|
||||
"MediaController",
|
||||
"Starting speaking, initial volume: $initialVolume",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Synchronized
|
||||
fun stopSpeaking() {
|
||||
val original = initialVolume ?: return
|
||||
|
||||
smoothVolumeTransition(
|
||||
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
|
||||
original,
|
||||
)
|
||||
|
||||
initialVolume = null
|
||||
}
|
||||
|
||||
private fun smoothVolumeTransition(fromVolume: Int, toVolume: Int) {
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
<string name="open_dialog_for_controlling">Open dialog for controlling</string>
|
||||
<string name="open_dialog_for_controlling_description">If disabled, clicking on the QS will cycle through modes. If enabled, it will show a dialog for controlling noise control mode and conversational awareness</string>
|
||||
<string name="disconnect_when_not_wearing">Disconnect AirPods when not wearing</string>
|
||||
<string name="disconnect_when_not_wearing_description">You will still be able to control them with the app - this just disconnects the audio.</string>
|
||||
<string name="disconnect_when_not_wearing_description">You will still be able to control them with the app; this just disconnects the audio.</string>
|
||||
<string name="advanced_options">Advanced Options</string>
|
||||
<string name="set_identity_resolving_key">Set Identity Resolving Key (IRK)</string>
|
||||
<string name="set_identity_resolving_key_description">Manually set the IRK value used for resolving BLE random addresses</string>
|
||||
@@ -340,4 +340,10 @@
|
||||
<string name="use_highest_refresh_rate">Use highest refresh rate available</string>
|
||||
<string name="debug_mode_description">Shows extra information and settings.</string>
|
||||
<string name="interaction">Interaction</string>
|
||||
<string name="heart_rate_monitoring">Heart Rate Monitoring</string>
|
||||
<string name="hrm_alert_description">Sends a notification when your heart rate is above the threshold. Consumes more battery.</string>
|
||||
<string name="heart_rate_alert">Heart rate alert</string>
|
||||
<string name="heart_rate_alert_threshold">Heart rate threshold</string>
|
||||
<string name="call_rejected">Call rejected</string>
|
||||
<string name="call_accepted">Call accepted</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user