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 {
|
defaultConfig {
|
||||||
applicationId = "me.kavishdevar.librepods"
|
applicationId = "me.kavishdevar.librepods"
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 65
|
versionCode = 80
|
||||||
versionName = appVersionName
|
versionName = appVersionName
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "7a502eac34ab57e1c12ba8f849e172cf",
|
"identityHash": "c547604d916583c3b94c7bf1ac787fdf",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "AppleEntity",
|
"tableName": "AppleEntity",
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "AppStateEntity",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
@@ -110,6 +110,12 @@
|
|||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isPremium",
|
||||||
|
"columnName": "isPremium",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "hasCompletedOnboarding",
|
"fieldPath": "hasCompletedOnboarding",
|
||||||
"columnName": "hasCompletedOnboarding",
|
"columnName": "hasCompletedOnboarding",
|
||||||
@@ -209,7 +215,7 @@
|
|||||||
],
|
],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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 androidx.room3.Room
|
||||||
import io.github.libxposed.service.XposedService
|
import io.github.libxposed.service.XposedService
|
||||||
import io.github.libxposed.service.XposedServiceHelper
|
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.BillingManager
|
||||||
import me.kavishdevar.librepods.billing.BillingProviderFactory
|
import me.kavishdevar.librepods.billing.BillingProviderFactory
|
||||||
import me.kavishdevar.librepods.database.LibrePodsDatabase
|
import me.kavishdevar.librepods.database.LibrePodsDatabase
|
||||||
@@ -50,6 +52,11 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener
|
|||||||
).build()
|
).build()
|
||||||
|
|
||||||
XposedServiceHelper.registerListener(this)
|
XposedServiceHelper.registerListener(this)
|
||||||
|
|
||||||
|
runBlocking(Dispatchers.IO) {
|
||||||
|
appDataRepository.awaitInitialized()
|
||||||
|
}
|
||||||
|
|
||||||
BillingManager.provider = BillingProviderFactory.create(this)
|
BillingManager.provider = BillingProviderFactory.create(this)
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
|
|||||||
+9
-11
@@ -21,7 +21,6 @@ package me.kavishdevar.librepods.billing
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -31,17 +30,19 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.LibrePodsApplication
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class FOSSBillingProvider(context: Context): BillingProvider {
|
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
|
override val isPremium: StateFlow<Boolean> = _isPremium
|
||||||
|
|
||||||
private val _price = MutableStateFlow(context.getString(R.string.name_your_own_price))
|
private val _price = MutableStateFlow(context.getString(R.string.name_your_own_price))
|
||||||
override val price: StateFlow<String> = _price
|
override val price: StateFlow<String> = _price
|
||||||
|
|
||||||
private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private var purchaseJob: Job? = null
|
private var purchaseJob: Job? = null
|
||||||
|
|
||||||
@@ -57,21 +58,18 @@ class FOSSBillingProvider(context: Context): BillingProvider {
|
|||||||
purchaseJob?.cancel()
|
purchaseJob?.cancel()
|
||||||
|
|
||||||
purchaseJob = scope.launch {
|
purchaseJob = scope.launch {
|
||||||
delay(5_000)
|
delay(5.seconds)
|
||||||
_isPremium.value = true
|
_isPremium.value = true
|
||||||
sharedPreferences.edit { putBoolean("foss_upgraded", true) }
|
appDataRepository.updateState { it.copy(isPremium = true) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun queryPurchases() {
|
override fun queryPurchases() {
|
||||||
val stored = sharedPreferences.getBoolean("foss_upgraded", false)
|
_isPremium.value = appDataRepository.state.value.isPremium
|
||||||
if (stored != _isPremium.value) {
|
|
||||||
_isPremium.value = stored
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun restorePurchases() {
|
override fun restorePurchases() {
|
||||||
_isPremium.value = true
|
_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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.LibrePodsApplication
|
||||||
|
|
||||||
const val TAG = "PlayBillingProvider"
|
const val TAG = "PlayBillingProvider"
|
||||||
|
|
||||||
@@ -49,10 +50,11 @@ private const val PREMIUM_PRODUCT_ID = "librepods.advanced_features.v2"
|
|||||||
class PlayBillingProvider(
|
class PlayBillingProvider(
|
||||||
context: Context
|
context: Context
|
||||||
) : BillingProvider, PurchasesUpdatedListener {
|
) : BillingProvider, PurchasesUpdatedListener {
|
||||||
|
private val appDataRepository = (context.applicationContext as LibrePodsApplication).appDataRepository
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
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
|
override val isPremium: StateFlow<Boolean> = _isPremium
|
||||||
|
|
||||||
private val _price = MutableStateFlow("unknown")
|
private val _price = MutableStateFlow("unknown")
|
||||||
@@ -177,6 +179,8 @@ class PlayBillingProvider(
|
|||||||
|
|
||||||
_isPremium.value = hasPremium
|
_isPremium.value = hasPremium
|
||||||
|
|
||||||
|
appDataRepository.updateState { it.copy(isPremium = hasPremium) }
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
purchases
|
purchases
|
||||||
.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED && !it.isAcknowledged }
|
.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED && !it.isAcknowledged }
|
||||||
|
|||||||
@@ -637,8 +637,9 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
return sendPacket(packet)
|
return sendPacket(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: implement adaptive volume
|
||||||
fun sendSourceFeatureCapabilities(): Boolean {
|
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(
|
val packet = AACPPacket.createUnknownPacket(
|
||||||
opcode = MessageOpcode.SOURCE_FEATURE_CAPABILITIES,
|
opcode = MessageOpcode.SOURCE_FEATURE_CAPABILITIES,
|
||||||
payload
|
payload
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ data class AppStateEntity(
|
|||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: Int = 0,
|
val id: Int = 0,
|
||||||
|
|
||||||
|
val isPremium: Boolean = false,
|
||||||
|
|
||||||
val hasCompletedOnboarding: Boolean = false,
|
val hasCompletedOnboarding: Boolean = false,
|
||||||
val lastVersionShown: String? = null,
|
val lastVersionShown: String? = null,
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ class AppleDevice(
|
|||||||
_metadata.update(transform)
|
_metadata.update(transform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal inline fun updateSettings(
|
||||||
|
transform: (AppleSettings) -> AppleSettings
|
||||||
|
) {
|
||||||
|
_settings.update(transform)
|
||||||
|
}
|
||||||
|
|
||||||
internal suspend fun emitEvent(event: AppleEvent) {
|
internal suspend fun emitEvent(event: AppleEvent) {
|
||||||
_events.emit(event)
|
_events.emit(event)
|
||||||
}
|
}
|
||||||
@@ -197,12 +203,9 @@ class AppleDevice(
|
|||||||
currentByte or modeBit
|
currentByte or modeBit
|
||||||
}
|
}
|
||||||
setControlCommand(ControlCommandIdentifier.LISTENING_MODE_CONFIGS, newValue)
|
setControlCommand(ControlCommandIdentifier.LISTENING_MODE_CONFIGS, newValue)
|
||||||
// sharedPreferences.edit { putInt("long_press_byte", newValue) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLongPressAction(side: String, action: StemAction) {
|
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 {
|
_settings.update {
|
||||||
if (side.lowercase() == "left") it.copy(leftLongPressAction = action) else it.copy(rightLongPressAction = action)
|
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(
|
data class AppleSettings(
|
||||||
val disconnectWhenNotWearing: Boolean = true, // disconnect_when_not_wearing
|
val disconnectWhenNotWearing: Boolean = true, // disconnect_when_not_wearing
|
||||||
|
|
||||||
|
val earDetectionEnabled: Boolean = true,
|
||||||
|
|
||||||
val cacheDisconnectedComponentBattery: Boolean = true,
|
val cacheDisconnectedComponentBattery: Boolean = true,
|
||||||
|
|
||||||
val headGesturesEnabled: Boolean = true, // head_gestures_enabled
|
val headGesturesEnabled: Boolean = true, // head_gestures_enabled
|
||||||
@@ -33,6 +35,9 @@ data class AppleSettings(
|
|||||||
val conversationalAwarenessPauseMusicEnabled: Boolean = false, // conversational_awareness_pause_music
|
val conversationalAwarenessPauseMusicEnabled: Boolean = false, // conversational_awareness_pause_music
|
||||||
val relativeConversationalAwarenessVolumeEnabled: Boolean = true, // relative_conversational_awareness_volume
|
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
|
): DeviceSettings
|
||||||
|
|||||||
+25
-40
@@ -25,7 +25,6 @@ import android.app.Activity
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.hardware.display.DisplayManager
|
import android.hardware.display.DisplayManager
|
||||||
@@ -46,7 +45,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.google.android.play.core.review.ReviewManagerFactory
|
import com.google.android.play.core.review.ReviewManagerFactory
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
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.navigation.NavigationRoot
|
||||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
||||||
|
import me.kavishdevar.librepods.repository.AppDataRepository
|
||||||
import me.kavishdevar.librepods.services.LibrePodsService
|
import me.kavishdevar.librepods.services.LibrePodsService
|
||||||
import me.kavishdevar.librepods.utils.XposedState
|
import me.kavishdevar.librepods.utils.XposedState
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
|
||||||
private lateinit var serviceConnection: ServiceConnection
|
private lateinit var serviceConnection: ServiceConnection
|
||||||
private lateinit var connectionStatusReceiver: BroadcastReceiver
|
private lateinit var connectionStatusReceiver: BroadcastReceiver
|
||||||
@@ -128,13 +128,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
try {
|
if (::serviceConnection.isInitialized) try {
|
||||||
unbindService(serviceConnection)
|
unbindService(serviceConnection)
|
||||||
Log.d("MainActivity", "Unbound service")
|
Log.d("MainActivity", "Unbound service")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("MainActivity", "Error while unbinding service: $e")
|
Log.e("MainActivity", "Error while unbinding service: $e")
|
||||||
}
|
}
|
||||||
try {
|
if (::connectionStatusReceiver.isInitialized) try {
|
||||||
unregisterReceiver(connectionStatusReceiver)
|
unregisterReceiver(connectionStatusReceiver)
|
||||||
Log.d("MainActivity", "Unregistered receiver")
|
Log.d("MainActivity", "Unregistered receiver")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -142,41 +142,24 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
super.onDestroy()
|
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
|
@Composable
|
||||||
fun Main() {
|
fun Main() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
|
||||||
|
|
||||||
val librepodsService = remember { mutableStateOf<LibrePodsService?>(null) }
|
val librepodsService = remember { mutableStateOf<LibrePodsService?>(null) }
|
||||||
|
|
||||||
|
val appDataRepository: AppDataRepository = (LocalContext.current.applicationContext as LibrePodsApplication).appDataRepository
|
||||||
|
val appState by appDataRepository.state.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (BuildConfig.PLAY_BUILD) {
|
if (BuildConfig.PLAY_BUILD) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val firstConn =
|
val firstConn = appState.firstSuccessfulConnectionTime?: 0L
|
||||||
sharedPreferences.getLong("first_connection_successful_time", 0L)
|
|
||||||
|
|
||||||
val alreadyPrompted =
|
val alreadyPrompted = appState.reviewPrompted
|
||||||
sharedPreferences.getBoolean("review_prompted", false)
|
|
||||||
|
|
||||||
val oneDay = 24 * 60 * 60 * 1000L
|
val oneDay = 24.hours.inWholeMilliseconds
|
||||||
|
|
||||||
if (
|
if (
|
||||||
firstConn != 0L &&
|
firstConn != 0L &&
|
||||||
@@ -185,17 +168,19 @@ fun Main() {
|
|||||||
) {
|
) {
|
||||||
triggerReviewFlow(context as? Activity ?: return@LaunchedEffect)
|
triggerReviewFlow(context as? Activity ?: return@LaunchedEffect)
|
||||||
|
|
||||||
sharedPreferences.edit {
|
appDataRepository.updateState {
|
||||||
putBoolean("review_prompted", true)
|
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 currentVersion = BuildConfig.VERSION_NAME.removeSuffix("-debug").removeSuffix("-play")
|
||||||
val releaseNotesShown = sharedPreferences.getBoolean(releaseNotesShownPrefKey, false)
|
val lastVersionShown = appState.lastVersionShown
|
||||||
|
|
||||||
|
val releaseNotesShown = lastVersionShown == currentVersion
|
||||||
|
|
||||||
val devicesState = remember(librepodsService.value) {
|
val devicesState = remember(librepodsService.value) {
|
||||||
librepodsService.value?.devices ?: MutableStateFlow(emptyMap())
|
librepodsService.value?.devices ?: MutableStateFlow(emptyMap())
|
||||||
@@ -241,12 +226,6 @@ fun Main() {
|
|||||||
val binder = service as LibrePodsService.LocalBinder
|
val binder = service as LibrePodsService.LocalBinder
|
||||||
val service = binder.getService()
|
val service = binder.getService()
|
||||||
librepodsService.value = service
|
librepodsService.value = service
|
||||||
|
|
||||||
if (!sharedPreferences.contains("first_connection_successful_time")) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putLong("first_connection_successful_time", System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
@@ -263,10 +242,16 @@ fun Main() {
|
|||||||
|
|
||||||
NavigationRoot(
|
NavigationRoot(
|
||||||
showReleaseNotes = !releaseNotesShown,
|
showReleaseNotes = !releaseNotesShown,
|
||||||
updatesShown = { sharedPreferences.edit { putBoolean(releaseNotesShownPrefKey, true) } },
|
updatesShown = {
|
||||||
|
appDataRepository.updateState {
|
||||||
|
it.copy(lastVersionShown = currentVersion)
|
||||||
|
}
|
||||||
|
},
|
||||||
showOnboarding = !onboardingComplete,
|
showOnboarding = !onboardingComplete,
|
||||||
onboardingComplete = {
|
onboardingComplete = {
|
||||||
sharedPreferences.edit { putBoolean("onboarding_complete", true) }
|
appDataRepository.updateState {
|
||||||
|
it.copy(hasCompletedOnboarding = true)
|
||||||
|
}
|
||||||
bindService()
|
bindService()
|
||||||
},
|
},
|
||||||
devicesState = devicesState
|
devicesState = devicesState
|
||||||
|
|||||||
+4
-4
@@ -37,10 +37,10 @@ import me.kavishdevar.librepods.R
|
|||||||
import me.kavishdevar.librepods.bluetooth.MacAddress
|
import me.kavishdevar.librepods.bluetooth.MacAddress
|
||||||
import me.kavishdevar.librepods.database.widget.WidgetConfigEntity
|
import me.kavishdevar.librepods.database.widget.WidgetConfigEntity
|
||||||
import me.kavishdevar.librepods.devices.Device
|
import me.kavishdevar.librepods.devices.Device
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
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 androidx.core.view.WindowCompat
|
||||||
import me.kavishdevar.librepods.LibrePodsApplication
|
import me.kavishdevar.librepods.LibrePodsApplication
|
||||||
import me.kavishdevar.librepods.R
|
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.screens.onboarding.PrivacyPolicyPage
|
||||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
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)
|
@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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -28,6 +28,8 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import me.kavishdevar.librepods.R
|
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.icons.richText
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||||
@@ -50,7 +52,7 @@ fun AboutCard(
|
|||||||
|
|
||||||
val serialNumber = remember { mutableIntStateOf(0) }
|
val serialNumber = remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
StyledList (title = stringResource(R.string.about)) {
|
StyledList(title = stringResource(R.string.about)) {
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.model_name),
|
contentText = stringResource(R.string.model_name),
|
||||||
supportingText = modelName
|
supportingText = modelName
|
||||||
@@ -61,7 +63,7 @@ fun AboutCard(
|
|||||||
supportingText = actualModel
|
supportingText = actualModel
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledListItem (
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.serial_number),
|
contentText = stringResource(R.string.serial_number),
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
+5
-11
@@ -18,11 +18,14 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@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.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import me.kavishdevar.librepods.R
|
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
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -30,7 +33,6 @@ fun AudioSettings(
|
|||||||
adaptiveVolumeCapability: Boolean,
|
adaptiveVolumeCapability: Boolean,
|
||||||
conversationalAwarenessCapability: Boolean,
|
conversationalAwarenessCapability: Boolean,
|
||||||
loudSoundReductionCapability: Boolean,
|
loudSoundReductionCapability: Boolean,
|
||||||
adaptiveAudioCapability: Boolean,
|
|
||||||
customEqCapability: Boolean,
|
customEqCapability: Boolean,
|
||||||
|
|
||||||
adaptiveVolumeChecked: Boolean,
|
adaptiveVolumeChecked: Boolean,
|
||||||
@@ -42,13 +44,12 @@ fun AudioSettings(
|
|||||||
loudSoundReductionChecked: Boolean,
|
loudSoundReductionChecked: Boolean,
|
||||||
onLoudSoundReductionCheckedChange: (Boolean) -> Unit,
|
onLoudSoundReductionCheckedChange: (Boolean) -> Unit,
|
||||||
|
|
||||||
navigateToAdaptiveStrength: () -> Unit,
|
|
||||||
navigateToEqualizer: () -> Unit,
|
navigateToEqualizer: () -> Unit,
|
||||||
|
|
||||||
vendorIdHook: Boolean,
|
vendorIdHook: Boolean,
|
||||||
isPremium: Boolean
|
isPremium: Boolean
|
||||||
) {
|
) {
|
||||||
if (adaptiveVolumeCapability || conversationalAwarenessCapability || loudSoundReductionCapability || adaptiveAudioCapability) {
|
if (adaptiveVolumeCapability || conversationalAwarenessCapability || loudSoundReductionCapability) {
|
||||||
StyledList(title = stringResource(R.string.audio)) {
|
StyledList(title = stringResource(R.string.audio)) {
|
||||||
if (adaptiveVolumeCapability) {
|
if (adaptiveVolumeCapability) {
|
||||||
StyledToggle(
|
StyledToggle(
|
||||||
@@ -80,13 +81,6 @@ fun AudioSettings(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adaptiveAudioCapability) {
|
|
||||||
StyledListItem(
|
|
||||||
contentText = stringResource(R.string.adaptive_audio),
|
|
||||||
onClick = navigateToAdaptiveStrength,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customEqCapability) {
|
if (customEqCapability) {
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.equalizer),
|
contentText = stringResource(R.string.equalizer),
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.apple
|
||||||
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@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.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
+10
-8
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@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.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -28,6 +28,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
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
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@@ -47,20 +49,20 @@ fun CallControlSettings(
|
|||||||
|
|
||||||
StyledList(title = stringResource(R.string.call_controls)) {
|
StyledList(title = stringResource(R.string.call_controls)) {
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.answer_call),
|
contentText = stringResource(R.string.answer_call),
|
||||||
supportingText = stringResource(R.string.press_once),
|
supportingText = stringResource(R.string.press_once),
|
||||||
enabled = false
|
enabled = false
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = muteUnmuteText,
|
contentText = muteUnmuteText,
|
||||||
supportingText = singlePressAction,
|
supportingText = singlePressAction,
|
||||||
onClick = { navigateToCallControlScreen(muteUnmuteText) } ,
|
onClick = { navigateToCallControlScreen(muteUnmuteText) },
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = hangUpText,
|
contentText = hangUpText,
|
||||||
supportingText = doublePressAction,
|
supportingText = doublePressAction,
|
||||||
onClick = { navigateToCallControlScreen(hangUpText) }
|
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)
|
@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.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import me.kavishdevar.librepods.R
|
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
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -38,12 +40,12 @@ fun HearingHealthSettings(
|
|||||||
if (hasPPECapability && shouldShowHearingAid) {
|
if (hasPPECapability && shouldShowHearingAid) {
|
||||||
StyledList(title = stringResource(R.string.hearing_health)) {
|
StyledList(title = stringResource(R.string.hearing_health)) {
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.hearing_protection),
|
contentText = stringResource(R.string.hearing_protection),
|
||||||
onClick = navigateToHearingProtection
|
onClick = navigateToHearingProtection
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.hearing_aid),
|
contentText = stringResource(R.string.hearing_aid),
|
||||||
onClick = navigateToHearingAid
|
onClick = navigateToHearingAid
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.apple
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
+4
-1
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.apple
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.animation.core.AnimationSpec
|
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.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -174,6 +175,7 @@ fun NoiseControlSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showLabels) {
|
if (showLabels) {
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(labelRes),
|
text = stringResource(labelRes),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
@@ -181,6 +183,7 @@ fun NoiseControlSettings(
|
|||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
modifier = Modifier.fillMaxWidth()
|
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/>.
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.data.StemAction
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PressAndHoldSettings(
|
fun PressAndHoldSettings(
|
||||||
@@ -46,12 +48,12 @@ fun PressAndHoldSettings(
|
|||||||
title = stringResource(R.string.press_and_hold_airpods)
|
title = stringResource(R.string.press_and_hold_airpods)
|
||||||
) {
|
) {
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.left),
|
contentText = stringResource(R.string.left),
|
||||||
supportingText = leftActionText,
|
supportingText = leftActionText,
|
||||||
onClick = navigateToLeftLongPress
|
onClick = navigateToLeftLongPress
|
||||||
)
|
)
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.right),
|
contentText = stringResource(R.string.right),
|
||||||
supportingText = rightActionText,
|
supportingText = rightActionText,
|
||||||
onClick = navigateToRightLongPress,
|
onClick = navigateToRightLongPress,
|
||||||
)
|
)
|
||||||
+11
-9
@@ -16,12 +16,14 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.common
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppInfoCard(
|
fun AppInfoCard(
|
||||||
@@ -29,24 +31,24 @@ fun AppInfoCard(
|
|||||||
) {
|
) {
|
||||||
StyledList(title = stringResource(R.string.about)) {
|
StyledList(title = stringResource(R.string.about)) {
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.version),
|
contentText = stringResource(R.string.version),
|
||||||
supportingText = BuildConfig.VERSION_NAME,
|
supportingText = BuildConfig.VERSION_NAME,
|
||||||
onClick = navigateToReleaseNotesScreen
|
onClick = navigateToReleaseNotesScreen
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.version_code),
|
contentText = stringResource(R.string.version_code),
|
||||||
supportingText = BuildConfig.VERSION_CODE.toString(),
|
supportingText = BuildConfig.VERSION_CODE.toString(),
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.flavor),
|
contentText = stringResource(R.string.flavor),
|
||||||
supportingText = BuildConfig.FLAVOR,
|
supportingText = BuildConfig.FLAVOR,
|
||||||
)
|
)
|
||||||
|
|
||||||
StyledListItem(
|
StyledListItem(
|
||||||
contentText = stringResource(R.string.build_type),
|
contentText = stringResource(R.string.build_type),
|
||||||
supportingText = BuildConfig.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.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.primitives
|
||||||
|
|
||||||
import android.graphics.RuntimeShader
|
import android.graphics.RuntimeShader
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.primitives
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -62,7 +62,7 @@ import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfirmationDialog(
|
fun StyledConfirmationDialog(
|
||||||
showDialog: MutableState<Boolean>,
|
showDialog: MutableState<Boolean>,
|
||||||
title: String,
|
title: String,
|
||||||
message: String,
|
message: String,
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package 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_NO
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
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.animateDp
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.primitives
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.primitives
|
||||||
|
|
||||||
import android.graphics.RenderEffect
|
import android.graphics.RenderEffect
|
||||||
import android.graphics.Shader
|
import android.graphics.Shader
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.primitives
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package me.kavishdevar.librepods.presentation.components
|
package me.kavishdevar.librepods.presentation.components.primitives
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
@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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.PurchaseScreen
|
||||||
import me.kavishdevar.librepods.presentation.screens.ReleaseNotesScreen
|
import me.kavishdevar.librepods.presentation.screens.ReleaseNotesScreen
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.AccessibilitySettingsScreen
|
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.AppleSettingsRoute
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.CallControlScreen
|
import me.kavishdevar.librepods.presentation.screens.apple.CallControlScreen
|
||||||
import me.kavishdevar.librepods.presentation.screens.apple.DebugRoute
|
import me.kavishdevar.librepods.presentation.screens.apple.DebugRoute
|
||||||
@@ -77,7 +76,7 @@ fun RenderScreenContent(
|
|||||||
updatesShown: () -> Unit,
|
updatesShown: () -> Unit,
|
||||||
onboardingComplete: () -> 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)
|
fun navigateToPurchase() = navigate(Screen.Purchase)
|
||||||
|
|
||||||
val navigateBack: (() -> Unit)? = if (backStack.size > 1) {
|
val navigateBack: (() -> Unit)? = if (backStack.size > 1) {
|
||||||
@@ -129,7 +128,6 @@ fun RenderScreenContent(
|
|||||||
navigateToLeftLongPress = { navigate(Screen.LongPress(screen.macAddress, left)) },
|
navigateToLeftLongPress = { navigate(Screen.LongPress(screen.macAddress, left)) },
|
||||||
navigateToRightLongPress = { navigate(Screen.LongPress(screen.macAddress, right)) },
|
navigateToRightLongPress = { navigate(Screen.LongPress(screen.macAddress, right)) },
|
||||||
navigateToPurchase = ::navigateToPurchase,
|
navigateToPurchase = ::navigateToPurchase,
|
||||||
navigateToAdaptiveStrength = { navigate(Screen.AdaptiveStrength(screen.macAddress)) },
|
|
||||||
navigateToEqualizer = { navigate(Screen.Equalizer(screen.macAddress)) },
|
navigateToEqualizer = { navigate(Screen.Equalizer(screen.macAddress)) },
|
||||||
navigateToHeadTracking = { navigate(Screen.HeadTracking(screen.macAddress)) },
|
navigateToHeadTracking = { navigate(Screen.HeadTracking(screen.macAddress)) },
|
||||||
navigateToAccessibility = { navigate(Screen.Accessibility(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)
|
Screen.OpenSourceLicenses -> OpenSourceLicensesScreen(navigateBack)
|
||||||
|
|
||||||
is Screen.UpdateHearingTest -> {
|
is Screen.UpdateHearingTest -> {
|
||||||
|
|||||||
@@ -55,11 +55,6 @@ sealed interface Screen: NavKey {
|
|||||||
override val macAddress: MacAddress
|
override val macAddress: MacAddress
|
||||||
): DeviceScreen
|
): DeviceScreen
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class AdaptiveStrength(
|
|
||||||
override val macAddress: MacAddress
|
|
||||||
): DeviceScreen
|
|
||||||
|
|
||||||
// @Serializable
|
// @Serializable
|
||||||
// data object CameraControl: Screen
|
// 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.TextFieldState
|
||||||
import androidx.compose.foundation.text.input.clearText
|
import androidx.compose.foundation.text.input.clearText
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -63,17 +62,17 @@ import com.kyant.backdrop.backdrops.layerBackdrop
|
|||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.AppInfoCard
|
import me.kavishdevar.librepods.presentation.components.common.AppInfoCard
|
||||||
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
|
import me.kavishdevar.librepods.presentation.components.common.DeviceInfoCard
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledBottomSheet
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledBottomSheet
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.NightTheme
|
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 me.kavishdevar.librepods.utils.XposedState
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSettingsScreen(
|
fun AppSettingsScreen(
|
||||||
viewModel: AppSettingsViewModel = viewModel(),
|
viewModel: AppSettingsViewModel = viewModel(),
|
||||||
|
|||||||
+5
-5
@@ -20,11 +20,11 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
|
||||||
import kotlin.time.Duration.Companion.seconds
|
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.BaseCapability
|
||||||
import me.kavishdevar.librepods.devices.ConnectionState
|
import me.kavishdevar.librepods.devices.ConnectionState
|
||||||
import me.kavishdevar.librepods.devices.Device
|
import me.kavishdevar.librepods.devices.Device
|
||||||
import me.kavishdevar.librepods.presentation.components.NoiseControlSettings
|
import me.kavishdevar.librepods.presentation.components.apple.NoiseControlSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
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.LibraryInlineDetail
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryRow
|
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryRow
|
||||||
import me.kavishdevar.librepods.R
|
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.theme.DesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OpenSourceLicensesScreen(
|
fun OpenSourceLicensesScreen(
|
||||||
|
|||||||
+6
-12
@@ -21,13 +21,9 @@ package me.kavishdevar.librepods.presentation.screens
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
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.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle
|
import me.kavishdevar.librepods.presentation.components.primitives.MaterialButtonStyle
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
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.LocalDesignSystem
|
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
|
||||||
import me.kavishdevar.librepods.utils.XposedState
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
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.bluetooth.att.ATTHandle
|
||||||
import me.kavishdevar.librepods.devices.AirPodsSpecs
|
import me.kavishdevar.librepods.devices.AirPodsSpecs
|
||||||
import me.kavishdevar.librepods.devices.BaseCapability
|
import me.kavishdevar.librepods.devices.BaseCapability
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
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 me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
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
|
package me.kavishdevar.librepods.presentation.screens.apple
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
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.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||||
import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
||||||
import me.kavishdevar.librepods.devices.AirPodsSpecs
|
import me.kavishdevar.librepods.devices.AirPodsSpecs
|
||||||
|
import me.kavishdevar.librepods.devices.AppleSettings
|
||||||
import me.kavishdevar.librepods.devices.BaseCapability
|
import me.kavishdevar.librepods.devices.BaseCapability
|
||||||
import me.kavishdevar.librepods.presentation.components.AboutCard
|
import me.kavishdevar.librepods.presentation.components.apple.AboutCard
|
||||||
import me.kavishdevar.librepods.presentation.components.AudioSettings
|
import me.kavishdevar.librepods.presentation.components.apple.AudioSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.BatteryView
|
import me.kavishdevar.librepods.presentation.components.apple.BatteryView
|
||||||
import me.kavishdevar.librepods.presentation.components.CallControlSettings
|
import me.kavishdevar.librepods.presentation.components.apple.CallControlSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.ConnectionSettings
|
import me.kavishdevar.librepods.presentation.components.apple.ConnectionSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.HearingHealthSettings
|
import me.kavishdevar.librepods.presentation.components.apple.HearingHealthSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.NoiseControlSettings
|
import me.kavishdevar.librepods.presentation.components.apple.NoiseControlSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.PressAndHoldSettings
|
import me.kavishdevar.librepods.presentation.components.apple.PressAndHoldSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
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.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppleSettingsRoute(
|
fun AppleSettingsRoute(
|
||||||
@@ -71,7 +85,6 @@ fun AppleSettingsRoute(
|
|||||||
navigateToLeftLongPress: () -> Unit,
|
navigateToLeftLongPress: () -> Unit,
|
||||||
navigateToRightLongPress: () -> Unit,
|
navigateToRightLongPress: () -> Unit,
|
||||||
navigateToPurchase: () -> Unit,
|
navigateToPurchase: () -> Unit,
|
||||||
navigateToAdaptiveStrength: () -> Unit,
|
|
||||||
navigateToEqualizer: () -> Unit,
|
navigateToEqualizer: () -> Unit,
|
||||||
navigateToHeadTracking: () -> Unit,
|
navigateToHeadTracking: () -> Unit,
|
||||||
navigateToAccessibility: () -> Unit,
|
navigateToAccessibility: () -> Unit,
|
||||||
@@ -94,7 +107,8 @@ fun AppleSettingsRoute(
|
|||||||
|
|
||||||
writeATTCharacteristic = viewModel::writeATTCharacteristic,
|
writeATTCharacteristic = viewModel::writeATTCharacteristic,
|
||||||
|
|
||||||
// onAutomaticEarDetectionChanged = viewModel::setAutomaticEarDetectionEnabled,
|
updateSettings = viewModel::updateSettings,
|
||||||
|
|
||||||
// onAutomaticConnectionChanged = viewModel::setAutomaticConnectionEnabled,
|
// onAutomaticConnectionChanged = viewModel::setAutomaticConnectionEnabled,
|
||||||
disconnect = viewModel::disconnect,
|
disconnect = viewModel::disconnect,
|
||||||
|
|
||||||
@@ -105,7 +119,6 @@ fun AppleSettingsRoute(
|
|||||||
navigateToLeftLongPress = navigateToLeftLongPress,
|
navigateToLeftLongPress = navigateToLeftLongPress,
|
||||||
navigateToRightLongPress = navigateToRightLongPress,
|
navigateToRightLongPress = navigateToRightLongPress,
|
||||||
navigateToPurchase = navigateToPurchase,
|
navigateToPurchase = navigateToPurchase,
|
||||||
navigateToAdaptiveStrength = navigateToAdaptiveStrength,
|
|
||||||
navigateToEqualizer = navigateToEqualizer,
|
navigateToEqualizer = navigateToEqualizer,
|
||||||
navigateToHeadTracking = navigateToHeadTracking,
|
navigateToHeadTracking = navigateToHeadTracking,
|
||||||
navigateToAccessibility = navigateToAccessibility,
|
navigateToAccessibility = navigateToAccessibility,
|
||||||
@@ -130,6 +143,8 @@ fun AppleSettingsScreen(
|
|||||||
|
|
||||||
writeATTCharacteristic: (ATTHandle, ByteArray) -> Unit,
|
writeATTCharacteristic: (ATTHandle, ByteArray) -> Unit,
|
||||||
|
|
||||||
|
updateSettings: (transform: (AppleSettings) -> AppleSettings) -> Unit,
|
||||||
|
|
||||||
// onAutomaticEarDetectionChanged: (Boolean) -> Unit,
|
// onAutomaticEarDetectionChanged: (Boolean) -> Unit,
|
||||||
// onAutomaticConnectionChanged: (Boolean) -> Unit,
|
// onAutomaticConnectionChanged: (Boolean) -> Unit,
|
||||||
|
|
||||||
@@ -142,7 +157,6 @@ fun AppleSettingsScreen(
|
|||||||
navigateToLeftLongPress: () -> Unit,
|
navigateToLeftLongPress: () -> Unit,
|
||||||
navigateToRightLongPress: () -> Unit,
|
navigateToRightLongPress: () -> Unit,
|
||||||
navigateToPurchase: () -> Unit,
|
navigateToPurchase: () -> Unit,
|
||||||
navigateToAdaptiveStrength: () -> Unit,
|
|
||||||
navigateToEqualizer: () -> Unit,
|
navigateToEqualizer: () -> Unit,
|
||||||
navigateToHeadTracking: () -> Unit,
|
navigateToHeadTracking: () -> Unit,
|
||||||
navigateToAccessibility: () -> 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)) {
|
if (baseCapabilities.contains(BaseCapability.LISTENING_MODE)) {
|
||||||
item(key = "spacer_noise") {
|
item(key = "spacer_noise") {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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)) {
|
if (baseCapabilities.contains(BaseCapability.HRM)) {
|
||||||
@@ -319,30 +380,18 @@ fun AppleSettingsScreen(
|
|||||||
|
|
||||||
item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "audio") {
|
item(key = "audio") {
|
||||||
val adaptiveVolumeCapability =
|
val adaptiveVolumeCapability = baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME)
|
||||||
baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME)
|
val conversationalAwarenessCapability = baseCapabilities.contains(BaseCapability.CONVERSATION_AWARENESS)
|
||||||
val conversationalAwarenessCapability =
|
val loudSoundReductionCapability = baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION)
|
||||||
baseCapabilities.contains(BaseCapability.CONVERSATION_AWARENESS)
|
|
||||||
val loudSoundReductionCapability =
|
|
||||||
baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION)
|
|
||||||
val adaptiveAudioCapability =
|
|
||||||
baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME)
|
|
||||||
|
|
||||||
val adaptiveVolumeChecked =
|
val adaptiveVolumeChecked = state.controlStates[ControlCommandIdentifier.ADAPTIVE_VOLUME_CONFIG]?.getOrNull(0) == 0x01.toByte()
|
||||||
state.controlStates[ControlCommandIdentifier.ADAPTIVE_VOLUME_CONFIG]?.getOrNull(
|
val conversationalAwarenessChecked = state.controlStates[ControlCommandIdentifier.CONVERSATION_DETECT_CONFIG]?.getOrNull(0) == 0x01.toByte()
|
||||||
0
|
|
||||||
) == 0x01.toByte()
|
|
||||||
val conversationalAwarenessChecked =
|
|
||||||
state.controlStates[ControlCommandIdentifier.CONVERSATION_DETECT_CONFIG]?.getOrNull(
|
|
||||||
0
|
|
||||||
) == 0x01.toByte()
|
|
||||||
|
|
||||||
AudioSettings(
|
AudioSettings(
|
||||||
adaptiveVolumeCapability = adaptiveVolumeCapability,
|
adaptiveVolumeCapability = adaptiveVolumeCapability,
|
||||||
conversationalAwarenessCapability = conversationalAwarenessCapability,
|
conversationalAwarenessCapability = conversationalAwarenessCapability,
|
||||||
loudSoundReductionCapability = loudSoundReductionCapability,
|
loudSoundReductionCapability = loudSoundReductionCapability,
|
||||||
adaptiveAudioCapability = adaptiveAudioCapability,
|
customEqCapability = metadata.version3.isNotBlank() && metadata.version3.first().digitToInt() >= 9,
|
||||||
customEqCapability = metadata.version3.startsWith("9"),
|
|
||||||
adaptiveVolumeChecked = adaptiveVolumeChecked,
|
adaptiveVolumeChecked = adaptiveVolumeChecked,
|
||||||
onAdaptiveVolumeCheckedChange = { checked ->
|
onAdaptiveVolumeCheckedChange = { checked ->
|
||||||
setControlCommandBoolean(
|
setControlCommandBoolean(
|
||||||
@@ -364,7 +413,6 @@ fun AppleSettingsScreen(
|
|||||||
byteArrayOf(if (checked) 0x01.toByte() else 0x00.toByte())
|
byteArrayOf(if (checked) 0x01.toByte() else 0x00.toByte())
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigateToAdaptiveStrength = navigateToAdaptiveStrength,
|
|
||||||
navigateToEqualizer = navigateToEqualizer,
|
navigateToEqualizer = navigateToEqualizer,
|
||||||
vendorIdHook = uiState.vendorIdHook,
|
vendorIdHook = uiState.vendorIdHook,
|
||||||
isPremium = uiState.isPremium
|
isPremium = uiState.isPremium
|
||||||
@@ -374,10 +422,28 @@ fun AppleSettingsScreen(
|
|||||||
item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "connection") {
|
item(key = "connection") {
|
||||||
ConnectionSettings(
|
ConnectionSettings(
|
||||||
automaticEarDetectionEnabled = state.controlStates[ControlCommandIdentifier.EAR_DETECTION_CONFIG]?.getOrNull(0) == 0x01.toByte(),
|
automaticEarDetectionEnabled = state.controlStates[ControlCommandIdentifier.EAR_DETECTION_CONFIG]?.getOrNull(0) == 0x01.toByte() || settings.earDetectionEnabled,
|
||||||
onAutomaticEarDetectionChanged = { setControlCommandBoolean(ControlCommandIdentifier.EAR_DETECTION_CONFIG, it) },
|
onAutomaticEarDetectionChanged = { enabled -> setControlCommandBoolean(ControlCommandIdentifier.EAR_DETECTION_CONFIG, enabled); updateSettings { it.copy(earDetectionEnabled = enabled) } },
|
||||||
automaticConnectionEnabled = state.controlStates[ControlCommandIdentifier.SMART_ROUTING_MODE]?.getOrNull(0) == 0x01.toByte(),
|
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 = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
item(key = "off_listening") {
|
item(key = "off_listening") {
|
||||||
val id = ControlCommandIdentifier.ALLOW_OFF_OPTION
|
val id = ControlCommandIdentifier.ALLOW_OFF_OPTION
|
||||||
@@ -487,7 +553,16 @@ fun AppleSettingsScreen(
|
|||||||
item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
|
item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||||
|
|
||||||
if (uiState.appSettings.debugMode) {
|
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(
|
StyledListItem(
|
||||||
contentText = "debug",
|
contentText = "debug",
|
||||||
onClick = navigateToDebugScreen
|
onClick = navigateToDebugScreen
|
||||||
@@ -517,6 +592,8 @@ fun AppleSettingsScreenPreviewApple() {
|
|||||||
setControlCommandBoolean = { _, _ -> },
|
setControlCommandBoolean = { _, _ -> },
|
||||||
writeATTCharacteristic = { _, _ -> },
|
writeATTCharacteristic = { _, _ -> },
|
||||||
|
|
||||||
|
updateSettings = { _ -> },
|
||||||
|
|
||||||
disconnect = {},
|
disconnect = {},
|
||||||
|
|
||||||
navigateBack = null,
|
navigateBack = null,
|
||||||
@@ -526,7 +603,6 @@ fun AppleSettingsScreenPreviewApple() {
|
|||||||
navigateToLeftLongPress = {},
|
navigateToLeftLongPress = {},
|
||||||
navigateToRightLongPress = {},
|
navigateToRightLongPress = {},
|
||||||
navigateToPurchase = {},
|
navigateToPurchase = {},
|
||||||
navigateToAdaptiveStrength = {},
|
|
||||||
navigateToEqualizer = {},
|
navigateToEqualizer = {},
|
||||||
navigateToHeadTracking = {},
|
navigateToHeadTracking = {},
|
||||||
navigateToAccessibility = {},
|
navigateToAccessibility = {},
|
||||||
@@ -559,6 +635,8 @@ fun AppleSettingsScreenPreviewMaterial() {
|
|||||||
setControlCommandBoolean = { _, _ -> },
|
setControlCommandBoolean = { _, _ -> },
|
||||||
writeATTCharacteristic = { _, _ -> },
|
writeATTCharacteristic = { _, _ -> },
|
||||||
|
|
||||||
|
updateSettings = { _ -> },
|
||||||
|
|
||||||
disconnect = {},
|
disconnect = {},
|
||||||
|
|
||||||
navigateBack = null,
|
navigateBack = null,
|
||||||
@@ -568,7 +646,6 @@ fun AppleSettingsScreenPreviewMaterial() {
|
|||||||
navigateToLeftLongPress = {},
|
navigateToLeftLongPress = {},
|
||||||
navigateToRightLongPress = {},
|
navigateToRightLongPress = {},
|
||||||
navigateToPurchase = {},
|
navigateToPurchase = {},
|
||||||
navigateToAdaptiveStrength = {},
|
|
||||||
navigateToEqualizer = {},
|
navigateToEqualizer = {},
|
||||||
navigateToHeadTracking = {},
|
navigateToHeadTracking = {},
|
||||||
navigateToAccessibility = {},
|
navigateToAccessibility = {},
|
||||||
|
|||||||
+3
-3
@@ -19,9 +19,9 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
|
||||||
@Composable
|
@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.bluetooth.aacp.types.SensorDataWxBuddyPayload
|
||||||
import me.kavishdevar.librepods.devices.DeviceComponent
|
import me.kavishdevar.librepods.devices.DeviceComponent
|
||||||
import me.kavishdevar.librepods.devices.PacketDestination
|
import me.kavishdevar.librepods.devices.PacketDestination
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.icons.richText
|
import me.kavishdevar.librepods.presentation.icons.richText
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
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 com.kyant.backdrop.highlight.Highlight
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
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.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
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.flow.debounce
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle
|
import me.kavishdevar.librepods.presentation.components.primitives.MaterialButtonStyle
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
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.HearingAidSettings
|
||||||
import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse
|
import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse
|
||||||
import me.kavishdevar.librepods.bluetooth.att.types.sendHearingAidSettings
|
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.components.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
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.aacp.types.ControlCommandIdentifier
|
||||||
import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse
|
import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse
|
||||||
import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings
|
import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings
|
||||||
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledConfirmationDialog
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
|
||||||
private const val TAG = "HearingAidScreen"
|
private const val TAG = "HearingAidScreen"
|
||||||
@@ -187,7 +187,7 @@ fun HearingAidScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfirmationDialog(
|
StyledConfirmationDialog(
|
||||||
showDialog = showDialog,
|
showDialog = showDialog,
|
||||||
title = "Enable Hearing Aid",
|
title = "Enable Hearing Aid",
|
||||||
message = "Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.",
|
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.R
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||||
import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+78
-59
@@ -27,10 +27,12 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
import androidx.compose.material3.toShape
|
import androidx.compose.material3.toShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalLocale
|
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.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledIconButton
|
import me.kavishdevar.librepods.devices.AppleSettings
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
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.LocalIcons
|
||||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
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.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HeartRateRoute(
|
fun HeartRateRoute(
|
||||||
@@ -68,7 +76,8 @@ fun HeartRateRoute(
|
|||||||
navigateBack = navigateBack,
|
navigateBack = navigateBack,
|
||||||
startHr = viewModel::startHr,
|
startHr = viewModel::startHr,
|
||||||
stopHr = viewModel::stopHr,
|
stopHr = viewModel::stopHr,
|
||||||
setHrRange = viewModel::setHrRange
|
setHrRange = viewModel::setHrRange,
|
||||||
|
updateSettings = viewModel::updateSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +88,11 @@ fun HeartRateScreen(
|
|||||||
navigateBack: (() -> Unit)?,
|
navigateBack: (() -> Unit)?,
|
||||||
startHr: () -> Unit,
|
startHr: () -> Unit,
|
||||||
stopHr: () -> Unit,
|
stopHr: () -> Unit,
|
||||||
setHrRange: (ClosedRange<Long>) -> Unit
|
setHrRange: (ClosedRange<Long>) -> Unit,
|
||||||
|
updateSettings: (transform: (AppleSettings) -> AppleSettings) -> Unit,
|
||||||
) {
|
) {
|
||||||
val state = uiState.state
|
val state = uiState.state
|
||||||
|
val settings = uiState.settings
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
@@ -129,59 +140,7 @@ fun HeartRateScreen(
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(topPadding))
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
|
|
||||||
val sliderValue = remember {
|
val healthPermissions = rememberPermissionState(HealthPermission.getWritePermission(HeartRateRecord::class))
|
||||||
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
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
AnimatedVisibility(state.currentHeartRate != null) {
|
AnimatedVisibility(state.currentHeartRate != null) {
|
||||||
if (state.currentHeartRate == null) return@AnimatedVisibility
|
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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
+3
-3
@@ -17,9 +17,9 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+10
-7
@@ -45,11 +45,11 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
|||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||||
import me.kavishdevar.librepods.data.StemAction
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
import kotlin.experimental.and
|
import kotlin.experimental.and
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
@@ -78,6 +78,9 @@ fun LongPress(
|
|||||||
|
|
||||||
val scrollState = rememberScrollState()
|
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(
|
StyledScaffold(
|
||||||
title = name,
|
title = name,
|
||||||
navigateBack = navigateBack
|
navigateBack = navigateBack
|
||||||
@@ -98,7 +101,7 @@ fun LongPress(
|
|||||||
selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES,
|
selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setLongPressAction(
|
viewModel.setLongPressAction(
|
||||||
name,
|
side,
|
||||||
StemAction.CYCLE_NOISE_CONTROL_MODES
|
StemAction.CYCLE_NOISE_CONTROL_MODES
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -109,7 +112,7 @@ fun LongPress(
|
|||||||
selected = longPressAction == StemAction.DIGITAL_ASSISTANT,
|
selected = longPressAction == StemAction.DIGITAL_ASSISTANT,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setLongPressAction(
|
viewModel.setLongPressAction(
|
||||||
name,
|
side,
|
||||||
StemAction.DIGITAL_ASSISTANT
|
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.platform.LocalLocale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import me.kavishdevar.librepods.BuildConfig
|
import me.kavishdevar.librepods.BuildConfig
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.data.recording.Recording
|
import me.kavishdevar.librepods.data.recording.Recording
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|||||||
+2
-2
@@ -39,8 +39,8 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledInputField
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
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.TransparencySettings
|
||||||
import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse
|
import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse
|
||||||
import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings
|
import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
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.HearingAidSettings
|
||||||
import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse
|
import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse
|
||||||
import me.kavishdevar.librepods.bluetooth.att.types.sendHearingAidSettings
|
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.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+3
-3
@@ -18,9 +18,9 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.AppInfoCard
|
import me.kavishdevar.librepods.presentation.components.common.AppInfoCard
|
||||||
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
|
import me.kavishdevar.librepods.presentation.components.common.DeviceInfoCard
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotSupportedPage(
|
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.rememberMultiplePermissionsState
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledList
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItem
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
|
||||||
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
|||||||
+4
@@ -198,5 +198,9 @@ class AppleViewModel(
|
|||||||
_heartRateRange.value = range
|
_heartRateRange.value = range
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateSettings( transform : (AppleSettings) -> AppleSettings) {
|
||||||
|
device.updateSettings(transform)
|
||||||
|
}
|
||||||
|
|
||||||
fun sendRawPacket(data: ByteArray): Boolean = device.sendRawPacket(data)
|
fun sendRawPacket(data: ByteArray): Boolean = device.sendRawPacket(data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package me.kavishdevar.librepods.repository
|
package me.kavishdevar.librepods.repository
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -16,6 +17,12 @@ class AppDataRepository(
|
|||||||
private val settingsDao: AppSettingsDao,
|
private val settingsDao: AppSettingsDao,
|
||||||
private val stateDao: AppStateDao,
|
private val stateDao: AppStateDao,
|
||||||
) {
|
) {
|
||||||
|
private val initialized = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
|
suspend fun awaitInitialized() {
|
||||||
|
initialized.await()
|
||||||
|
}
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
private val _settings = MutableStateFlow(AppSettingsEntity())
|
private val _settings = MutableStateFlow(AppSettingsEntity())
|
||||||
@@ -28,6 +35,7 @@ class AppDataRepository(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
_settings.value = settingsDao.get() ?: AppSettingsEntity()
|
_settings.value = settingsDao.get() ?: AppSettingsEntity()
|
||||||
_state.value = stateDao.get() ?: AppStateEntity()
|
_state.value = stateDao.get() ?: AppStateEntity()
|
||||||
|
initialized.complete(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,14 @@ import android.os.IBinder
|
|||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.os.ext.SdkExtensions
|
import android.os.ext.SdkExtensions
|
||||||
import android.provider.Settings
|
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.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.health.connect.client.permission.HealthPermission
|
import androidx.health.connect.client.permission.HealthPermission
|
||||||
import androidx.health.connect.client.records.HeartRateRecord
|
import androidx.health.connect.client.records.HeartRateRecord
|
||||||
@@ -111,8 +116,53 @@ class LibrePodsService : Service() {
|
|||||||
(application as LibrePodsApplication).healthConnectClient
|
(application as LibrePodsApplication).healthConnectClient
|
||||||
}
|
}
|
||||||
|
|
||||||
private val hasConnectedToAACP by lazy {
|
private var isCallRinging = false
|
||||||
appDataRepository.state.value.hasConnectedToAACP
|
|
||||||
|
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() {
|
override fun onCreate() {
|
||||||
@@ -132,6 +182,14 @@ class LibrePodsService : Service() {
|
|||||||
localMac = null // TODO: smart routing. MAC_ADDRESS message gives host mac?
|
localMac = null // TODO: smart routing. MAC_ADDRESS message gives host mac?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val telephonyManager = getSystemService(TelephonyManager::class.java)
|
||||||
|
|
||||||
|
telephonyManager.registerTelephonyCallback(
|
||||||
|
mainExecutor,
|
||||||
|
telephonyCallback
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
startForegroundNotification()
|
startForegroundNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,7 +596,6 @@ class LibrePodsService : Service() {
|
|||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun observeAppSettings(): Job {
|
fun observeAppSettings(): Job {
|
||||||
var oldAppSettings: AppSettingsEntity = appDataRepository.settings.value
|
var oldAppSettings: AppSettingsEntity = appDataRepository.settings.value
|
||||||
|
|
||||||
@@ -562,9 +619,12 @@ class LibrePodsService : Service() {
|
|||||||
|
|
||||||
|
|
||||||
if (state.aacpPackets != previousState.aacpPackets) {
|
if (state.aacpPackets != previousState.aacpPackets) {
|
||||||
if (!hasConnectedToAACP) {
|
if (!appDataRepository.state.value.hasConnectedToAACP) {
|
||||||
appDataRepository.updateState { it.copy(hasConnectedToAACP = true) }
|
appDataRepository.updateState { it.copy(hasConnectedToAACP = true) }
|
||||||
}
|
}
|
||||||
|
if (appDataRepository.state.value.firstSuccessfulConnectionTime == null) {
|
||||||
|
appDataRepository.updateState { it.copy(firstSuccessfulConnectionTime = System.currentTimeMillis()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
@@ -655,7 +715,7 @@ class LibrePodsService : Service() {
|
|||||||
TAG,
|
TAG,
|
||||||
"ear detection control command value: ${earDetectionCtrlCmdValue.toHexString()}"
|
"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")
|
Log.d(TAG, "ear detection enabled: $earDetectionEnabled")
|
||||||
if (earDetectionEnabled) {
|
if (earDetectionEnabled) {
|
||||||
processComponentStateChange(
|
processComponentStateChange(
|
||||||
@@ -695,10 +755,29 @@ class LibrePodsService : Service() {
|
|||||||
"conversational awareness state: ${state.conversationalAwarenessState}"
|
"conversational awareness state: ${state.conversationalAwarenessState}"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: multi-step volume change. implementation exists in linux rewrite; copy from there
|
|
||||||
when (state.conversationalAwarenessState) {
|
when (state.conversationalAwarenessState) {
|
||||||
1, 2 -> MediaController.startSpeaking()
|
1 -> {
|
||||||
6, 8, 9 -> MediaController.stopSpeaking()
|
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
|
// @Synchronized
|
||||||
fun startSpeaking() {
|
// 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")
|
// 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) {
|
// @Synchronized
|
||||||
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
// fun stopSpeaking() {
|
||||||
Log.d("MediaController", "Initial Volume: $initialVolume")
|
// Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume")
|
||||||
val targetVolume = if (relativeVolume) {
|
// if (initialVolume != null) {
|
||||||
(initialVolume!! * conversationalAwarenessVolume / 100)
|
// smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!)
|
||||||
} else if (initialVolume!! > (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)) {
|
// if (conversationalAwarenessPauseMusic) {
|
||||||
(audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)
|
// sendPlay()
|
||||||
} else {
|
// }
|
||||||
initialVolume!!
|
// initialVolume = null
|
||||||
}
|
// }
|
||||||
smoothVolumeTransition(initialVolume!!, targetVolume)
|
// }
|
||||||
if (conversationalAwarenessPauseMusic) {
|
|
||||||
sendPause(force = true)
|
@Synchronized
|
||||||
}
|
fun setVolume(volumePercent: Int) {
|
||||||
}
|
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||||
Log.d("MediaController", "Initial Volume: $initialVolume")
|
val targetVolume = maxVolume * volumePercent / 100
|
||||||
|
|
||||||
|
smoothVolumeTransition(
|
||||||
|
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
|
||||||
|
targetVolume,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stopSpeaking() {
|
fun startSpeaking() {
|
||||||
Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume")
|
if (initialVolume == null) {
|
||||||
if (initialVolume != null) {
|
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||||
smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!)
|
|
||||||
if (conversationalAwarenessPauseMusic) {
|
|
||||||
sendPlay()
|
|
||||||
}
|
|
||||||
initialVolume = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
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">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="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">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="advanced_options">Advanced Options</string>
|
||||||
<string name="set_identity_resolving_key">Set Identity Resolving Key (IRK)</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>
|
<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="use_highest_refresh_rate">Use highest refresh rate available</string>
|
||||||
<string name="debug_mode_description">Shows extra information and settings.</string>
|
<string name="debug_mode_description">Shows extra information and settings.</string>
|
||||||
<string name="interaction">Interaction</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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user